69 lines
2.3 KiB
Go
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
|
|
}
|