// Package secret encrypts the few values the server must be able to hand back // out again — today, each site's broker password. // // A password that gets given to an enrolling PC cannot be hashed, so the // choice is plaintext in a column or encrypted with a key that lives outside // the database. Encrypted means a stolen dump is not a set of live broker // logins, which is exactly what the plaintext column would be. package secret import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" "os" ) // Box seals and opens values with AES-256-GCM. type Box struct{ aead cipher.AEAD } var ErrNoKey = errors.New("BEHAVISION_SECRET_KEY is not set") // FromEnv builds a Box from a base64 32-byte key. // // Refuses a short key outright rather than stretching it. A key derived from // whatever someone typed into an env var is a key with unknown entropy, and // "it worked" would hide that permanently. func FromEnv(name string) (*Box, error) { raw := os.Getenv(name) if raw == "" { return nil, ErrNoKey } key, err := base64.StdEncoding.DecodeString(raw) if err != nil { key, err = base64.RawURLEncoding.DecodeString(raw) } if err != nil { return nil, fmt.Errorf("%s must be base64: %w", name, err) } return New(key) } func New(key []byte) (*Box, error) { if len(key) != 32 { return nil, fmt.Errorf("key must be 32 bytes, got %d "+ "(generate one with: openssl rand -base64 32)", len(key)) } blk, err := aes.NewCipher(key) if err != nil { return nil, err } aead, err := cipher.NewGCM(blk) if err != nil { return nil, err } return &Box{aead: aead}, nil } // NewKey generates a key for provisioning. func NewKey() (string, error) { var k [32]byte if _, err := rand.Read(k[:]); err != nil { return "", err } return base64.StdEncoding.EncodeToString(k[:]), nil } // Seal returns nonce||ciphertext. // // `aad` binds the ciphertext to where it is stored — the agent id for a broker // password. Without it a row copied from one agent to another decrypts happily, // so a database write becomes a way to hand one site another site's // credentials. func (b *Box) Seal(plain []byte, aad string) ([]byte, error) { nonce := make([]byte, b.aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } return b.aead.Seal(nonce, nonce, plain, []byte(aad)), nil } func (b *Box) Open(sealed []byte, aad string) ([]byte, error) { n := b.aead.NonceSize() if len(sealed) < n { return nil, errors.New("ciphertext is truncated") } out, err := b.aead.Open(nil, sealed[:n], sealed[n:], []byte(aad)) if err != nil { // Deliberately vague to the caller's caller: whether a value failed to // decrypt because of the key or because of tampering is not something // to report over HTTP. return nil, errors.New("cannot decrypt: wrong key or altered data") } return out, nil } func (b *Box) SealString(plain, aad string) ([]byte, error) { return b.Seal([]byte(plain), aad) } func (b *Box) OpenString(sealed []byte, aad string) (string, error) { out, err := b.Open(sealed, aad) return string(out), err }