Files
Cyrene_v2/internal/auth/password.go
T

69 lines
2.3 KiB
Go

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$<salt>$<digest>". 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
}