package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "fmt" "io" ) type Encryptor struct { key []byte // 32 bytes for AES-256 } // NewEncryptor создает новый шифровальщик с заданным ключом // key должен быть 32 байта для AES-256 func NewEncryptor(key []byte) (*Encryptor, error) { if len(key) != 32 { return nil, fmt.Errorf("encryption key must be 32 bytes, got %d", len(key)) } return &Encryptor{ key: key, }, nil } // Encrypt шифрует plaintext с использованием AES-GCM // Возвращает base64 encoded ciphertext с appended nonce func (e *Encryptor) Encrypt(plaintext []byte) (string, error) { if len(plaintext) == 0 { return "", nil } // Создаем блок AES block, err := aes.NewCipher(e.key) if err != nil { return "", fmt.Errorf("failed to create cipher: %w", err) } // Создаем GCM режим gcm, err := cipher.NewGCM(block) if err != nil { return "", fmt.Errorf("failed to create GCM: %w", err) } // Генерируем случайный nonce nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", fmt.Errorf("failed to generate nonce: %w", err) } // Шифруем: ciphertext = nonce + encrypted_data ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) // Кодируем в base64 для хранения в БД encoded := base64.StdEncoding.EncodeToString(ciphertext) return encoded, nil } // Decrypt расшифровывает base64 encoded ciphertext // Ожидает формат: nonce + encrypted_data func (e *Encryptor) Decrypt(encodedCiphertext string) ([]byte, error) { if encodedCiphertext == "" { return []byte{}, nil } // Декодируем из base64 ciphertext, err := base64.StdEncoding.DecodeString(encodedCiphertext) if err != nil { return nil, fmt.Errorf("failed to decode base64: %w", err) } if len(ciphertext) == 0 { return []byte{}, nil } // Создаем блок AES block, err := aes.NewCipher(e.key) if err != nil { return nil, fmt.Errorf("failed to create cipher: %w", err) } // Создаем GCM режим gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %w", err) } nonceSize := gcm.NonceSize() if len(ciphertext) < nonceSize { return nil, fmt.Errorf("ciphertext too short") } // Извлекаем nonce и зашифрованные данные nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] // Расшифровываем plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, fmt.Errorf("failed to decrypt: %w", err) } return plaintext, nil } // EncryptString удобная обертка для шифрования строк func (e *Encryptor) EncryptString(plaintext string) (string, error) { return e.Encrypt([]byte(plaintext)) } // DecryptString удобная обертка для расшифровки в строку func (e *Encryptor) DecryptString(encodedCiphertext string) (string, error) { plaintext, err := e.Decrypt(encodedCiphertext) if err != nil { return "", err } return string(plaintext), nil } // MustEncrypt паникует при ошибке шифрования (для инициализации) func (e *Encryptor) MustEncrypt(plaintext string) string { encrypted, err := e.EncryptString(plaintext) if err != nil { panic(fmt.Sprintf("encryption failed: %v", err)) } return encrypted } // MustDecrypt паникует при ошибке расшифровки (для инициализации) func (e *Encryptor) MustDecrypt(encodedCiphertext string) string { decrypted, err := e.DecryptString(encodedCiphertext) if err != nil { panic(fmt.Sprintf("decryption failed: %v", err)) } return decrypted } // GenerateRandomKey генерирует случайный 32-байтный ключ для AES-256 func GenerateRandomKey() ([]byte, error) { key := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, key); err != nil { return nil, fmt.Errorf("failed to generate random key: %w", err) } return key, nil } // GenerateRandomKeyString генерирует случайный ключ в виде hex строки func GenerateRandomKeyString() (string, error) { key, err := GenerateRandomKey() if err != nil { return "", err } return base64.StdEncoding.EncodeToString(key), nil } // KeyFromString создает ключ из строки (ожидается 32 байта в base64 или plain) func KeyFromString(keyStr string) ([]byte, error) { // Пробуем декодировать как base64 key, err := base64.StdEncoding.DecodeString(keyStr) if err == nil && len(key) == 32 { return key, nil } // Если не base64, используем как есть (должно быть 32 байта) if len(keyStr) == 32 { return []byte(keyStr), nil } return nil, fmt.Errorf("invalid key length: expected 32 bytes, got %d", len(keyStr)) }