Argon2 Hash & Verify — Go source
Hash passwords with Argon2id — the winner of the Password Hashing Competition. Configure memory, iterations, and parallelism. WASM-powered, client-side.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package argon2 ports src/lib/argon2.ts (dual source: the web lib hashes via
// the argon2-browser WASM build, the CLI lib via golang.org/x/crypto/argon2 —
// kept in lock-step by the shared test vectors in argon2_test.go). Pure logic,
// never panics — invalid input returns an error.
//
// The TS lib marshals the reference C library; this port mirrors its observable
// contract instead: the same PHC string format
//
// $argon2id$v=19$m=65536,t=3,p=1$<b64 salt>$<b64 digest>
//
// (salt and digest as unpadded standard Base64), the same OWASP-recommended
// default profile, and the same validation errors. Hashing always emits
// argon2id v19; verification accepts argon2i and argon2id (the C library also
// takes argon2d, which x/crypto does not implement).
package argon2
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
xargon2 "golang.org/x/crypto/argon2"
)
// Defaults following the OWASP-recommended Argon2id profile, mirroring
// ARGON2_DEFAULTS in src/lib/argon2.ts (64 MiB, 3 passes).
const (
DefaultMemory = 65_536 // KiB (64 MiB)
DefaultIterations = 3
DefaultParallelism = 1
DefaultHashLength = 32
)
// SaltBytes is the random salt size in bytes (128 bits — the PHC recommendation).
const SaltBytes = 16
// phcVersion is the Argon2 version 1.3, as encoded in PHC strings (v=19).
const phcVersion = 19
// minSaltLen is the reference implementation's minimum salt length (8 bytes);
// shorter explicit salts are rejected exactly as the C library rejects them.
const minSaltLen = 8
// maxParallelism bounds lanes to what x/crypto's uint8 thread count can carry.
const maxParallelism = 255
// phcRe matches `$argon2id$v=19$m=65536,t=3,p=1$salt$hash` (digest optional).
// Same pattern as parseArgon2 in src/lib/argon2.ts.
var phcRe = regexp.MustCompile(`^\$(argon2(?:d|i|id))\$v=(\d+)\$m=(\d+),t=(\d+),p=(\d+)\$([A-Za-z0-9+/]+)(?:\$([A-Za-z0-9+/]+))?$`)
// Options configures Hash. The zero value of every numeric field means "use
// the default" (mirroring the TS lib's optional parameters); an explicit
// negative or out-of-range value is rejected. Salt is nil → a fresh random
// SaltBytes-byte salt is generated per call.
type Options struct {
// Memory is the memory cost in KiB (default 65536 = 64 MiB). Must be >= 1024.
Memory int
// Iterations is the time cost — passes over memory (default 3). Must be >= 1.
Iterations int
// Parallelism is the lane count (default 1). Must be >= 1.
Parallelism int
// HashLength is the digest length in bytes (default 32). Must be 16..64.
HashLength int
// Salt, when non-nil, is used verbatim (must be >= 8 bytes).
Salt []byte
}
// Result is the outcome of Hash, mirroring Argon2Result.
type Result struct {
// Hash is the raw digest, lowercase hex (HashLength bytes).
Hash string
// Encoded is the self-contained PHC string — store this, verify against it.
Encoded string
// Salt is the salt used, lowercase hex (16 bytes unless Salt was given).
Salt string
}
// Params is what ParseArgon2 extracts from a PHC string, mirroring Argon2Params.
type Params struct {
Type string // "argon2d" | "argon2i" | "argon2id"
Version int
Memory int
Iterations int
Parallelism int
// Salt, decoded from the embedded Base64 into lowercase hex.
Salt string
// Hash, decoded from the embedded Base64 into lowercase hex ("" if absent).
Hash string
}
// normalizeOptions validates and normalizes hashing parameters, mirroring
// normalizeOptions in the TS lib. The zero value of each field selects the
// default (TS distinguishes undefined from 0; Go cannot, so 0 = default and
// only negative or out-of-range values are invalid).
func normalizeOptions(opts Options) (memory, iterations, parallelism, hashLength int, err error) {
memory, iterations, parallelism, hashLength = opts.Memory, opts.Iterations, opts.Parallelism, opts.HashLength
if memory == 0 {
memory = DefaultMemory
}
if iterations == 0 {
iterations = DefaultIterations
}
if parallelism == 0 {
parallelism = DefaultParallelism
}
if hashLength == 0 {
hashLength = DefaultHashLength
}
if memory < 1024 {
return 0, 0, 0, 0, errors.New("Memory must be at least 1024 KiB")
}
if iterations < 1 {
return 0, 0, 0, 0, errors.New("Iterations must be at least 1")
}
if parallelism < 1 || parallelism > maxParallelism {
return 0, 0, 0, 0, errors.New("Parallelism must be between 1 and 255")
}
if hashLength < 16 || hashLength > 64 {
return 0, 0, 0, 0, errors.New("Hash length must be between 16 and 64 bytes")
}
return memory, iterations, parallelism, hashLength, nil
}
// ParseArgon2 parses a PHC-format Argon2 string
// (`$argon2id$v=19$m=65536,t=3,p=1$salt$hash`) into its typed parameters.
// It accepts argon2d / argon2i / argon2id. The digest segment is optional
// (some encoders omit it); salt and hash are returned as lowercase hex.
// It returns an error on any malformed input — the Go twin of parseArgon2.
func ParseArgon2(encoded string) (Params, error) {
m := phcRe.FindStringSubmatch(strings.TrimSpace(encoded))
if m == nil {
return Params{}, errors.New("Invalid Argon2 string: expected $argon2id$v=19$m=...,t=...,p=...$salt$hash")
}
salt, err := base64.RawStdEncoding.DecodeString(m[6])
if err != nil {
return Params{}, errors.New("Invalid Argon2 string: non-Base64 characters")
}
p := Params{Type: m[1], Salt: hex.EncodeToString(salt)}
if m[7] != "" {
digest, err := base64.RawStdEncoding.DecodeString(m[7])
if err != nil {
return Params{}, errors.New("Invalid Argon2 string: non-Base64 characters")
}
p.Hash = hex.EncodeToString(digest)
}
for i, dst := range []*int{&p.Version, &p.Memory, &p.Iterations, &p.Parallelism} {
n, err := strconv.Atoi(m[i+2]) // digits-only per the regex; belt and braces
if err != nil {
return Params{}, fmt.Errorf("Invalid Argon2 string: bad numeric field %q", m[i+2])
}
*dst = n
}
return p, nil
}
// Hash hashes a password with Argon2id (hybrid of Argon2i's side-channel
// resistance and Argon2d's GPU resistance — the recommended mode for password
// storage). It returns the digest (hex), the salt used (hex), and the
// self-contained PHC string. A fresh random 16-byte salt is generated per
// call unless opts.Salt is given. It is the Go twin of argon2Hash and must
// agree with it on every shared test vector.
func Hash(password string, opts Options) (Result, error) {
memory, iterations, parallelism, hashLength, err := normalizeOptions(opts)
if err != nil {
return Result{}, err
}
salt := opts.Salt
if salt == nil {
salt = make([]byte, SaltBytes)
if _, err := rand.Read(salt); err != nil {
return Result{}, fmt.Errorf("failed to generate salt: %w", err)
}
} else if len(salt) < minSaltLen {
return Result{}, errors.New("Salt is too short")
}
digest := xargon2.IDKey([]byte(password), salt,
uint32(iterations), uint32(memory), uint8(parallelism), uint32(hashLength))
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
phcVersion, memory, iterations, parallelism,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(digest))
return Result{
Hash: hex.EncodeToString(digest),
Encoded: encoded,
Salt: hex.EncodeToString(salt),
}, nil
}
// Verify verifies a password against a PHC-format encoded hash (as produced
// by Hash). It returns true on match, false on mismatch, and an error only on
// a malformed encoded string or unsupported parameters — the Go twin of
// argon2Verify. The type is read from the string itself; argon2i and argon2id
// are supported (argon2d is not — x/crypto implements neither lane mode's
// data-independent/data-dependent mix that verification would need).
func Verify(encoded, password string) (bool, error) {
params, err := ParseArgon2(encoded) // validate format up front
if err != nil {
return false, err
}
if params.Hash == "" {
return false, errors.New("Invalid Argon2 string: no digest to verify against")
}
if params.Version != phcVersion {
return false, fmt.Errorf("unsupported Argon2 version %d (only v=19)", params.Version)
}
// Guard the x/crypto call domain the way the C library guards its own —
// out-of-range costs would otherwise risk a panic instead of an error.
if params.Iterations < 1 {
return false, errors.New("Time is too small")
}
if params.Parallelism < 1 || params.Parallelism > maxParallelism {
return false, errors.New("Threads too few")
}
if params.Memory < minSaltLen*params.Parallelism {
return false, errors.New("Memory is too small")
}
if params.Memory > math.MaxUint32 {
return false, errors.New("Memory cost is too large")
}
salt, err := hex.DecodeString(params.Salt)
if err != nil {
return false, errors.New("Invalid Argon2 string: non-Base64 characters")
}
if len(salt) < minSaltLen {
return false, errors.New("Salt is too short")
}
digest, err := hex.DecodeString(params.Hash)
if err != nil {
return false, errors.New("Invalid Argon2 string: non-Base64 characters")
}
var want []byte
switch params.Type {
case "argon2id":
want = xargon2.IDKey([]byte(password), salt,
uint32(params.Iterations), uint32(params.Memory),
uint8(params.Parallelism), uint32(len(digest)))
case "argon2i":
want = xargon2.Key([]byte(password), salt,
uint32(params.Iterations), uint32(params.Memory),
uint8(params.Parallelism), uint32(len(digest)))
default: // "argon2d" — accepted by the regex (and the C library), not by x/crypto
return false, errors.New("argon2d verification is not supported (argon2i and argon2id only)")
}
return subtle.ConstantTimeCompare(want, digest) == 1, nil
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →