package identity import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/binary" "encoding/hex" "errors" "fmt" "strconv" "strings" ) const ( currentPBKDF2Iterations = 600_000 legacyPBKDF2Iterations = 120_000 maximumPBKDF2Iterations = 2_000_000 passwordDigestBytes = 32 ) var ErrInvalidPasswordHash = errors.New("invalid password hash") type PasswordHasher struct{} func (PasswordHasher) Hash(password string) (string, error) { saltBytes := make([]byte, 16) if _, err := rand.Read(saltBytes); err != nil { return "", fmt.Errorf("generate password salt: %w", err) } return hashWithSalt(password, hex.EncodeToString(saltBytes), currentPBKDF2Iterations), nil } func (PasswordHasher) Verify(password, stored string) bool { iterations, salt, expected, err := parsePasswordHash(stored) if err != nil { return false } actual := pbkdf2SHA256([]byte(password), []byte(salt), iterations, len(expected)) return subtle.ConstantTimeCompare(actual, expected) == 1 } func (PasswordHasher) NeedsUpgrade(stored string) bool { iterations, _, _, err := parsePasswordHash(stored) return err == nil && iterations < currentPBKDF2Iterations } func hashWithSalt(password, salt string, iterations int) string { digest := pbkdf2SHA256([]byte(password), []byte(salt), iterations, passwordDigestBytes) return fmt.Sprintf("pbkdf2_sha256$%d$%s$%s", iterations, salt, hex.EncodeToString(digest)) } func parsePasswordHash(stored string) (int, string, []byte, error) { parts := strings.Split(stored, "$") iterations := legacyPBKDF2Iterations var salt, encodedDigest string switch { case len(parts) == 4 && parts[0] == "pbkdf2_sha256": parsed, err := strconv.Atoi(parts[1]) if err != nil { return 0, "", nil, ErrInvalidPasswordHash } iterations, salt, encodedDigest = parsed, parts[2], parts[3] case len(parts) == 2: salt, encodedDigest = parts[0], parts[1] default: return 0, "", nil, ErrInvalidPasswordHash } if iterations < 1 || iterations > maximumPBKDF2Iterations || salt == "" { return 0, "", nil, ErrInvalidPasswordHash } digest, err := hex.DecodeString(encodedDigest) if err != nil || len(digest) != passwordDigestBytes { return 0, "", nil, ErrInvalidPasswordHash } return iterations, salt, digest, nil } func pbkdf2SHA256(password, salt []byte, iterations, keyLength int) []byte { const hashLength = sha256.Size blocks := (keyLength + hashLength - 1) / hashLength result := make([]byte, 0, blocks*hashLength) buffer := make([]byte, len(salt)+4) copy(buffer, salt) for block := 1; block <= blocks; block++ { binary.BigEndian.PutUint32(buffer[len(salt):], uint32(block)) mac := hmac.New(sha256.New, password) _, _ = mac.Write(buffer) u := mac.Sum(nil) t := append([]byte(nil), u...) for round := 1; round < iterations; round++ { mac.Reset() _, _ = mac.Write(u) u = mac.Sum(nil) for index := range t { t[index] ^= u[index] } } result = append(result, t...) } return result[:keyLength] }