// Package secretbox encrypts small secrets (tokens, passwords, private keys) // before they are written to the database, using AES-256-GCM. // // Sealed values look like "enc1:". Opening a value that isn't sealed // returns it unchanged, so data written before encryption was turned on is // still readable and gets sealed the next time it is saved. package secretbox import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "strings" ) const prefix = "enc1:" type Box struct{ aead cipher.AEAD } // GenerateKey returns a new random key in the form New accepts. func GenerateKey() string { b := make([]byte, 32) rand.Read(b) return base64.StdEncoding.EncodeToString(b) } // New builds a Box from a key given as base64 or hex of 32 bytes. Any other // text is treated as a passphrase and stretched with SHA-256. func New(key string) (*Box, error) { key = strings.TrimSpace(key) if key == "" { return nil, errors.New("empty key") } raw := decodeKey(key) block, err := aes.NewCipher(raw) if err != nil { return nil, err } aead, err := cipher.NewGCM(block) if err != nil { return nil, err } return &Box{aead: aead}, nil } func decodeKey(key string) []byte { if b, err := base64.StdEncoding.DecodeString(key); err == nil && len(b) == 32 { return b } if b, err := hex.DecodeString(key); err == nil && len(b) == 32 { return b } sum := sha256.Sum256([]byte(key)) return sum[:] } // Seal encrypts s. The empty string stays empty so unset secrets stay visibly unset. func (b *Box) Seal(s string) string { if s == "" || strings.HasPrefix(s, prefix) { return s } nonce := make([]byte, b.aead.NonceSize()) rand.Read(nonce) return prefix + base64.StdEncoding.EncodeToString(b.aead.Seal(nonce, nonce, []byte(s), nil)) } // Open decrypts a sealed value. A wrong key or damaged data is an error, so a // bad key can never silently turn secrets into garbage. func (b *Box) Open(s string) (string, error) { if !strings.HasPrefix(s, prefix) { return s, nil } raw, err := base64.StdEncoding.DecodeString(s[len(prefix):]) if err != nil || len(raw) < b.aead.NonceSize() { return "", errors.New("damaged secret") } n := b.aead.NonceSize() plain, err := b.aead.Open(nil, raw[:n], raw[n:], nil) if err != nil { return "", errors.New("can't decrypt a stored secret: is HAMMERHEAD_MASTER_KEY the key it was saved with?") } return string(plain), nil }