// Package seal encrypts purpose-bound values for app handoffs and local storage. package seal import ( "crypto/aes" "crypto/cipher" "crypto/hkdf" "crypto/rand" "crypto/sha256" "errors" ) // Sealer owns a copy of the key and an application namespace. type Sealer struct { dek []byte namespace string } // New requires a 32-byte key and an explicit namespace. Keep the namespace // stable: changing it makes previously sealed values unreadable. func New(dek []byte, namespace string) (*Sealer, error) { if len(dek) != 32 { return nil, errors.New("seal: DEK must be 32 bytes") } if len(namespace) == 0 || len(namespace) > 128 { return nil, errors.New("seal: invalid namespace") } for _, b := range []byte(namespace) { if !(b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '/' || b == '_' || b == '-') { return nil, errors.New("seal: invalid namespace") } } return &Sealer{dek: append([]byte(nil), dek...), namespace: namespace}, nil } func (s *Sealer) purposeKey(purpose string) ([]byte, error) { return hkdf.Key(sha256.New, s.dek, nil, s.namespace+":"+purpose, 32) } func (s *Sealer) aeadFor(purpose string) (cipher.AEAD, error) { key, err := s.purposeKey(purpose) if err != nil { return nil, err } block, err := aes.NewCipher(key) if err != nil { return nil, err } return cipher.NewGCM(block) } // Seal uses the historical Oficina envelope: random nonce followed by GCM // ciphertext and authentication tag. The namespace and purpose bind the key. func (s *Sealer) Seal(purpose string, plaintext []byte) ([]byte, error) { aead, err := s.aeadFor(purpose) if err != nil { return nil, err } nonce := make([]byte, aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } return aead.Seal(nonce, nonce, plaintext, nil), nil } // Open refuses a different key, namespace or purpose, and altered ciphertext. func (s *Sealer) Open(purpose string, ct []byte) ([]byte, error) { aead, err := s.aeadFor(purpose) if err != nil { return nil, err } if len(ct) < aead.NonceSize() { return nil, errors.New("seal: ciphertext too short") } return aead.Open(nil, ct[:aead.NonceSize()], ct[aead.NonceSize():], nil) }