package password import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "strings" "golang.org/x/crypto/argon2" ) const ( time = 1 memory = 64 * 1024 threads = 4 keyLen = 32 ) // Hash creates an argon2id hash of the password func Hash(password string) string { salt := make([]byte, 16) rand.Read(salt) hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, memory, time, threads, b64Salt, b64Hash) return encoded } // Verify checks if a password matches the hash func Verify(password, encodedHash string) (bool, error) { parts := strings.Split(encodedHash, "$") if len(parts) != 6 { return false, fmt.Errorf("invalid hash format") } if parts[1] != "argon2id" { return false, fmt.Errorf("invalid hash variant") } var version int _, err := fmt.Sscanf(parts[2], "v=%d", &version) if err != nil { return false, err } if version != argon2.Version { return false, fmt.Errorf("incompatible argon2 version") } var m, t, p uint32 _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p) if err != nil { return false, err } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, err } expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, err } hash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(expectedHash))) // Use constant-time comparison to prevent timing attacks if subtle.ConstantTimeCompare(hash, expectedHash) == 1 { return true, nil } return false, nil }