package util import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "io" "strings" "golang.org/x/crypto/bcrypt" ) func SHA224String(password string) string { hash := sha256.New224() hash.Write([]byte(password)) val := hash.Sum(nil) str := "" for _, v := range val { str += fmt.Sprintf("%02x", v) } return str } func HashPassword(password string) (string, error) { if len(strings.TrimSpace(password)) < 6 { return "", errors.New("password too short") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(hash), nil } func IsBcryptHash(hash string) bool { return strings.HasPrefix(hash, "$2a$") || strings.HasPrefix(hash, "$2b$") || strings.HasPrefix(hash, "$2y$") } func VerifyPassword(password string, storedHash string) (ok bool, legacy bool) { if IsBcryptHash(storedHash) { err := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) return err == nil, false } return SHA224String(password) == storedHash, true } func HmacSHA256Hex(payload string, secret string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(payload)) sum := mac.Sum(nil) str := "" for _, v := range sum { str += fmt.Sprintf("%02x", v) } return str } func DecodeBase64Key(raw string, expectedLen int) ([]byte, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, errors.New("empty key") } decoded, err := base64.StdEncoding.DecodeString(raw) if err != nil { return nil, err } if len(decoded) != expectedLen { return nil, fmt.Errorf("invalid key length: expected %d, got %d", expectedLen, len(decoded)) } return decoded, nil } func EncryptAESGCM(plainText string, key []byte) (string, error) { block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return "", err } cipherText := gcm.Seal(nil, nonce, []byte(plainText), nil) payload := append(nonce, cipherText...) return "v1:" + base64.StdEncoding.EncodeToString(payload), nil } func DecryptAESGCM(cipherText string, key []byte) (string, error) { if !strings.HasPrefix(cipherText, "v1:") { return "", errors.New("unsupported ciphertext version") } encoded := strings.TrimPrefix(cipherText, "v1:") payload, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", err } block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonceSize := gcm.NonceSize() if len(payload) < nonceSize { return "", errors.New("invalid ciphertext") } nonce, enc := payload[:nonceSize], payload[nonceSize:] plain, err := gcm.Open(nil, nonce, enc, nil) if err != nil { return "", err } return string(plain), nil }