// Package seal provides authenticated encryption utilities for pixa. // It uses NaCl secretbox (XSalsa20-Poly1305) for sealing data // and HKDF-SHA256 for key derivation. package seal import ( "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "io" "golang.org/x/crypto/hkdf" "golang.org/x/crypto/nacl/secretbox" ) // Key sizes for NaCl secretbox. const ( KeySize = 32 // NaCl secretbox key size NonceSize = 24 // NaCl secretbox nonce size ) // Errors returned by crypto operations. var ( ErrDecryptionFailed = errors.New("decryption failed: invalid ciphertext or key") ErrInvalidPayload = errors.New("invalid encrypted payload") ErrKeyDerivation = errors.New("key derivation failed") ) // DeriveKey uses HKDF-SHA256 to derive a key from master key material. // The salt parameter provides domain separation between different key usages. func DeriveKey(masterKey []byte, salt string) ([KeySize]byte, error) { var key [KeySize]byte hkdfReader := hkdf.New(sha256.New, masterKey, []byte(salt), nil) if _, err := io.ReadFull(hkdfReader, key[:]); err != nil { return key, ErrKeyDerivation } return key, nil } // Encrypt encrypts plaintext using NaCl secretbox (XSalsa20-Poly1305). // Returns base64url-encoded ciphertext with the nonce prepended. func Encrypt(key [KeySize]byte, plaintext []byte) (string, error) { // Generate random nonce var nonce [NonceSize]byte if _, err := rand.Read(nonce[:]); err != nil { return "", err } // Encrypt: nonce + secretbox.Seal(plaintext) encrypted := secretbox.Seal(nonce[:], plaintext, &nonce, &key) return base64.RawURLEncoding.EncodeToString(encrypted), nil } // Decrypt decrypts base64url-encoded ciphertext using NaCl secretbox. // Expects the nonce to be prepended to the ciphertext. func Decrypt(key [KeySize]byte, ciphertext string) ([]byte, error) { // Decode base64url data, err := base64.RawURLEncoding.DecodeString(ciphertext) if err != nil { return nil, ErrInvalidPayload } // Check minimum length (nonce + at least some ciphertext + auth tag) if len(data) < NonceSize+secretbox.Overhead { return nil, ErrInvalidPayload } // Extract nonce var nonce [NonceSize]byte copy(nonce[:], data[:NonceSize]) // Decrypt plaintext, ok := secretbox.Open(nil, data[NonceSize:], &nonce, &key) if !ok { return nil, ErrDecryptionFailed } return plaintext, nil }