Enchantrix/pkg/trix/crypto.go
Claude afb11667e6
feat: add encryption sigil with pre-obfuscation layer
Implements ChaChaPolySigil that applies pre-obfuscation before sending
data to CPU encryption routines. This ensures raw plaintext is never
passed directly to encryption functions.

Key improvements:
- XORObfuscator and ShuffleMaskObfuscator for pre-encryption transforms
- Nonce is now properly embedded in ciphertext, not stored separately
  in headers (production-ready, not demo-style)
- Trix crypto integration with EncryptPayload/DecryptPayload methods
- Comprehensive test coverage following Good/Bad/Ugly pattern
2025-12-25 12:36:32 +00:00

189 lines
5.3 KiB
Go

package trix
import (
"errors"
"time"
"github.com/Snider/Enchantrix/pkg/enchantrix"
)
var (
// ErrNoEncryptionKey is returned when encryption is requested without a key.
ErrNoEncryptionKey = errors.New("trix: encryption key not configured")
// ErrAlreadyEncrypted is returned when trying to encrypt already encrypted data.
ErrAlreadyEncrypted = errors.New("trix: payload is already encrypted")
// ErrNotEncrypted is returned when trying to decrypt non-encrypted data.
ErrNotEncrypted = errors.New("trix: payload is not encrypted")
)
const (
// HeaderKeyEncrypted indicates whether the payload is encrypted.
HeaderKeyEncrypted = "encrypted"
// HeaderKeyAlgorithm stores the encryption algorithm used.
HeaderKeyAlgorithm = "encryption_algorithm"
// HeaderKeyEncryptedAt stores when the payload was encrypted.
HeaderKeyEncryptedAt = "encrypted_at"
// HeaderKeyObfuscator stores the obfuscator type used.
HeaderKeyObfuscator = "obfuscator"
// AlgorithmChaCha20Poly1305 is the identifier for ChaCha20-Poly1305.
AlgorithmChaCha20Poly1305 = "xchacha20-poly1305"
// ObfuscatorXOR identifies the XOR obfuscator.
ObfuscatorXOR = "xor"
// ObfuscatorShuffleMask identifies the shuffle-mask obfuscator.
ObfuscatorShuffleMask = "shuffle-mask"
)
// CryptoConfig holds encryption configuration for a Trix container.
type CryptoConfig struct {
// Key is the 32-byte encryption key.
Key []byte
// Obfuscator type: "xor" (default) or "shuffle-mask"
Obfuscator string
}
// EncryptPayload encrypts the Trix payload using ChaCha20-Poly1305 with pre-obfuscation.
//
// The nonce is embedded in the ciphertext itself and is NOT stored separately
// in the header. This is the production-ready approach (not demo-style).
//
// Header metadata is updated to indicate encryption status without exposing
// cryptographic parameters that are already embedded in the ciphertext.
func (t *Trix) EncryptPayload(config *CryptoConfig) error {
if config == nil || len(config.Key) != 32 {
return ErrNoEncryptionKey
}
// Check if already encrypted
if encrypted, ok := t.Header[HeaderKeyEncrypted].(bool); ok && encrypted {
return ErrAlreadyEncrypted
}
// Create the obfuscator
var obfuscator enchantrix.PreObfuscator
obfuscatorName := ObfuscatorXOR
switch config.Obfuscator {
case ObfuscatorShuffleMask:
obfuscator = &enchantrix.ShuffleMaskObfuscator{}
obfuscatorName = ObfuscatorShuffleMask
default:
obfuscator = &enchantrix.XORObfuscator{}
}
// Create the encryption sigil
sigil, err := enchantrix.NewChaChaPolySigilWithObfuscator(config.Key, obfuscator)
if err != nil {
return err
}
// Encrypt the payload
ciphertext, err := sigil.In(t.Payload)
if err != nil {
return err
}
// Update payload with ciphertext
t.Payload = ciphertext
// Update header with encryption metadata
// NOTE: We do NOT store the nonce in the header - it's embedded in the ciphertext
if t.Header == nil {
t.Header = make(map[string]interface{})
}
t.Header[HeaderKeyEncrypted] = true
t.Header[HeaderKeyAlgorithm] = AlgorithmChaCha20Poly1305
t.Header[HeaderKeyObfuscator] = obfuscatorName
t.Header[HeaderKeyEncryptedAt] = time.Now().UTC().Format(time.RFC3339)
return nil
}
// DecryptPayload decrypts the Trix payload using the provided key.
//
// The nonce is extracted from the ciphertext itself - no need to read it
// from the header separately.
func (t *Trix) DecryptPayload(config *CryptoConfig) error {
if config == nil || len(config.Key) != 32 {
return ErrNoEncryptionKey
}
// Check if encrypted
encrypted, ok := t.Header[HeaderKeyEncrypted].(bool)
if !ok || !encrypted {
return ErrNotEncrypted
}
// Determine obfuscator from header
var obfuscator enchantrix.PreObfuscator
if obfType, ok := t.Header[HeaderKeyObfuscator].(string); ok {
switch obfType {
case ObfuscatorShuffleMask:
obfuscator = &enchantrix.ShuffleMaskObfuscator{}
default:
obfuscator = &enchantrix.XORObfuscator{}
}
} else {
obfuscator = &enchantrix.XORObfuscator{}
}
// Create the decryption sigil
sigil, err := enchantrix.NewChaChaPolySigilWithObfuscator(config.Key, obfuscator)
if err != nil {
return err
}
// Decrypt the payload
plaintext, err := sigil.Out(t.Payload)
if err != nil {
return err
}
// Update payload with plaintext
t.Payload = plaintext
// Update header to indicate decrypted state
t.Header[HeaderKeyEncrypted] = false
return nil
}
// IsEncrypted returns true if the payload is currently encrypted.
func (t *Trix) IsEncrypted() bool {
if t.Header == nil {
return false
}
encrypted, ok := t.Header[HeaderKeyEncrypted].(bool)
return ok && encrypted
}
// GetEncryptionAlgorithm returns the encryption algorithm used, if any.
func (t *Trix) GetEncryptionAlgorithm() string {
if t.Header == nil {
return ""
}
algo, ok := t.Header[HeaderKeyAlgorithm].(string)
if !ok {
return ""
}
return algo
}
// NewEncryptedTrix creates a new Trix container with an encrypted payload.
// This is a convenience function for creating encrypted containers in one step.
func NewEncryptedTrix(payload []byte, key []byte, header map[string]interface{}) (*Trix, error) {
if header == nil {
header = make(map[string]interface{})
}
t := &Trix{
Header: header,
Payload: payload,
}
config := &CryptoConfig{Key: key}
if err := t.EncryptPayload(config); err != nil {
return nil, err
}
return t, nil
}