package cryptox import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" ) var ErrKeyUnavailable = errors.New("encryption key unavailable") type AESGCM struct { aead cipher.AEAD version int purpose string } func NewAESGCM(encodedKey string, version int, purpose string) (*AESGCM, error) { if encodedKey == "" { return nil, nil } key, err := base64.StdEncoding.DecodeString(encodedKey) if err != nil { key, err = base64.RawStdEncoding.DecodeString(encodedKey) } if err != nil || len(key) != 32 { return nil, errors.New("encryption key must be base64-encoded 32 bytes") } if version < 1 || purpose == "" { return nil, errors.New("encryption version and purpose are required") } block, err := aes.NewCipher(key) if err != nil { return nil, err } aead, err := cipher.NewGCM(block) if err != nil { return nil, err } return &AESGCM{aead: aead, version: version, purpose: purpose}, nil } func (c *AESGCM) Encrypt(plaintext []byte) ([]byte, int, error) { if c == nil { return nil, 0, ErrKeyUnavailable } nonce := make([]byte, c.aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, 0, err } ciphertext := c.aead.Seal(nil, nonce, plaintext, c.additionalData()) return append(nonce, ciphertext...), c.version, nil } func (c *AESGCM) Decrypt(encrypted []byte, version int) ([]byte, error) { if c == nil { return nil, ErrKeyUnavailable } if version != c.version { return nil, fmt.Errorf("encryption key version %d is not loaded", version) } nonceSize := c.aead.NonceSize() if len(encrypted) <= nonceSize { return nil, errors.New("encrypted value is truncated") } plaintext, err := c.aead.Open(nil, encrypted[:nonceSize], encrypted[nonceSize:], c.additionalData()) if err != nil { return nil, errors.New("encrypted value authentication failed") } return plaintext, nil } func (c *AESGCM) additionalData() []byte { return []byte(fmt.Sprintf("ai-gateway/%s/v%d", c.purpose, c.version)) }