package customer import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/binary" "errors" "strconv" "strings" "time" ) var rawURL = base64.RawURLEncoding func RandomToken(n int) string { if n < 16 { n = 16 } b := make([]byte, n) _, _ = rand.Read(b) return rawURL.EncodeToString(b) } func NewPasswordHash(password string) (salt string, hash string, err error) { if len(strings.TrimSpace(password)) < 12 { return "", "", errors.New("password must be at least 12 characters") } s := make([]byte, 16) if _, err := rand.Read(s); err != nil { return "", "", err } h := pbkdf2SHA256([]byte(password), s, 310000, 32) return rawURL.EncodeToString(s), rawURL.EncodeToString(h), nil } func VerifyPassword(password, salt, hash string) bool { s, err := rawURL.DecodeString(salt) if err != nil { return false } want, err := rawURL.DecodeString(hash) if err != nil { return false } got := pbkdf2SHA256([]byte(password), s, 310000, len(want)) return len(got) == len(want) && subtle.ConstantTimeCompare(got, want) == 1 } func pbkdf2SHA256(password, salt []byte, iterations, keyLen int) []byte { hLen := sha256.Size blocks := (keyLen + hLen - 1) / hLen out := make([]byte, 0, blocks*hLen) for block := 1; block <= blocks; block++ { mac := hmac.New(sha256.New, password) mac.Write(salt) var n [4]byte binary.BigEndian.PutUint32(n[:], uint32(block)) mac.Write(n[:]) u := mac.Sum(nil) t := append([]byte(nil), u...) for i := 1; i < iterations; i++ { mac = hmac.New(sha256.New, password) mac.Write(u) u = mac.Sum(nil) for j := range t { t[j] ^= u[j] } } out = append(out, t...) } return out[:keyLen] } // RegistrationChallenge is a stateless, username-bound proof-of-work challenge. // It prevents a solved challenge from being reused to create multiple different // accounts while avoiding an unbounded server-side challenge store. func RegistrationChallenge(secret, username string, ttlSeconds int) string { if ttlSeconds <= 0 { ttlSeconds = 300 } nonce := RandomToken(18) expires := time.Now().UTC().Add(time.Duration(ttlSeconds) * time.Second).Unix() body := strings.ToLower(strings.TrimSpace(username)) + "|" + nonce + "|" + strconv.FormatInt(expires, 10) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(body)) return nonce + "." + strconv.FormatInt(expires, 10) + "." + rawURL.EncodeToString(mac.Sum(nil)) } func VerifyRegistrationChallenge(secret, username, challenge string) bool { parts := strings.Split(challenge, ".") if len(parts) != 3 { return false } expires, err := strconv.ParseInt(parts[1], 10, 64) if err != nil || expires < time.Now().UTC().Unix() || expires > time.Now().UTC().Add(10*time.Minute).Unix() { return false } body := strings.ToLower(strings.TrimSpace(username)) + "|" + parts[0] + "|" + parts[1] mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(body)) want := mac.Sum(nil) got, err := rawURL.DecodeString(parts[2]) return err == nil && len(got) == len(want) && subtle.ConstantTimeCompare(got, want) == 1 } func VerifyRegistrationProof(username, challenge string, counter uint64, bits int) bool { if bits <= 0 { return true } if bits > 28 { return false } msg := "nh-register|" + strings.ToLower(strings.TrimSpace(username)) + "|" + challenge + "|" + strconv.FormatUint(counter, 10) h := sha256.Sum256([]byte(msg)) full := bits / 8 rem := bits % 8 for i := 0; i < full; i++ { if h[i] != 0 { return false } } if rem > 0 { mask := byte(0xff << (8 - rem)) return h[full]&mask == 0 } return true } func HashInviteCode(code string) string { h := sha256.Sum256([]byte(strings.TrimSpace(code))) return base64.RawURLEncoding.EncodeToString(h[:]) }