commit 79014dd21e08ecdad773b0d1fcb25e4cb66a22c3 Author: euphoria-laxis Date: Wed Jul 24 19:42:25 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8962e3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +### Go +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work + +### GoLand +.idea/ diff --git a/decoder/decoder.go b/decoder/decoder.go new file mode 100644 index 0000000..54f8ecc --- /dev/null +++ b/decoder/decoder.go @@ -0,0 +1,62 @@ +package decoder + +import ( + "crypto/subtle" + "encoding/base64" + "fmt" + "strings" + + "euphoria-laxis.fr/go-packages/argon2/encoder" + "golang.org/x/crypto/argon2" +) + +type Decoder struct{} + +func NewDecoder() *Decoder { + return new(Decoder) +} + +func (decoder *Decoder) decodeHash(encodedHash string) (o *encoder.Options, salt, hash []byte, err error) { + values := strings.Split(encodedHash, "$") + if len(values) != 6 { + return nil, nil, nil, encoder.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, encoder.ErrIncompatibleVersion + } + o = new(encoder.Options) + _, err = fmt.Sscanf(values[3], "m=%d,t=%d,p=%d", &o.Memory, &o.Iterations, &o.Parallelism) + if err != nil { + return nil, nil, nil, err + } + salt, err = base64.RawStdEncoding.DecodeString(values[4]) + if err != nil { + return nil, nil, nil, err + } + o.SaltLength = uint32(len(salt)) + + hash, err = base64.RawStdEncoding.DecodeString(values[5]) + if err != nil { + return nil, nil, nil, err + } + o.KeyLength = uint32(len(hash)) + + return o, 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/decoder/decoder_test.go b/decoder/decoder_test.go new file mode 100644 index 0000000..8211bd1 --- /dev/null +++ b/decoder/decoder_test.go @@ -0,0 +1,51 @@ +package decoder + +import ( + "testing" + + "euphoria-laxis.fr/go-packages/argon2/encoder" +) + +func TestDecoder(t *testing.T) { + opts := []encoder.OptFunc{ + encoder.SetMemory(32 * 1024), // 32 bits + encoder.SetParallelism(4), // 4 concurrent actions + encoder.SetKeyLength(32), // key length + encoder.SetSaltLength(32), // salt length + encoder.SetIterations(4), // 4 iterations, should be fast since there's 4 concurrent actions + } + e, _ := encoder.NewEncoder(opts...) + randomString, err := e.RandomString(32) + if err != nil { + t.Fatal(err) + } + var hashedString string + hashedString, err = e.HashString(randomString) + if err != nil { + t.Fatal(err) + } + d := NewDecoder() + var match bool + match, err = d.CompareStringToHash(randomString, hashedString) + if err != nil { + t.Fatal(err) + } + if !match { + t.Log("passwords comparison failed") + t.Log("passwords should match") + t.Fail() + } + randomString, err = e.RandomString(32) + if err != nil { + t.Fatal(err) + } + match, err = d.CompareStringToHash(randomString, hashedString) + if err != nil { + t.Fatal(err) + } + if match { + t.Log("passwords comparison failed") + t.Log("passwords shouldn't match") + t.Fail() + } +} diff --git a/encoder/encoder.go b/encoder/encoder.go new file mode 100644 index 0000000..2c4e1a5 --- /dev/null +++ b/encoder/encoder.go @@ -0,0 +1,65 @@ +package encoder + +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/encoder/encoder_test.go b/encoder/encoder_test.go new file mode 100644 index 0000000..0b19f55 --- /dev/null +++ b/encoder/encoder_test.go @@ -0,0 +1,33 @@ +package encoder + +import ( + "testing" +) + +var ( + randomString, hashedString string + opts []OptFunc +) + +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 + e, _ := NewEncoder(opts...) + randomString, err = e.RandomString(32) + if err != nil { + t.Fatal(err) + } + hashedString, err = e.HashString(randomString) + if err != nil { + t.Fatal(err) + } +} diff --git a/encoder/options.go b/encoder/options.go new file mode 100644 index 0000000..38a6d72 --- /dev/null +++ b/encoder/options.go @@ -0,0 +1,55 @@ +package encoder + +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 + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b43777c --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module euphoria-laxis.fr/go-packages/argon2 + +go 1.22 + +require golang.org/x/crypto v0.25.0 + +require golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c339b1b --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=