package security import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" "io" ) type CredentialCipher struct { aead cipher.AEAD } func NewCredentialCipher(encodedKey string) (*CredentialCipher, error) { key, err := base64.StdEncoding.DecodeString(encodedKey) if err != nil { return nil, fmt.Errorf("decode credential key: %w", err) } if len(key) != 32 { return nil, errors.New("credential key must be a base64-encoded 32-byte key") } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("create credential cipher: %w", err) } aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("create credential AEAD: %w", err) } return &CredentialCipher{aead: aead}, nil } func (c *CredentialCipher) Encrypt(plaintext string) ([]byte, error) { if plaintext == "" { return nil, errors.New("credential cannot be empty") } nonce := make([]byte, c.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("generate credential nonce: %w", err) } return c.aead.Seal(nonce, nonce, []byte(plaintext), nil), nil } func (c *CredentialCipher) Decrypt(ciphertext []byte) (string, error) { if len(ciphertext) < c.aead.NonceSize() { return "", errors.New("credential ciphertext is truncated") } nonce := ciphertext[:c.aead.NonceSize()] plaintext, err := c.aead.Open(nil, nonce, ciphertext[c.aead.NonceSize():], nil) if err != nil { return "", errors.New("decrypt credential: authentication failed") } return string(plaintext), nil }