diff --git a/argon2/argon2.go b/argon2/argon2.go deleted file mode 100644 index 54ef9d2..0000000 --- a/argon2/argon2.go +++ /dev/null @@ -1,104 +0,0 @@ -package argon2 - -import ( - "crypto/rand" - "crypto/subtle" - "encoding/base64" - "errors" - "fmt" - "golang.org/x/crypto/argon2" - "strings" -) - -type params struct { - memory uint32 - iterations uint32 - parallelism uint8 - saltLength uint32 - keyLength uint32 -} - -var ( - ErrInvalidHash = errors.New("the encoded hash is not in the correct format") - ErrIncompatibleVersion = errors.New("incompatible version of argon2") - p = ¶ms{ - memory: 64 * 1024, - iterations: 3, - parallelism: 2, - saltLength: 16, - keyLength: 32, - } -) - -func generateRandomBytes(n uint32) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - if err != nil { - return nil, err - } - - return b, nil -} - -func decodeHash(encodedHash string) (p *params, salt, hash []byte, err error) { - vals := strings.Split(encodedHash, "$") - if len(vals) != 6 { - return nil, nil, nil, ErrInvalidHash - } - var version int - _, err = fmt.Sscanf(vals[2], "v=%d", &version) - if err != nil { - return nil, nil, nil, err - } - if version != argon2.Version { - return nil, nil, nil, ErrIncompatibleVersion - } - p = ¶ms{} - _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism) - if err != nil { - return nil, nil, nil, err - } - salt, err = base64.RawStdEncoding.DecodeString(vals[4]) - if err != nil { - return nil, nil, nil, err - } - p.saltLength = uint32(len(salt)) - - hash, err = base64.RawStdEncoding.DecodeString(vals[5]) - if err != nil { - return nil, nil, nil, err - } - p.keyLength = uint32(len(hash)) - - return p, salt, hash, nil -} - -func CompareStringToArgon2Hash(password string, hashedPassword string) (match bool, err error) { - p, salt, hash, err := decodeHash(hashedPassword) - if err != nil { - return false, err - } - otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength) - if subtle.ConstantTimeCompare(hash, otherHash) == 1 { - return true, nil - } - return false, nil -} - -func HashStringArgon2(password string) (encodedHash string, err error) { - salt, err := generateRandomBytes(p.saltLength) - if err != nil { - return "", err - } - hash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength) - b64Salt := base64.RawStdEncoding.EncodeToString(salt) - b64Hash := base64.RawStdEncoding.EncodeToString(hash) - encodedHash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.memory, p.iterations, p.parallelism, b64Salt, b64Hash) - - return encodedHash, nil -} - -func RandomString(s int) (string, error) { - b, err := generateRandomBytes(uint32(s)) - return base64.URLEncoding.EncodeToString(b), err -} diff --git a/argon2/argon2_test.go b/argon2/argon2_test.go index 4035daa..e164485 100644 --- a/argon2/argon2_test.go +++ b/argon2/argon2_test.go @@ -7,24 +7,37 @@ import ( var ( randomString, hashedString string + opts []OptFunc ) -func TestHashStringArgon2(t *testing.T) { +func TestOptions(t *testing.T) { + opts = []OptFunc{ + SetMemory(32 * 1024), // 32 bits + SetParallelism(4), // 4 concurrent actions + SetKeyLength(32), // key length + SetSaltLength(32), // salt length + SetIterations(4), // 4 iterations, should be fast since there's 4 concurrent actions + } +} + +func TestEncoder(t *testing.T) { var err error - randomString, err = RandomString(32) + encoder, _ := NewEncoder(opts...) + randomString, err = encoder.RandomString(32) if err != nil { log.Print(err) t.Fail() } - hashedString, err = HashStringArgon2(randomString) + hashedString, err = encoder.HashString(randomString) if err != nil { log.Print(err) t.Fail() } } -func TestCompareStringToArgon2Hash(t *testing.T) { - match, err := CompareStringToArgon2Hash(randomString, hashedString) +func TestDecoder(t *testing.T) { + decoder, _ := NewDecoder(opts...) + match, err := decoder.CompareStringToHash(randomString, hashedString) if err != nil { log.Print(err) t.Fail() @@ -34,12 +47,13 @@ func TestCompareStringToArgon2Hash(t *testing.T) { log.Println("passwords should match") t.Fail() } - randomString, err = RandomString(32) + encoder, _ := NewEncoder(opts...) + randomString, err = encoder.RandomString(32) if err != nil { log.Print(err) t.Fail() } - match, err = CompareStringToArgon2Hash(randomString, hashedString) + match, err = decoder.CompareStringToHash(randomString, hashedString) if err != nil { log.Print(err) t.Fail() diff --git a/argon2/decoder.go b/argon2/decoder.go new file mode 100644 index 0000000..dfd52b9 --- /dev/null +++ b/argon2/decoder.go @@ -0,0 +1,66 @@ +package argon2 + +import ( + "crypto/subtle" + "encoding/base64" + "fmt" + "golang.org/x/crypto/argon2" + "strings" +) + +type Decoder struct { + Options +} + +func NewDecoder(opts ...OptFunc) (*Decoder, *Options) { + o := defaultOptions + for _, fn := range opts { + fn(&o) + } + + return &Decoder{o}, &o +} + +func (decoder *Decoder) decodeHash(encodedHash string) (d *Decoder, salt, hash []byte, err error) { + values := strings.Split(encodedHash, "$") + if len(values) != 6 { + return nil, nil, nil, ErrInvalidHash + } + var version int + _, err = fmt.Sscanf(values[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + _, err = fmt.Sscanf(values[3], "m=%d,t=%d,p=%d", &decoder.memory, &decoder.iterations, &decoder.parallelism) + if err != nil { + return nil, nil, nil, err + } + salt, err = base64.RawStdEncoding.DecodeString(values[4]) + if err != nil { + return nil, nil, nil, err + } + d = decoder + d.saltLength = uint32(len(salt)) + hash, err = base64.RawStdEncoding.DecodeString(values[5]) + if err != nil { + return nil, nil, nil, err + } + d.keyLength = uint32(len(hash)) + + return d, salt, hash, nil +} + +func (decoder *Decoder) CompareStringToHash(password string, hashedPassword string) (match bool, err error) { + p, salt, hash, err := decoder.decodeHash(hashedPassword) + if err != nil { + return false, err + } + otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength) + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return true, nil + } + return false, nil +} diff --git a/argon2/encoder.go b/argon2/encoder.go new file mode 100644 index 0000000..4d2575e --- /dev/null +++ b/argon2/encoder.go @@ -0,0 +1,64 @@ +package argon2 + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "golang.org/x/crypto/argon2" +) + +type Encoder struct { + Options +} + +func NewEncoder(opts ...OptFunc) (*Encoder, *Options) { + o := defaultOptions + for _, fn := range opts { + fn(&o) + } + + return &Encoder{o}, &o +} + +func (encoder *Encoder) generateRandomBytes(n uint32) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} + +func (encoder *Encoder) HashString(password string) (encodedHash string, err error) { + salt, err := encoder.generateRandomBytes(encoder.saltLength) + if err != nil { + return "", err + } + hash := argon2.IDKey( + []byte(password), + salt, + encoder.iterations, + encoder.memory, + encoder.parallelism, + encoder.keyLength, + ) + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Hash := base64.RawStdEncoding.EncodeToString(hash) + encodedHash = fmt.Sprintf( + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, + encoder.memory, + encoder.iterations, + encoder.parallelism, + b64Salt, + b64Hash, + ) + + return encodedHash, nil +} + +func (encoder *Encoder) RandomString(s int) (string, error) { + b, err := encoder.generateRandomBytes(uint32(s)) + return base64.URLEncoding.EncodeToString(b), err +} diff --git a/argon2/options.go b/argon2/options.go new file mode 100644 index 0000000..675fb39 --- /dev/null +++ b/argon2/options.go @@ -0,0 +1,55 @@ +package argon2 + +import "errors" + +type Options struct { + memory uint32 + iterations uint32 + parallelism uint8 + saltLength uint32 + keyLength uint32 +} + +var ( + ErrInvalidHash = errors.New("the encoded hash is not in the correct format") + ErrIncompatibleVersion = errors.New("incompatible version of argon2") + defaultOptions = Options{ + memory: 64 * 1024, + iterations: 3, + parallelism: 2, + saltLength: 16, + keyLength: 32, + } +) + +type OptFunc func(*Options) + +func SetMemory(memory uint32) OptFunc { + return func(options *Options) { + options.memory = memory + } +} + +func SetIterations(iterations uint32) OptFunc { + return func(options *Options) { + options.iterations = iterations + } +} + +func SetParallelism(parallelism uint8) OptFunc { + return func(options *Options) { + options.parallelism = parallelism + } +} + +func SetSaltLength(saltLength uint32) OptFunc { + return func(options *Options) { + options.saltLength = saltLength + } +} + +func SetKeyLength(keyLength uint32) OptFunc { + return func(options *Options) { + options.keyLength = keyLength + } +}