diff --git a/argon2/argon2.go b/argon2/argon2.go index 29f0a2de45..854fd528a2 100644 --- a/argon2/argon2.go +++ b/argon2/argon2.go @@ -16,8 +16,8 @@ // Argon2i (implemented by Key) is the side-channel resistant version of Argon2. // It uses data-independent memory access, which is preferred for password // hashing and password-based key derivation. Argon2i requires more passes over -// memory than Argon2id to protect from trade-off attacks. The recommended -// parameters (taken from [2]) for non-interactive operations are time=3 and to +// memory than Argon2id to protect from trade-off attacks. The first recommended +// parameters (taken from [2]) for non-interactive operations are time=1 and to // use the maximum available memory. // // # Argon2id @@ -26,22 +26,31 @@ // Argon2i and Argon2d. It uses data-independent memory access for the first // half of the first iteration over the memory and data-dependent memory access // for the rest. Argon2id is side-channel resistant and provides better brute- -// force cost savings due to time-memory tradeoffs than Argon2i. The recommended +// force cost savings due to time-memory tradeoffs than Argon2i. The first recommended // parameters for non-interactive operations (taken from [2]) are time=1 and to // use the maximum available memory. // // [1] https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf -// [2] https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-9.3 +// [2] https://datatracker.ietf.org/doc/html/rfc9106#section-4 +// [3] https://datatracker.ietf.org/doc/html/rfc9106#section-3.1 package argon2 import ( + "bytes" + "crypto/rand" + "crypto/subtle" + "encoding/base64" "encoding/binary" + "errors" + "fmt" + "io" + "strconv" "sync" "golang.org/x/crypto/blake2b" ) -// The Argon2 version implemented by this package. +// Version is the Argon2 version implemented by this package. const Version = 0x13 const ( @@ -50,6 +59,48 @@ const ( argon2id ) +const ( + maxPasswordLength = (1 << 32) - 1 + maxSecretLength = (1 << 32) - 1 + minKeyLength = 4 + defaultThreadCount = 4 + defaultSaltLength = 16 + defaultKeyLength = 128 + defaultMemory = 65536 + defaultTime = 3 +) + +// ErrMismatchedHashAndPassword is returned from CompareHashAndPassword when a password and hash do +// not match. +var ErrMismatchedHashAndPassword = errors.New("crypto/argon2: hashedPassword is not the hash of the given password") + +// InvalidHashVersionError is returned from CompareHashAndPassword when a hash was created with +// an Argon2 algorithm other than the version specified by the RFC [3]. +var InvalidHashVersionError = errors.New(fmt.Sprintf("crypto/argon2: only argon2 algorithm version '%d' is supported", Version)) + +// InvalidHashPrefixError is returned from CompareHashAndPassword when a hash starts with something other than '$' +type InvalidHashPrefixError byte + +func (ih InvalidHashPrefixError) Error() string { + return fmt.Sprintf("crypto/argon2: argon2 hashes must start with '$', but hashedSecret started with '%c'", byte(ih)) +} + +// ErrPasswordTooLong is returned when the password passed to GenerateFromPassword +// is longer than allowed by the RFC [3] (i.e. > 2^32-1 bytes). +type ErrPasswordTooLong int + +func (ptl ErrPasswordTooLong) Error() string { + return fmt.Sprintf("crypto/argon2: Argon2 passwords cannot exceed 2^(32)-1 bytes, but password is length %d", ptl) +} + +// ErrSecretTooLong is returned when the secret passed to GenerateFromPassword +// is longer than allowed by the RFC [3] (i.e. > 2^32-1 bytes). +type ErrSecretTooLong int + +func (stl ErrSecretTooLong) Error() string { + return fmt.Sprintf("crypto/argon2: Argon2 secret values cannot exceed 2^(32)-1 bytes, but secret is length %d", stl) +} + // Key derives a key from the password, salt, and cost parameters using Argon2i // returning a byte slice of length keyLen that can be used as cryptographic // key. The CPU cost and parallelism degree must be greater than zero. @@ -59,7 +110,7 @@ const ( // // key := argon2.Key([]byte("some password"), salt, 3, 32*1024, 4, 32) // -// The draft RFC recommends[2] time=3, and memory=32*1024 is a sensible number. +// The RFC recommends[2] time=3, and memory=32*1024 is a sensible number. // If using that amount of memory (32 MB) is not possible in some contexts then // the time parameter can be increased to compensate. // @@ -81,9 +132,9 @@ func Key(password, salt []byte, time, memory uint32, threads uint8, keyLen uint3 // For example, you can get a derived key for e.g. AES-256 (which needs a // 32-byte key) by doing: // -// key := argon2.IDKey([]byte("some password"), salt, 1, 64*1024, 4, 32) +// key := argon2.IDKey([]byte("some password"), salt, 3, 64*1024, 4, 32) // -// The draft RFC recommends[2] time=1, and memory=64*1024 is a sensible number. +// The RFC suggests[2] that time=3 and memory=64*1024 are sensible numbers. // If using that amount of memory (64 MB) is not possible in some contexts then // the time parameter can be increased to compensate. // @@ -97,6 +148,124 @@ func IDKey(password, salt []byte, time, memory uint32, threads uint8, keyLen uin return deriveKey(argon2id, password, salt, nil, nil, time, memory, threads, keyLen) } +// IDKeyWithSecret derives a key from the password, salt, secret (also known as "pepper"), +// and cost parameters using Argon2id returning a byte slice of length keyLen that can be used as +// cryptographic key. The CPU cost and parallelism degree must be greater than +// zero. +// +// For example, you can get a derived key for e.g. AES-256 (which needs a +// 32-byte key) by doing: +// +// key := argon2.IDKey([]byte("some password"), []byte("secret pepper"), salt, 3, 64*1024, 4, 32) +// +// The RFC suggests[2] that time=3 and memory=64*1024 are sensible numbers. +// If using that amount of memory (64 MB) is not possible in some contexts then +// the time parameter can be increased to compensate. +// +// The time parameter specifies the number of passes over the memory and the +// memory parameter specifies the size of the memory in KiB. For example +// memory=64*1024 sets the memory cost to ~64 MB. The number of threads can be +// adjusted to the numbers of available CPUs. The cost parameters should be +// increased as memory latency and CPU parallelism increases. Remember to get a +// good random salt. +func IDKeyWithSecret(password, salt, secret []byte, time, memory uint32, threads uint8, keyLen uint32) []byte { + return deriveKey(argon2id, password, salt, secret, nil, time, memory, threads, keyLen) +} + +// GenerateFromPassword returns the hash of the password using Argon2id with the given optional secret +// and parameters for `keyLength` (length of the generated key), `time` (number of iterations over memory), +// `memory` (in kibibytes), and `threads`. If 0 is passed for keyLength, time, memory, or +// threads, they will be replaced by default values keyLength=128, time=3, memory=65536, threads=4. A random salt +// of length 16 bytes will be generated. +// +// Example usage: +// myHashedPassword := argon2.GenerateFromPassword([]byte("mypassword"), []byte("optionalsecretpepper"), 3, 8*256, 2, 0) +// +// The authors provide more detail parameter recommendations for different system scenarios in the RFC [2]. +// +// Use CompareHashAndPassword, as defined in this package, +// to compare the returned hashed password with its cleartext version. +// GenerateFromPassword does not accept passwords or secrets longer than 2^32-1 bytes, which +// are the longest passwords and secrets Argon2 will operate on. +func GenerateFromPassword(password, secret []byte, time, memory uint32, threads uint8, keyLength uint32) ([]byte, error) { + if len(password) > maxPasswordLength { + return nil, ErrPasswordTooLong(len(password)) + } + if len(secret) > maxSecretLength { + return nil, ErrSecretTooLong(len(secret)) + } + p, err := newFromPassword(password, nil, secret, time, memory, threads, keyLength) + if err != nil { + return nil, err + } + return p.encode(), nil +} + +// CompareHashAndPassword compares an Argon2 hashed password and an optional secret with a possible +// plaintext equivalent. Returns nil on success, or an error on failure. +func CompareHashAndPassword(hashedPassword, plaintext, secret []byte) error { + p, err := newFromHash(hashedPassword) + if err != nil { + return err + } + + otherP, err := newFromPassword(plaintext, p.salt, secret, p.time, p.memory, p.threads, uint32(len(p.hash))) + if subtle.ConstantTimeCompare(p.encode(), otherP.encode()) == 1 { + return nil + } + + return ErrMismatchedHashAndPassword +} + +func newFromPassword(password, salt, secret []byte, time, memory uint32, threads uint8, keyLength uint32) (*hashed, error) { + if threads == 0 { + threads = defaultThreadCount + } + + // Memory size m MUST be an integer number of kibibytes from 8*p to 2^(32)-1 + // as specified by the RFC [3]. + if memory == 0 { + memory = defaultMemory + } + minMemory := 8 * uint32(threads) + if memory < minMemory { + memory = minMemory + } + if time < 1 { + time = defaultTime + } + if keyLength < minKeyLength { + keyLength = defaultKeyLength + } + + p := new(hashed) + p.time = time + p.threads = threads + p.memory = memory + + if salt == nil { + salt = make([]byte, defaultSaltLength) + _, err := io.ReadFull(rand.Reader, salt) + if err != nil { + return nil, err + } + } + hash := IDKeyWithSecret(password, salt, secret, time, memory, threads, keyLength) + p.hash = hash + p.salt = salt + return p, nil +} + +func newFromHash(hashedSecret []byte) (*hashed, error) { + p := new(hashed) + _, err := p.decode(hashedSecret) + if err != nil { + return nil, err + } + + return p, nil +} + func deriveKey(mode int, password, salt, secret, data []byte, time, memory uint32, threads uint8, keyLen uint32) []byte { if time < 1 { panic("argon2: number of rounds too small") @@ -115,6 +284,72 @@ func deriveKey(mode int, password, salt, secret, data []byte, time, memory uint3 return extractKey(B, memory, uint32(threads), keyLen) } +type hashed struct { + hash []byte + salt []byte + time, memory uint32 + threads uint8 +} + +func (p *hashed) encode() []byte { + b := bytes.Buffer{} + + b.WriteString("$argon2id$v=19$m=") + b.WriteString(strconv.FormatUint(uint64(p.memory), 10)) + + b.WriteString(",t=") + b.WriteString(strconv.FormatUint(uint64(p.time), 10)) + + b.WriteString(",p=") + b.WriteString(strconv.FormatUint(uint64(p.threads), 10)) + + b.WriteString("$") + b.WriteString(base64.RawStdEncoding.EncodeToString(p.salt)) + + b.WriteString("$") + b.WriteString(base64.RawStdEncoding.EncodeToString(p.hash)) + + return b.Bytes() +} + +func (p *hashed) decode(sbytes []byte) (int, error) { + if sbytes[0] != '$' { + return -1, InvalidHashPrefixError(sbytes[0]) + } + + subSlices := bytes.Split(sbytes, []byte("$")) + if len(subSlices) != 6 { + return -1, errors.New(fmt.Sprintf("crypto/argon2: %s is an invalid argument", sbytes)) + } + + if !bytes.Equal(subSlices[2], []byte("v=19")) { + return -1, InvalidHashVersionError + } + + decodedSalt, err := base64.RawStdEncoding.DecodeString(string(subSlices[4])) + if err != nil { + return -1, err + } + p.salt = decodedSalt + + decodedHash, err := base64.RawStdEncoding.DecodeString(string(subSlices[5])) + if err != nil { + return -1, err + } + p.hash = decodedHash + + _, err = fmt.Sscanf(string(subSlices[3]), "m=%d,t=%d,p=%d", &p.memory, &p.time, &p.threads) + if err != nil { + return -1, err + } + + return 0, nil +} + +func (p *hashed) String() string { + return fmt.Sprintf("&{hash: %#v, salt: %#v, memory: %d, time: %c, threads: %c}", string(p.hash), p.salt, p.memory, p.time, p.threads) +} + const ( blockLength = 128 syncPoints = 4 diff --git a/argon2/argon2_test.go b/argon2/argon2_test.go index 775b97a404..a4c1750702 100644 --- a/argon2/argon2_test.go +++ b/argon2/argon2_test.go @@ -6,7 +6,9 @@ package argon2 import ( "bytes" + "encoding/base64" "encoding/hex" + "reflect" "testing" ) @@ -77,6 +79,69 @@ func testArgon2id(t *testing.T) { } } +func TestArgon2IsEasy(t *testing.T) { + pass := []byte("mypassword") + hp, err := GenerateFromPassword(pass, nil, 0, 0, 0, 0) + if err != nil { + t.Fatalf("GenerateFromPassword error: %s", err) + } + + if CompareHashAndPassword(hp, pass, nil) != nil { + t.Errorf("%s should hash %s correctly", hp, pass) + } + + notPass := "notthepass" + err = CompareHashAndPassword(hp, []byte(notPass), nil) + if err != ErrMismatchedHashAndPassword { + t.Errorf("%v and %s should be mismatched", hp, notPass) + } +} + +func TestArgon2WithSecretIsEasy(t *testing.T) { + pass := []byte("mypassword") + sec := []byte("shhhh!") + hp, err := GenerateFromPassword(pass, sec, 0, 1, 0, 0) + if err != nil { + t.Fatalf("GenerateFromPassword error: %s", err) + } + + if CompareHashAndPassword(hp, pass, sec) != nil { + t.Errorf("%v should hash %s correctly", hp, pass) + } + + notPass := "notthepass" + err = CompareHashAndPassword(hp, []byte(notPass), sec) + if err != ErrMismatchedHashAndPassword { + t.Errorf("%v and %s should be mismatched", hp, notPass) + } +} + +func TestArgon2IsCorrect(t *testing.T) { + pass := []byte("foobar") + salt := []byte("abcdefghijklmnop") + expectedHash := []byte("$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o") + + h, err := newFromPassword(pass, salt, nil, 2, 64*1024, 1, 32) + if err != nil { + t.Errorf("Unable to create hash %s: %v", string(expectedHash), err) + } + + eh, err := newFromHash(expectedHash) + if err != nil { + t.Errorf("Unable to parse %s: %v", string(expectedHash), err) + } + + // This is not the safe way to compare these hashes. We do this only for + // testing clarity. Use argon2.CompareHashAndPassword() + if err == nil && !bytes.Equal(expectedHash, eh.encode()) { + t.Errorf("Parsed hash %v should equal %v", eh.encode(), expectedHash) + } + if err == nil && !bytes.Equal(h.encode(), eh.encode()) { + t.Errorf("Incorrect hash: got: %s want %s", h.encode(), eh.encode()) + } + +} + func TestVectors(t *testing.T) { password, salt := []byte("password"), []byte("somesalt") for i, v := range testVectors { @@ -91,6 +156,107 @@ func TestVectors(t *testing.T) { } } +type CompareHashAndPasswordTest struct { + hashedPassword []byte + password []byte + secret []byte + expectedErr error +} + +var compareHashAndPasswordTests = []CompareHashAndPasswordTest{ + {[]byte("$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o"), []byte("foobar"), nil, nil}, + {[]byte("$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o"), []byte("notfoobar"), nil, ErrMismatchedHashAndPassword}, + {[]byte("$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o"), nil, nil, ErrMismatchedHashAndPassword}, + {[]byte("$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o"), []byte("foobar"), []byte("verysecurepepper"), ErrMismatchedHashAndPassword}, + {[]byte("$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$62SNes97JZvKGqH1gm+0EWabDcX9i3FJFpZJ0PGRaTI"), []byte("foobar"), []byte("verysecurepepper"), nil}, + {[]byte("$argon2id$v=19$m=256,t=1,p=1$c2FsdHlzYWx0$6UMlxN3kDbxCSZVA+XS/pIUl5eS7hFoUIKDAndacf58"), []byte("helloworld"), nil, nil}, +} + +func TestCompareHashAndPassword(t *testing.T) { + for _, chpt := range compareHashAndPasswordTests { + err := CompareHashAndPassword(chpt.hashedPassword, chpt.password, chpt.secret) + if err != nil && chpt.expectedErr == nil { + t.Errorf("%s should hash %s correctly", chpt.hashedPassword, chpt.password) + } + if err == nil && chpt.expectedErr == ErrMismatchedHashAndPassword { + t.Errorf("%s and %s should be mismatched", chpt.hashedPassword, chpt.password) + } + } +} + +func errCheck(t *testing.T, name string, expected, err error) { + if err == nil { + t.Errorf("%s: Should have returned an error", name) + } + if err != nil && err != expected { + t.Errorf("%s gave err %v but should have given %v", name, err, expected) + } +} + +type InvalidPasswordTest struct { + err error + password []byte + secret []byte +} + +var invalidPasswordTests = []InvalidPasswordTest{ + {ErrPasswordTooLong(maxPasswordLength + 1), make([]byte, maxPasswordLength+1), nil}, + {ErrSecretTooLong(maxSecretLength + 1), nil, make([]byte, maxSecretLength+1)}, +} + +func TestInvalidPasswordErrors(t *testing.T) { + for _, ipt := range invalidPasswordTests { + _, err := GenerateFromPassword(ipt.password, ipt.secret, 4, 8*256, 4, 0) + errCheck(t, "GenerateFromPassword", ipt.err, err) + } +} + +type InvalidHashTest struct { + err error + hash []byte +} + +var invalidTests = []InvalidHashTest{ + {InvalidHashVersionError, []byte("$argon2id$v=20$m=65536,t=3,p=2$AgICAgICAgICAgICAgICAg$DWQN9Y14dmwIwDejSotTydAe8EUtdbZetSUg6WsB5lk")}, + {InvalidHashPrefixError('%'), []byte("%argon2id$v=19$m=65536,t=3,p=2$AgICAgICAgICAgICAgICAg$DWQN9Y14dmwIwDejSotTydAe8EUtdbZetSUg6WsB5lk")}, +} + +func TestInvalidHashErrors(t *testing.T) { + for _, iht := range invalidTests { + p := new(hashed) + _, err := p.decode(iht.hash) + errCheck(t, "decode", iht.err, err) + + _, err = newFromHash(iht.hash) + errCheck(t, "newFromHash", iht.err, err) + + err = CompareHashAndPassword(iht.hash, []byte("anything"), nil) + errCheck(t, "CompareHashAndPassword", iht.err, err) + + err = CompareHashAndPassword(iht.hash, []byte("anything"), []byte("shhhh")) + errCheck(t, "CompareHashAndPassword", iht.err, err) + } +} + +func TestEncodedParams(t *testing.T) { + hash := []byte("$argon2id$v=19$m=65536,t=2,p=1$YWJjZGVmZ2hpamtsbW5vcA$BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o") + decodedHash, _ := base64.RawStdEncoding.DecodeString("BztdyfEefG5V18ZNlztPrfZaU5duVFKZiI6dJeWht0o") + wantP := &hashed{ + hash: decodedHash, + salt: []byte("abcdefghijklmnop"), + memory: 65536, + time: 2, + threads: 1, + } + gotP, err := newFromHash(hash) + if err != nil { + t.Errorf("Unable to parse %s: %v", string(hash), err) + } + if !reflect.DeepEqual(gotP, wantP) { + t.Errorf("Error decoding hash. got: %v want: %v", gotP, wantP) + } +} + func benchmarkArgon2(mode int, time, memory uint32, threads uint8, keyLen uint32, b *testing.B) { password := []byte("password") salt := []byte("choosing random salts is hard")