From 96032a87740f04da9748e30ea5a75d8094f186c5 Mon Sep 17 00:00:00 2001 From: euphoria-laxis Date: Wed, 13 Sep 2023 10:18:10 +0200 Subject: [PATCH 1/3] Rename utils package argon2 utils was not explicit enough when using package, argon2 describes exactly what this package does. --- {utils => argon2}/crypto.go | 2 +- {utils => argon2}/crypto_struct.go | 2 +- {utils => argon2}/crypto_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename {utils => argon2}/crypto.go (99%) rename {utils => argon2}/crypto_struct.go (96%) rename {utils => argon2}/crypto_test.go (98%) diff --git a/utils/crypto.go b/argon2/crypto.go similarity index 99% rename from utils/crypto.go rename to argon2/crypto.go index cba15de..e7a0840 100644 --- a/utils/crypto.go +++ b/argon2/crypto.go @@ -1,4 +1,4 @@ -package utils +package argon2 import ( "crypto/rand" diff --git a/utils/crypto_struct.go b/argon2/crypto_struct.go similarity index 96% rename from utils/crypto_struct.go rename to argon2/crypto_struct.go index 2694748..771ebc7 100644 --- a/utils/crypto_struct.go +++ b/argon2/crypto_struct.go @@ -1,4 +1,4 @@ -package utils +package argon2 import "errors" diff --git a/utils/crypto_test.go b/argon2/crypto_test.go similarity index 98% rename from utils/crypto_test.go rename to argon2/crypto_test.go index c3d078e..4035daa 100644 --- a/utils/crypto_test.go +++ b/argon2/crypto_test.go @@ -1,4 +1,4 @@ -package utils +package argon2 import ( "log" From 9f501e70359d28c10bced95b27681ad593f4a3a9 Mon Sep 17 00:00:00 2001 From: euphoria-laxis Date: Wed, 13 Sep 2023 10:20:11 +0200 Subject: [PATCH 2/3] Rename files for better understanding of files content --- argon2/{crypto.go => argon2.go} | 21 +++++++++++++++++++++ argon2/{crypto_test.go => argon2_test.go} | 0 argon2/crypto_struct.go | 23 ----------------------- 3 files changed, 21 insertions(+), 23 deletions(-) rename argon2/{crypto.go => argon2.go} (83%) rename argon2/{crypto_test.go => argon2_test.go} (100%) delete mode 100644 argon2/crypto_struct.go diff --git a/argon2/crypto.go b/argon2/argon2.go similarity index 83% rename from argon2/crypto.go rename to argon2/argon2.go index e7a0840..54ef9d2 100644 --- a/argon2/crypto.go +++ b/argon2/argon2.go @@ -4,11 +4,32 @@ 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) diff --git a/argon2/crypto_test.go b/argon2/argon2_test.go similarity index 100% rename from argon2/crypto_test.go rename to argon2/argon2_test.go diff --git a/argon2/crypto_struct.go b/argon2/crypto_struct.go deleted file mode 100644 index 771ebc7..0000000 --- a/argon2/crypto_struct.go +++ /dev/null @@ -1,23 +0,0 @@ -package argon2 - -import "errors" - -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, - } -) From 2c0cdb2188ad22841aa00aaa0e4a590824fe9638 Mon Sep 17 00:00:00 2001 From: euphoria-laxis Date: Wed, 13 Sep 2023 11:11:23 +0200 Subject: [PATCH 3/3] Separate encoder and decoder and add configurable options --- argon2/argon2.go | 104 ------------------------------------------ argon2/argon2_test.go | 28 +++++++++--- argon2/decoder.go | 66 +++++++++++++++++++++++++++ argon2/encoder.go | 64 ++++++++++++++++++++++++++ argon2/options.go | 55 ++++++++++++++++++++++ 5 files changed, 206 insertions(+), 111 deletions(-) delete mode 100644 argon2/argon2.go create mode 100644 argon2/decoder.go create mode 100644 argon2/encoder.go create mode 100644 argon2/options.go 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 + } +}