package localdb import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "io" "os" ) // getEncryptionKey derives a 32-byte key from environment variable or machine ID func getEncryptionKey() []byte { key := os.Getenv("QUOTEFORGE_ENCRYPTION_KEY") if key == "" { // Fallback to a machine-based key (hostname + fixed salt) hostname, _ := os.Hostname() key = hostname + "quoteforge-salt-2024" } // Hash to get exactly 32 bytes for AES-256 hash := sha256.Sum256([]byte(key)) return hash[:] } // Encrypt encrypts plaintext using AES-256-GCM func Encrypt(plaintext string) (string, error) { if plaintext == "" { return "", nil } key := getEncryptionKey() 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(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } // Decrypt decrypts ciphertext that was encrypted with Encrypt func Decrypt(ciphertext string) (string, error) { if ciphertext == "" { return "", nil } key := getEncryptionKey() data, err := base64.StdEncoding.DecodeString(ciphertext) 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(data) < nonceSize { return "", errors.New("ciphertext too short") } nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil) if err != nil { return "", err } return string(plaintext), nil }