package crypto import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "io" ) // Encrypt encrypts plaintext using AES-GCM with the provided key. // The key must be 32 bytes (AES-256). func Encrypt(plaintext []byte, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } // Seal appends the ciphertext to the nonce, so we return [nonce][ciphertext] return gcm.Seal(nonce, nonce, plaintext, nil), nil } // Decrypt decrypts ciphertext using AES-GCM with the provided key. func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonceSize := gcm.NonceSize() if len(ciphertext) < nonceSize { return nil, fmt.Errorf("ciphertext too short") } nonce, actualCiphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] return gcm.Open(nil, nonce, actualCiphertext, nil) } // GenerateBlindIndex creates a deterministic HMAC-SHA256 hash of a value. // This allows searching for encrypted data without knowing the plaintext. func GenerateBlindIndex(value string, salt []byte) string { h := hmac.New(sha256.New, salt) h.Write([]byte(value)) return hex.EncodeToString(h.Sum(nil)) } // GenerateKey generates a random 32-byte key. func GenerateKey() ([]byte, error) { key := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, key); err != nil { return nil, err } return key, nil }