Skip to content

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 →