-
-
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, nil
Hi!
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