Last active
May 31, 2024 02:15
-
-
Save alexedwards/34277fae0f48abe36822b375f0f6a621 to your computer and use it in GitHub Desktop.
Password hashing and verification with Argon2id
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
great blog post, thank you