package util import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "io" "strings" "golang.org/x/crypto/bcrypt" ) // Функции SHA224String здесь больше нет, как и ветки сравнения паролей по ней. // // Это был формат хранения паролей предыдущего поколения: несолёный SHA-224, // то есть быстрый хеш общего назначения, по которому словарная атака идёт // на порядки быстрее, чем по bcrypt, а одинаковые пароли дают одинаковые // хеши. VerifyPassword принимал такие хеши как «legacy» и молча переписывал // их в bcrypt при первом успешном входе. // // В HY2XS v1 такого хеша не может существовать. Единственные места, создающие // учётную запись, — ensureSecureBootstrapAdmin и `reset-admin`, и оба пишут // bcrypt; миграции 002/003, переносившие таблицу `account` предыдущего // поколения, удалены; установка возможна только на чистый хост, а конфигурация // 0.x отклоняется по HY2XS_CONFIG_SCHEMA_VERSION. То есть compatibility-ветка // пережила ровно тот слой совместимости, ради которого существовала, и // осталась запасным путём проверки пароля со слабым алгоритмом — в обработчике // логина, доступном без аутентификации. 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$") } // VerifyPassword проверяет пароль ровно против одного формата хранения. // // Хеш, который не является bcrypt, — это не «пароль в старом формате», а // повреждённая или подменённая строка в базе, и правильный ответ на неё — // отказ во входе. func VerifyPassword(password string, storedHash string) bool { if !IsBcryptHash(storedHash) { return false } return bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) == nil } 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 }