Artifact 0f8257af851a52d147a8681b46f4d15ff4bd65d558fd8b2055fb2a5a2e11be07:
- File
internal/oltp/password.go
— part of check-in
[bf5ba9d2fc]
at
2026-08-16 17:07:31
on branch trunk
— oltp: accounts and per-repo roles
0001 §8's v2 tables. Accounts are the web/API identity (argon2id verifier, site-admin flag, read/write/admin per repo); the RFC §6.3 shared secret stays the sync credential, since the protocol fixes its derivation and the server must recompute it.
ops.Auth becomes the required role, so the Admin level that was already declared now means something. The gate stays open until the first account exists, mirroring the sync-user policy, so existing deployments and local single-user serving are unaffected. (user: dmestas, size: 2454) [annotate] [blame] [check-ins using]
// password.go holds the account password verifier. This is quarry's own // choice and is deliberately NOT the sync protocol's credential: RFC §6.3 // fixes the push login as SHA1(project-code/user/password), a value the // server must be able to recompute, which rules it out as a password // store. An account password is verified here and never recovered, so it // gets a memory-hard KDF instead. package oltp import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // argon2id parameters. Cost lives IN the encoded hash, so raising these // later leaves existing hashes verifiable — each one carries the // parameters it was made with. const ( argonTime = 1 argonMemory = 64 * 1024 // 64 MiB argonThreads = 4 argonKeyLen = 32 argonSaltLen = 16 ) // HashPassword returns an encoded argon2id verifier in the standard // PHC string format, salt and all. func HashPassword(plain string) (string, error) { if plain == "" { return "", errors.New("oltp: empty password") } salt := make([]byte, argonSaltLen) if _, err := rand.Read(salt); err != nil { return "", err } key := argon2.IDKey([]byte(plain), salt, argonTime, argonMemory, argonThreads, argonKeyLen) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argonMemory, argonTime, argonThreads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(key)), nil } // VerifyPassword reports whether plain matches the encoded verifier. A // malformed verifier is a mismatch, never a panic — the comparison is // constant-time so a wrong password and a wrong length cost the same. func VerifyPassword(encoded, plain string) bool { parts := strings.Split(encoded, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false } var version int if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil || version != argon2.Version { return false } var memory uint32 var time uint32 var threads uint8 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil { return false } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false } want, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false } got := argon2.IDKey([]byte(plain), salt, time, memory, threads, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1 }