package auth import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "strings" ) // HashPassword derives a salted, iterated SHA-256 digest of password and // returns it encoded as "sha256$$". Each call uses a fresh random // salt, so equal passwords produce different encodings. // // NOTE: this is a self-contained, dependency-free scheme that is deliberately // weaker than bcrypt (cost 12). It is acceptable for the current single-binary // milestone; swap to golang.org/x/crypto/bcrypt before exposing auth to // untrusted parties in production. func HashPassword(password string) (string, error) { salt := make([]byte, saltLength) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("auth: generate salt: %w", err) } digest := derivePassword(password, salt, passwordIterations) return passwordScheme + "$" + base64.RawStdEncoding.EncodeToString(salt) + "$" + base64.RawStdEncoding.EncodeToString(digest), nil } // CheckPassword reports whether password matches the encoded hash. Malformed // input, an unknown scheme, or a mismatch all return false. func CheckPassword(password, encoded string) bool { scheme, salt, digest, err := parsePasswordHash(encoded) if err != nil || scheme != passwordScheme { return false } want := derivePassword(password, salt, passwordIterations) return subtle.ConstantTimeCompare(want, digest) == 1 } // parsePasswordHash splits an encoded hash into scheme, salt, and digest. func parsePasswordHash(encoded string) (scheme string, salt, digest []byte, err error) { parts := strings.Split(encoded, "$") if len(parts) != 3 { return "", nil, nil, fmt.Errorf("auth: malformed password hash") } salt, err = base64.RawStdEncoding.DecodeString(parts[1]) if err != nil { return "", nil, nil, fmt.Errorf("auth: decode salt: %w", err) } digest, err = base64.RawStdEncoding.DecodeString(parts[2]) if err != nil { return "", nil, nil, fmt.Errorf("auth: decode digest: %w", err) } return parts[0], salt, digest, nil } // derivePassword returns the iterated salted SHA-256 digest of password. func derivePassword(password string, salt []byte, iterations int) []byte { sum := sha256.Sum256(append(salt, []byte(password)...)) out := sum[:] for i := 1; i < iterations; i++ { cur := sha256.Sum256(append(out, salt...)) out = cur[:] } return out }