-
-
Save alexedwards/34277fae0f48abe36822b375f0f6a621 to your computer and use it in GitHub Desktop.
| package main | |
| import ( | |
| "crypto/rand" | |
| "crypto/subtle" | |
| "encoding/base64" | |
| "errors" | |
| "fmt" | |
| "log" | |
| "strings" | |
| "golang.org/x/crypto/argon2" | |
| ) | |
| var ( | |
| ErrInvalidHash = errors.New("the encoded hash is not in the correct format") | |
| ErrIncompatibleVersion = errors.New("incompatible version of argon2") | |
| ) | |
| type params struct { | |
| memory uint32 | |
| iterations uint32 | |
| parallelism uint8 | |
| saltLength uint32 | |
| keyLength uint32 | |
| } | |
| func main() { | |
| p := ¶ms{ | |
| memory: 64 * 1024, | |
| iterations: 3, | |
| parallelism: 2, | |
| saltLength: 16, | |
| keyLength: 32, | |
| } | |
| encodedHash, err := generateFromPassword("password123", p) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| match, err := comparePasswordAndHash("pa$$word", encodedHash) | |
| if err != nil { | |
| log.Fatal(err) | |
| } | |
| fmt.Printf("Match: %v\n", match) | |
| } | |
| func generateFromPassword(password string, p *params) (encodedHash string, err error) { | |
| // Generate a cryptographically secure random salt. | |
| salt, err := generateRandomBytes(p.saltLength) | |
| if err != nil { | |
| return "", err | |
| } | |
| // Pass the plaintext password, salt and parameters to the argon2.IDKey | |
| // function. This will generate a hash of the password using the Argon2id | |
| // variant. | |
| hash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength) | |
| // Base64 encode the salt and hashed password. | |
| b64Salt := base64.RawStdEncoding.EncodeToString(salt) | |
| b64Hash := base64.RawStdEncoding.EncodeToString(hash) | |
| // Return a string using the standard encoded hash representation. | |
| 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 generateRandomBytes(n uint32) ([]byte, error) { | |
| b := make([]byte, n) | |
| _, err := rand.Read(b) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return b, nil | |
| } | |
| func comparePasswordAndHash(password, encodedHash string) (match bool, err error) { | |
| // Extract the parameters, salt and derived key from the encoded password | |
| // hash. | |
| p, salt, hash, err := decodeHash(encodedHash) | |
| if err != nil { | |
| return false, err | |
| } | |
| // Derive the key from the other password using the same parameters. | |
| otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength) | |
| // Check that the contents of the hashed passwords are identical. Note | |
| // that we are using the subtle.ConstantTimeCompare() function for this | |
| // to help prevent timing attacks. | |
| if subtle.ConstantTimeCompare(hash, otherHash) == 1 { | |
| return true, nil | |
| } | |
| return false, 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.Strict().DecodeString(vals[4]) | |
| if err != nil { | |
| return nil, nil, nil, err | |
| } | |
| p.saltLength = uint32(len(salt)) | |
| hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5]) | |
| if err != nil { | |
| return nil, nil, nil, err | |
| } | |
| p.keyLength = uint32(len(hash)) | |
| return p, salt, hash, nil | |
| } |
Just a minor refactor but you could replace lines 96-99 with:
return subtle.ConstantTimeCompare(hash, otherHash) == 1, nilHi!
How about to use argon2.Key() instead of argon2.IDKey()?
Seems like argon2i more suitable for server side password hashing and argon2id is more relevant to "proof-of-work" checks.
@AleksandrMakarenkov my understanding is that 2i should be preferred to 2d for password hashing --- not necessarily preferred to 2id. Using 2id is arguably better than just 2i in most cases as it provides some resistance to TMTO attacks.
@alexedwards Hey!
You were right :D
After investigating some posts i realized that argon2id is recommended as "primary algo" for most cases.
Here the link, https://crypto.stackexchange.com/questions/48935/why-use-argon2i-or-argon2d-if-argon2id-exists,
i hope it helps someone else!
great blog post, thank you
Hi,
Great code and blog post! I've implemented a similar thing in my Go version of crypt. Feel very welcome to do a code review or comment it in another constructive way.
Best regards