// Package security provides the security primitives used across the backend: // symmetric encryption for secrets at rest, hashing for access tokens and // passwords, and the prompt-injection / least-privilege controls applied by // the agent runtime. package security import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "io" "golang.org/x/crypto/bcrypt" ) // Crypto wraps an AES-GCM cipher derived from a master key. It is used to // encrypt model API keys at rest. type Crypto struct { aead cipher.AEAD } // NewCrypto initialises an AES-GCM cipher from a 32-byte master key. func NewCrypto(masterKey string) (*Crypto, error) { if len(masterKey) != 32 { return nil, fmt.Errorf("master key must be 32 bytes, got %d", len(masterKey)) } block, err := aes.NewCipher([]byte(masterKey)) if err != nil { return nil, fmt.Errorf("create aes cipher: %w", err) } aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("create gcm: %w", err) } return &Crypto{aead: aead}, nil } // Encrypt returns a base64-encoded ciphertext of the plaintext. A random // nonce is prepended to the ciphertext. func (c *Crypto) Encrypt(plaintext string) (string, error) { if plaintext == "" { return "", nil } nonce := make([]byte, c.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", fmt.Errorf("read nonce: %w", err) } ct := c.aead.Seal(nonce, nonce, []byte(plaintext), nil) return base64.StdEncoding.EncodeToString(ct), nil } // Decrypt reverses Encrypt. An empty input yields an empty output. func (c *Crypto) Decrypt(b64 string) (string, error) { if b64 == "" { return "", nil } raw, err := base64.StdEncoding.DecodeString(b64) if err != nil { return "", fmt.Errorf("base64 decode: %w", err) } ns := c.aead.NonceSize() if len(raw) < ns { return "", errors.New("ciphertext too short") } pt, err := c.aead.Open(nil, raw[:ns], raw[ns:], nil) if err != nil { return "", fmt.Errorf("gcm open: %w", err) } return string(pt), nil } // HashToken returns a hex SHA-256 digest of an app access token. Tokens are // random and high-entropy, so a plain hash (no salt) is sufficient and keeps // lookups O(1) on the unique index. func HashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } // HashPassword returns a bcrypt hash of a password. func HashPassword(password string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", fmt.Errorf("bcrypt: %w", err) } return string(b), nil } // VerifyPassword checks a bcrypt hash against a plaintext password. func VerifyPassword(hash, password string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil }