diff --git a/go.mod b/go.mod
index 8f83270d10e8e72a22c652d5cf334ac52a97d21d..0717e9c076b9c0937874aa8e903aca16731ed4b0 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.21.0
 toolchain go1.22.1
 
 require (
-	git.autistici.org/ai3/go-common v0.0.0-20241017171051-880a2c5ae7f4
+	git.autistici.org/ai3/go-common v0.0.0-20250125130542-62b40adde91d
 	git.autistici.org/id/usermetadb v0.0.0-20241017171915-b5c24a0ff9b7
 	github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874
 	github.com/cenkalti/backoff/v4 v4.3.0
diff --git a/go.sum b/go.sum
index 55e715abac33f0613fa8e607bce07bb5c1643e61..58c8561f6ba59137d4522981e885b45371b4f545 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 git.autistici.org/ai3/go-common v0.0.0-20241017171051-880a2c5ae7f4 h1:xB5K4GL4VlguEOknhgz+AN3k8nmx19y91RRUvByaLnQ=
 git.autistici.org/ai3/go-common v0.0.0-20241017171051-880a2c5ae7f4/go.mod h1:JpPfOTGgbvAElF0wdmv81y6l3CRsdS4z3IkNOETUDVo=
+git.autistici.org/ai3/go-common v0.0.0-20250125130542-62b40adde91d h1:20u44cuDBH0xxPojMsDt0j9x+8FGZhifOuNe9zwoqos=
+git.autistici.org/ai3/go-common v0.0.0-20250125130542-62b40adde91d/go.mod h1:JpPfOTGgbvAElF0wdmv81y6l3CRsdS4z3IkNOETUDVo=
 git.autistici.org/id/usermetadb v0.0.0-20240906122220-42ab91c67c5c h1:se5XxGQN0CBmbfcBCa+oP9SXXMXQy7ovI/BApx0Mz/M=
 git.autistici.org/id/usermetadb v0.0.0-20240906122220-42ab91c67c5c/go.mod h1:gu8lZEoo9XWy6wzPRaXP5Gy/tIRKqYW6zbKdyqT9I8M=
 git.autistici.org/id/usermetadb v0.0.0-20241017171915-b5c24a0ff9b7 h1:9rOxoXT+dARc56YrRV1g4CQsM1K1f+sT8ofoZ5YoO2M=
diff --git a/vendor/git.autistici.org/ai3/go-common/pwhash/argon2.go b/vendor/git.autistici.org/ai3/go-common/pwhash/argon2.go
index fef03caa0c001faace0d16123b23e53e5f0626ec..5c53eec3226176203ef843fec265efa5191c7bfa 100644
--- a/vendor/git.autistici.org/ai3/go-common/pwhash/argon2.go
+++ b/vendor/git.autistici.org/ai3/go-common/pwhash/argon2.go
@@ -6,15 +6,15 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
-	"log"
 	"strconv"
 	"strings"
 
 	"golang.org/x/crypto/argon2"
 )
 
