// Package crypto provides AES-256-GCM sealing for customer ticketing // credentials stored in Mongo (ยง4.2): ciphertext format base64(nonce|sealed). package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" ) const keyLen = 32 // Seal encrypts plaintext with AES-256-GCM and returns // base64(nonce|ciphertext) for storage. func Seal(key, plaintext []byte) (string, error) { gcm, err := newGCM(key) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return "", fmt.Errorf("nonce: %w", err) } sealed := gcm.Seal(nonce, nonce, plaintext, nil) return base64.StdEncoding.EncodeToString(sealed), nil } // Open decrypts a value produced by Seal. func Open(key []byte, encoded string) ([]byte, error) { gcm, err := newGCM(key) if err != nil { return nil, err } raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return nil, fmt.Errorf("decode ciphertext: %w", err) } if len(raw) < gcm.NonceSize() { return nil, errors.New("ciphertext shorter than nonce") } plaintext, err := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil) if err != nil { return nil, fmt.Errorf("decrypt: %w", err) } return plaintext, nil } func newGCM(key []byte) (cipher.AEAD, error) { if len(key) != keyLen { return nil, fmt.Errorf("key must be %d bytes, got %d", keyLen, len(key)) } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("aes: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("gcm: %w", err) } return gcm, nil }