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 | |
} |
@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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi!
How about to use
argon2.Key()
instead ofargon2.IDKey()
?Seems like
argon2i
more suitable for server side password hashing andargon2id
is more relevant to "proof-of-work" checks.