-var (
-	argonKeyLen  uint32 = 32
+const (
+	argonLegacyKeySize  = 32
+	argonDefaultKeySize = 16
 	argonSaltLen        = 16
 )
 
@@ -29,9 +29,11 @@ type argon2PasswordHash struct {
 
 // newArgon2PasswordHash returns an Argon2i-based PasswordHash using the
 // specified parameters for time, memory, and number of threads.
-func newArgon2PasswordHash(time, mem uint32, threads uint8, codec argon2Codec) PasswordHash {
+func newArgon2PasswordHash(kind string, keySize int, time, mem uint32, threads uint8, codec argon2Codec) PasswordHash {
 	return &argon2PasswordHash{
 		params: argon2Params{
+			KeySize: keySize,
+			Kind:    kind,
 			Time:    time,
 			Memory:  mem,
 			Threads: threads,
@@ -41,8 +43,8 @@ func newArgon2PasswordHash(time, mem uint32, threads uint8, codec argon2Codec) P
 }
 
 // NewArgon2 returns an Argon2i-based PasswordHash using the default parameters.
-func NewArgon2() PasswordHash {
-	return NewArgon2WithParams(
+func NewArgon2Legacy() PasswordHash {
+	return NewArgon2LegacyWithParams(
 		defaultArgon2Params.Time,
 		defaultArgon2Params.Memory,
 		defaultArgon2Params.Threads,
@@ -51,8 +53,8 @@ func NewArgon2() PasswordHash {
 
 // NewArgon2WithParams returns an Argon2i-based PasswordHash using the
 // specified parameters for time, memory, and number of threads.
-func NewArgon2WithParams(time, mem uint32, threads uint8) PasswordHash {
-	return newArgon2PasswordHash(time, mem, threads, &a2Codec{})
+func NewArgon2LegacyWithParams(time, mem uint32, threads uint8) PasswordHash {
+	return newArgon2PasswordHash(kindArgon2I, argonLegacyKeySize, time, mem, threads, &a2LegacyCodec{})
 }
 
 // NewArgon2Std returns an Argon2i-based PasswordHash that conforms
@@ -65,12 +67,12 @@ func NewArgon2Std() PasswordHash {
 	)
 }
 
-// NewArgon2StdWithParams returns an Argon2i-based PasswordHash using
+// NewArgon2StdWithParams returns an Argon2id-based PasswordHash using
 // the specified parameters for time, memory, and number of
-// threads. This will use the string encoding ("$argon2$") documented
+// threads. This will use the string encoding ("$argon2id$") documented
 // in the argon2 reference implementation.
 func NewArgon2StdWithParams(time, mem uint32, threads uint8) PasswordHash {
-	return newArgon2PasswordHash(time, mem, threads, &argon2StdCodec{})
+	return newArgon2PasswordHash(kindArgon2ID, argonDefaultKeySize, time, mem, threads, &argon2StdCodec{})
 }
 
 // ComparePassword returns true if the given password matches the
@@ -80,28 +82,53 @@ func (s *argon2PasswordHash) ComparePassword(encrypted, password string) bool {
 	if err != nil {
 		return false
 	}
-	dk2 := argon2.Key([]byte(password), salt, params.Time, params.Memory, params.Threads, argonKeyLen)
+
+	dk2 := params.hash(password, salt)
 	return subtle.ConstantTimeCompare(dk, dk2) == 1
 }
 
 // Encrypt the given password with the Argon2 algorithm.
 func (s *argon2PasswordHash) Encrypt(password string) string {
 	salt := getRandomBytes(argonSaltLen)
-	dk := argon2.Key([]byte(password), salt, s.params.Time, s.params.Memory, s.params.Threads, argonKeyLen)
+	dk := s.params.hash(password, salt)
 	return s.codec.encodeArgon2Hash(s.params, salt, dk)
 }
 
+const (
+	kindArgon2I  = "argon2i"
+	kindArgon2ID = "argon2id"
+)
+
 type argon2Params struct {
+	Kind    string
+	KeySize int
 	Time    uint32
 	Memory  uint32
 	Threads uint8
 }
 
+func (p argon2Params) hash(password string, salt []byte) []byte {
+	if p.KeySize == 0 {
+		panic("key size is 0")
+	}
+
+	switch p.Kind {
+	case kindArgon2I:
+		return argon2.Key([]byte(password), salt, p.Time, p.Memory, p.Threads, uint32(p.KeySize))
+	case kindArgon2ID:
+		return argon2.IDKey([]byte(password), salt, p.Time, p.Memory, p.Threads, uint32(p.KeySize))
+	default:
+		panic("unknown argon2 hash kind")
+	}
+}
+
 // Default Argon2 parameters are tuned for a high-traffic
 // authentication service (<1ms per operation).
 var defaultArgon2Params = argon2Params{
+	Kind:    kindArgon2ID,
+	KeySize: 16,
 	Time:    1,
-	Memory:  4 * 1024,
+	Memory:  64 * 1024,
 	Threads: 4,
 }
 
@@ -110,13 +137,14 @@ type argon2Codec interface {
 	decodeArgon2Hash(string) (argon2Params, []byte, []byte, error)
 }
 
-type a2Codec struct{}
+// Argon2i legacy encoding, do not use.
+type a2LegacyCodec struct{}
 
-func (*a2Codec) encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
+func (*a2LegacyCodec) encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
 	return fmt.Sprintf("$a2$%d$%d$%d$%x$%x", params.Time, params.Memory, params.Threads, salt, dk)
 }
 
-func (*a2Codec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk []byte, err error) {
+func (*a2LegacyCodec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk []byte, err error) {
 	if !strings.HasPrefix(s, "$a2$") {
 		err = errors.New("not an Argon2 password hash")
 		return
@@ -128,6 +156,8 @@ func (*a2Codec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk
 		return
 	}
 
+	params.Kind = kindArgon2I
+
 	var i uint64
 
 	if i, err = strconv.ParseUint(parts[0], 10, 32); err != nil {
@@ -149,16 +179,36 @@ func (*a2Codec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk
 	if err != nil {
 		return
 	}
+
 	dk, err = hex.DecodeString(parts[4])
+	if err != nil {
+		return
+	}
+
+	params.KeySize = len(dk)
+	switch len(dk) {
+	case 16, 24, 32:
+	default:
+		err = errors.New("bad key size")
+	}
+
 	return
 }
 
+// Standard Argon2 encoding as per the reference implementation in
+// https://github.com/P-H-C/phc-winner-argon2/blob/4ac8640c2adc1257677d27d3f833c8d1ee68c7d2/src/encoding.c#L242-L252
 type argon2StdCodec struct{}
 
+const argon2HashVersionStr = "v=19"
+
 func (*argon2StdCodec) encodeArgon2Hash(params argon2Params, salt, dk []byte) string {
 	encSalt := base64.RawStdEncoding.EncodeToString(salt)
 	encDK := base64.RawStdEncoding.EncodeToString(dk)
-	return fmt.Sprintf("$argon2i$v=19$m=%d,t=%d,p=%d$%s$%s", params.Memory, params.Time, params.Threads, encSalt, encDK)
+	return fmt.Sprintf(
+		"$%s$%s$m=%d,t=%d,p=%d$%s$%s",
+		params.Kind, argon2HashVersionStr,
+		params.Memory, params.Time, params.Threads,
+		encSalt, encDK)
 }
 
 func parseArgon2HashParams(s string) (params argon2Params, err error) {
@@ -182,7 +232,7 @@ func parseArgon2HashParams(s string) (params argon2Params, err error) {
 			i, err = strconv.ParseUint(kv[1], 10, 8)
 			params.Threads = uint8(i)
 		default:
-			err = errors.New("unknown parameter in hash")
+			err = fmt.Errorf("unknown parameter '%s' in hash", kv[0])
 		}
 		if err != nil {
 			return
@@ -192,30 +242,46 @@ func parseArgon2HashParams(s string) (params argon2Params, err error) {
 }
 
 func (*argon2StdCodec) decodeArgon2Hash(s string) (params argon2Params, salt []byte, dk []byte, err error) {
-	if !strings.HasPrefix(s, "$argon2i$") {
+	var kind string
+	switch {
+	case strings.HasPrefix(s, "$argon2i$"):
+		kind = kindArgon2I
+	case strings.HasPrefix(s, "$argon2id$"):
+		kind = kindArgon2ID
+	default:
 		err = errors.New("not an Argon2 password hash")
 		return
 	}
 
-	parts := strings.SplitN(s[9:], "$", 4)
-	if len(parts) != 4 {
+	parts := strings.SplitN(s, "$", 6)
+	if len(parts) != 6 {
 		err = errors.New("bad encoding")
 		return
 	}
-	if parts[0] != "v=19" {
+	if parts[2] != argon2HashVersionStr {
 		err = errors.New("bad argon2 hash version")
 		return
 	}
 
-	params, err = parseArgon2HashParams(parts[1])
+	params, err = parseArgon2HashParams(parts[3])
 	if err != nil {
 		return
 	}
-	if salt, err = base64.RawStdEncoding.DecodeString(parts[2]); err != nil {
+	params.Kind = kind
+
+	if salt, err = base64.RawStdEncoding.DecodeString(parts[4]); err != nil {
+		return
+	}
+	if dk, err = base64.RawStdEncoding.DecodeString(parts[5]); err != nil {
 		return
 	}
-	dk, err = base64.RawStdEncoding.DecodeString(parts[3])
 
-	log.Printf("params: %+v", params)
+	params.KeySize = len(dk)
+	switch len(dk) {
+	case 16, 24, 32:
+	default:
+		err = errors.New("bad key size")
+	}
+
 	return
 }
diff --git a/vendor/git.autistici.org/ai3/go-common/pwhash/password.go b/vendor/git.autistici.org/ai3/go-common/pwhash/password.go
index 07b093d412991e5bb07f78e9e400b94aad6ae60a..dee3839d76f930773ee0d787077bc3f96a7cffd5 100644
--- a/vendor/git.autistici.org/ai3/go-common/pwhash/password.go
+++ b/vendor/git.autistici.org/ai3/go-common/pwhash/password.go
@@ -4,7 +4,7 @@
 // The format is the well-known dollar-separated field string,
 // extended with optional algorithm-specific parameters:
 //
-//     $id[$params...]$salt$encrypted
+//	$id[$params...]$salt$encrypted
 //
 // We extend 'id' beyond the values supported by the libc crypt(3)
 // function with the following hashing algorithms:
@@ -16,9 +16,8 @@
 // the parameterized benchmarks are named with
 // time/memory(MB)/threads. For nicer results:
 //
-//     go test -bench=Argon2 -run=none . 2>&1 | \
-//         awk '/^Bench/ {ops=1000000000 / $3; print $1 " " ops " ops/sec"}'
-//
+//	go test -bench=Argon2 -run=none . 2>&1 | \
+//	    awk '/^Bench/ {ops=1000000000 / $3; print $1 " " ops " ops/sec"}'
 package pwhash
 
 import (
@@ -49,12 +48,13 @@ func getRandomBytes(n int) []byte {
 
 // A registry of default handlers for decoding passwords.
 var prefixRegistry = map[string]PasswordHash{
-	"$1$":       NewSystemCrypt(),
-	"$5$":       NewSystemCrypt(),
-	"$6$":       NewSystemCrypt(),
-	"$s$":       NewScrypt(),
-	"$a2$":      NewArgon2(),
-	"$argon2i$": NewArgon2Std(),
+	"$1$":        NewSystemCrypt(),
+	"$5$":        NewSystemCrypt(),
+	"$6$":        NewSystemCrypt(),
+	"$s$":        NewScrypt(),
+	"$a2$":       NewArgon2Legacy(),
+	"$argon2i$":  NewArgon2Std(),
+	"$argon2id$": NewArgon2Std(),
 }
 
 // ComparePassword returns true if the given password matches the
@@ -65,6 +65,7 @@ func ComparePassword(encrypted, password string) bool {
 			return h.ComparePassword(encrypted, password)
 		}
 	}
+
 	return false
 }
 
@@ -73,7 +74,7 @@ func ComparePassword(encrypted, password string) bool {
 var DefaultEncryptAlgorithm PasswordHash
 
 func init() {
-	DefaultEncryptAlgorithm = NewArgon2()
+	DefaultEncryptAlgorithm = NewArgon2Std()
 }
 
 // Encrypt will encrypt a password with the default algorithm.
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 573f2b2f99afa17fccb8fed11abcbcb368ce95bc..a18d74a11d1bc3ea496f17ad25717317b2c625c7 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -2,7 +2,7 @@
 ## explicit; go 1.20
 filippo.io/edwards25519
 filippo.io/edwards25519/field
-# git.autistici.org/ai3/go-common v0.0.0-20241017171051-880a2c5ae7f4
+# git.autistici.org/ai3/go-common v0.0.0-20250125130542-62b40adde91d
 ## explicit; go 1.21.0
 git.autistici.org/ai3/go-common
 git.autistici.org/ai3/go-common/clientutil