Files
restic-manager/internal/auth/passwords.go
T
steve 03e5ec31f1
CI / Test (store) (pull_request) Successful in 38s
CI / Test (rest) (pull_request) Successful in 48s
CI / Test (server-http) (pull_request) Successful in 1m10s
CI / Lint (pull_request) Successful in 33s
CI / Build (linux/amd64) (pull_request) Successful in 24s
CI / Build (windows/amd64) (pull_request) Successful in 48s
CI / Build (linux/arm64) (pull_request) Successful in 23s
ci: shard test job + cheap argon2 in test mode
Test job was wall-clocked by `internal/server/http` (~156s on the
self-hosted runner under -race). Two changes here cut that:

1. Matrix-shard the test job by package group: server-http, store,
   and "rest" (everything else, computed via `go list | grep -v`).
   Each shard runs on its own runner so the heavy package isn't
   CPU-starved by siblings.

2. `auth.HashPassword` drops to cheap argon2id params (8 KiB / 1
   iter / 1 lane) when `testing.Testing()` returns true. Production
   params are unchanged. VerifyPassword reads params from the
   encoded hash so cheap-params hashes verify identically — no test
   call sites need to change.
2026-05-05 08:40:50 +01:00

108 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package auth handles password hashing (argon2id), session
// management, CSRF tokens, and bearer-token verification for agents.
package auth
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"testing"
"golang.org/x/crypto/argon2"
)
// argon2id parameters following RFC 9106 §4 "second
// recommended option" (memory-constrained):
// - 64 MiB memory, 3 iterations, 4 lanes, 32-byte tag.
//
// These are tunable per-deployment if a beefy controller wants to
// crank them; we ship a defensible default.
const (
defaultMemoryKiB = 64 * 1024
defaultIterations = 3
defaultParallel = 4
defaultSaltLen = 16
defaultKeyLen = 32
)
// Cheap params used only when the binary is a `go test` binary
// (testing.Testing() == true). Argon2id at production params costs
// 300500 ms per hash and dominates wall time on CI runners under
// `-race`. Tests don't need real KDF strength — VerifyPassword reads
// params from the encoded hash, so verifying a cheap-params hash
// works the same way.
const (
testMemoryKiB = 8
testIterations = 1
testParallel = 1
)
// HashPassword returns an argon2id-encoded string of the form
//
// $argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>
//
// safe to store in a TEXT column. The salt is freshly random per call.
func HashPassword(password string) (string, error) {
mem, iter, par := uint32(defaultMemoryKiB), uint32(defaultIterations), uint8(defaultParallel)
if testing.Testing() {
mem, iter, par = testMemoryKiB, testIterations, testParallel
}
salt := make([]byte, defaultSaltLen)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("auth: read salt: %w", err)
}
hash := argon2.IDKey([]byte(password), salt,
iter, mem, par, defaultKeyLen)
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version,
mem, iter, par,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
// VerifyPassword returns nil if password matches the encoded hash.
// On any decode error or mismatch the error is non-nil — callers
// should treat all non-nil returns as "invalid credentials" and not
// leak which case it was.
func VerifyPassword(encoded, password string) error {
parts := strings.Split(encoded, "$")
// "$argon2id$v=...$m=...,t=...,p=...$<salt>$<hash>" → 6 parts (leading empty)
if len(parts) != 6 || parts[1] != "argon2id" {
return errors.New("auth: unrecognised hash format")
}
var version int
if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil {
return fmt.Errorf("auth: parse version: %w", err)
}
if version != argon2.Version {
return fmt.Errorf("auth: unsupported argon2 version %d", version)
}
var memory, iterations uint32
var parallel uint8
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d",
&memory, &iterations, &parallel); err != nil {
return fmt.Errorf("auth: parse params: %w", err)
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return fmt.Errorf("auth: decode salt: %w", err)
}
want, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return fmt.Errorf("auth: decode hash: %w", err)
}
got := argon2.IDKey([]byte(password), salt,
iterations, memory, parallel, uint32(len(want)))
if subtle.ConstantTimeCompare(got, want) != 1 {
return errors.New("auth: invalid password")
}
return nil
}