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
438 lines
12 KiB
Go
438 lines
12 KiB
Go
package trix_test
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"github.com/Snider/Enchantrix/pkg/trix"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestEncryptPayload_Good(t *testing.T) {
|
|
t.Run("BasicEncryption", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
for i := range key {
|
|
key[i] = byte(i + 1)
|
|
}
|
|
|
|
originalPayload := []byte("This is a secret message that should be encrypted.")
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{"content_type": "text/plain"},
|
|
Payload: originalPayload,
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
// Verify encryption occurred
|
|
assert.True(t, trixContainer.IsEncrypted())
|
|
assert.Equal(t, trix.AlgorithmChaCha20Poly1305, trixContainer.GetEncryptionAlgorithm())
|
|
assert.NotEqual(t, originalPayload, trixContainer.Payload)
|
|
|
|
// Verify header metadata
|
|
assert.Equal(t, true, trixContainer.Header[trix.HeaderKeyEncrypted])
|
|
assert.Equal(t, trix.AlgorithmChaCha20Poly1305, trixContainer.Header[trix.HeaderKeyAlgorithm])
|
|
assert.Equal(t, trix.ObfuscatorXOR, trixContainer.Header[trix.HeaderKeyObfuscator])
|
|
assert.NotEmpty(t, trixContainer.Header[trix.HeaderKeyEncryptedAt])
|
|
|
|
// Verify NO nonce in header (this is the key improvement over demo-style)
|
|
_, hasNonce := trixContainer.Header["nonce"]
|
|
assert.False(t, hasNonce, "nonce should NOT be stored in header")
|
|
})
|
|
|
|
t.Run("WithShuffleMaskObfuscator", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
payload := []byte("test data")
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: payload,
|
|
}
|
|
|
|
config := &trix.CryptoConfig{
|
|
Key: key,
|
|
Obfuscator: trix.ObfuscatorShuffleMask,
|
|
}
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, trix.ObfuscatorShuffleMask, trixContainer.Header[trix.HeaderKeyObfuscator])
|
|
})
|
|
|
|
t.Run("WithNilHeader", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
trixContainer := &trix.Trix{
|
|
Payload: []byte("test"),
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, trixContainer.Header)
|
|
assert.True(t, trixContainer.IsEncrypted())
|
|
})
|
|
}
|
|
|
|
func TestEncryptPayload_Bad(t *testing.T) {
|
|
t.Run("NilConfig", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{Payload: []byte("test")}
|
|
err := trixContainer.EncryptPayload(nil)
|
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
|
})
|
|
|
|
t.Run("InvalidKeySize", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{Payload: []byte("test")}
|
|
|
|
config := &trix.CryptoConfig{Key: []byte("too short")}
|
|
err := trixContainer.EncryptPayload(config)
|
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
|
})
|
|
|
|
t.Run("AlreadyEncrypted", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
|
Payload: []byte("test"),
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
err := trixContainer.EncryptPayload(config)
|
|
assert.ErrorIs(t, err, trix.ErrAlreadyEncrypted)
|
|
})
|
|
}
|
|
|
|
func TestDecryptPayload_Good(t *testing.T) {
|
|
t.Run("BasicDecryption", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
for i := range key {
|
|
key[i] = byte(i + 1)
|
|
}
|
|
|
|
originalPayload := []byte("This is a secret message that should be encrypted.")
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: originalPayload,
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
|
|
// Encrypt
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
assert.True(t, trixContainer.IsEncrypted())
|
|
|
|
// Decrypt
|
|
err = trixContainer.DecryptPayload(config)
|
|
require.NoError(t, err)
|
|
assert.False(t, trixContainer.IsEncrypted())
|
|
assert.Equal(t, originalPayload, trixContainer.Payload)
|
|
})
|
|
|
|
t.Run("WithShuffleMaskObfuscator", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
originalPayload := []byte("test with shuffle mask")
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: originalPayload,
|
|
}
|
|
|
|
config := &trix.CryptoConfig{
|
|
Key: key,
|
|
Obfuscator: trix.ObfuscatorShuffleMask,
|
|
}
|
|
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
err = trixContainer.DecryptPayload(config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, originalPayload, trixContainer.Payload)
|
|
})
|
|
|
|
t.Run("EmptyPayload", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: []byte{},
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
err = trixContainer.DecryptPayload(config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte{}, trixContainer.Payload)
|
|
})
|
|
}
|
|
|
|
func TestDecryptPayload_Bad(t *testing.T) {
|
|
t.Run("NilConfig", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
|
Payload: []byte("encrypted data"),
|
|
}
|
|
err := trixContainer.DecryptPayload(nil)
|
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
|
})
|
|
|
|
t.Run("InvalidKeySize", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
|
Payload: []byte("encrypted data"),
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: []byte("too short")}
|
|
err := trixContainer.DecryptPayload(config)
|
|
assert.ErrorIs(t, err, trix.ErrNoEncryptionKey)
|
|
})
|
|
|
|
t.Run("NotEncrypted", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: []byte("not encrypted"),
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
err := trixContainer.DecryptPayload(config)
|
|
assert.ErrorIs(t, err, trix.ErrNotEncrypted)
|
|
})
|
|
|
|
t.Run("WrongKey", func(t *testing.T) {
|
|
key1 := make([]byte, 32)
|
|
key2 := make([]byte, 32)
|
|
key2[0] = 1
|
|
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: []byte("secret"),
|
|
}
|
|
|
|
config1 := &trix.CryptoConfig{Key: key1}
|
|
err := trixContainer.EncryptPayload(config1)
|
|
require.NoError(t, err)
|
|
|
|
config2 := &trix.CryptoConfig{Key: key2}
|
|
err = trixContainer.DecryptPayload(config2)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestDecryptPayload_Ugly(t *testing.T) {
|
|
t.Run("MissingObfuscatorHeader", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: []byte("test"),
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
// Remove the obfuscator header
|
|
delete(trixContainer.Header, trix.HeaderKeyObfuscator)
|
|
|
|
// Should still work with default XOR obfuscator
|
|
err = trixContainer.DecryptPayload(config)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestNewEncryptedTrix_Good(t *testing.T) {
|
|
t.Run("Basic", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
payload := []byte("secret message")
|
|
header := map[string]interface{}{"custom": "value"}
|
|
|
|
trixContainer, err := trix.NewEncryptedTrix(payload, key, header)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, trixContainer.IsEncrypted())
|
|
assert.Equal(t, "value", trixContainer.Header["custom"])
|
|
assert.NotEqual(t, payload, trixContainer.Payload)
|
|
})
|
|
|
|
t.Run("WithNilHeader", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
payload := []byte("secret message")
|
|
|
|
trixContainer, err := trix.NewEncryptedTrix(payload, key, nil)
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, trixContainer.IsEncrypted())
|
|
assert.NotNil(t, trixContainer.Header)
|
|
})
|
|
}
|
|
|
|
func TestNewEncryptedTrix_Bad(t *testing.T) {
|
|
t.Run("InvalidKey", func(t *testing.T) {
|
|
_, err := trix.NewEncryptedTrix([]byte("test"), []byte("short"), nil)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestIsEncrypted(t *testing.T) {
|
|
t.Run("NilHeader", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{}
|
|
assert.False(t, trixContainer.IsEncrypted())
|
|
})
|
|
|
|
t.Run("MissingKey", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{Header: map[string]interface{}{}}
|
|
assert.False(t, trixContainer.IsEncrypted())
|
|
})
|
|
|
|
t.Run("FalseValue", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: false},
|
|
}
|
|
assert.False(t, trixContainer.IsEncrypted())
|
|
})
|
|
|
|
t.Run("TrueValue", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: true},
|
|
}
|
|
assert.True(t, trixContainer.IsEncrypted())
|
|
})
|
|
|
|
t.Run("WrongType", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyEncrypted: "true"},
|
|
}
|
|
assert.False(t, trixContainer.IsEncrypted())
|
|
})
|
|
}
|
|
|
|
func TestGetEncryptionAlgorithm(t *testing.T) {
|
|
t.Run("NilHeader", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{}
|
|
assert.Empty(t, trixContainer.GetEncryptionAlgorithm())
|
|
})
|
|
|
|
t.Run("MissingKey", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{Header: map[string]interface{}{}}
|
|
assert.Empty(t, trixContainer.GetEncryptionAlgorithm())
|
|
})
|
|
|
|
t.Run("ValidAlgorithm", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyAlgorithm: "test-algo"},
|
|
}
|
|
assert.Equal(t, "test-algo", trixContainer.GetEncryptionAlgorithm())
|
|
})
|
|
|
|
t.Run("WrongType", func(t *testing.T) {
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{trix.HeaderKeyAlgorithm: 123},
|
|
}
|
|
assert.Empty(t, trixContainer.GetEncryptionAlgorithm())
|
|
})
|
|
}
|
|
|
|
func TestEncryptedTrixRoundTrip(t *testing.T) {
|
|
t.Run("FullRoundTrip", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
for i := range key {
|
|
key[i] = byte(i * 3)
|
|
}
|
|
|
|
originalPayload := []byte("This is the original secret message that will be encrypted, stored, and decrypted.")
|
|
header := map[string]interface{}{
|
|
"content_type": "text/plain",
|
|
"custom_field": "custom_value",
|
|
}
|
|
|
|
// Create encrypted Trix
|
|
config := &trix.CryptoConfig{Key: key}
|
|
trixContainer := &trix.Trix{
|
|
Header: header,
|
|
Payload: originalPayload,
|
|
}
|
|
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
// Encode to binary format
|
|
encoded, err := trix.Encode(trixContainer, "ENCR", nil)
|
|
require.NoError(t, err)
|
|
|
|
// Decode from binary format
|
|
decoded, err := trix.Decode(encoded, "ENCR", nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify still encrypted after decode
|
|
assert.True(t, decoded.IsEncrypted())
|
|
|
|
// Decrypt
|
|
err = decoded.DecryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
// Verify payload matches original
|
|
assert.Equal(t, originalPayload, decoded.Payload)
|
|
assert.Equal(t, "custom_value", decoded.Header["custom_field"])
|
|
})
|
|
}
|
|
|
|
func TestNonceNotInHeader(t *testing.T) {
|
|
t.Run("NonceEmbeddedNotExposed", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: []byte("secret data"),
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
// Verify nonce is NOT in header
|
|
_, hasNonce := trixContainer.Header["nonce"]
|
|
assert.False(t, hasNonce)
|
|
|
|
// But the ciphertext contains the nonce (first 24 bytes)
|
|
assert.GreaterOrEqual(t, len(trixContainer.Payload), 24)
|
|
|
|
// Encode and decode
|
|
encoded, err := trix.Encode(trixContainer, "TEST", nil)
|
|
require.NoError(t, err)
|
|
|
|
decoded, err := trix.Decode(encoded, "TEST", nil)
|
|
require.NoError(t, err)
|
|
|
|
// Still no nonce in header after decode
|
|
_, hasNonce = decoded.Header["nonce"]
|
|
assert.False(t, hasNonce)
|
|
|
|
// But decryption still works (nonce is embedded in payload)
|
|
err = decoded.DecryptPayload(config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("secret data"), decoded.Payload)
|
|
})
|
|
}
|
|
|
|
func TestPlaintextNotExposed(t *testing.T) {
|
|
t.Run("CleartextNeverInCiphertext", func(t *testing.T) {
|
|
key := make([]byte, 32)
|
|
distinctivePayload := []byte("DISTINCTIVE_SECRET_PATTERN_THAT_SHOULD_NOT_APPEAR")
|
|
|
|
trixContainer := &trix.Trix{
|
|
Header: map[string]interface{}{},
|
|
Payload: distinctivePayload,
|
|
}
|
|
|
|
config := &trix.CryptoConfig{Key: key}
|
|
err := trixContainer.EncryptPayload(config)
|
|
require.NoError(t, err)
|
|
|
|
// The plaintext should not appear in the encrypted payload
|
|
assert.False(t, bytes.Contains(trixContainer.Payload, distinctivePayload))
|
|
assert.False(t, bytes.Contains(trixContainer.Payload, []byte("DISTINCTIVE")))
|
|
assert.False(t, bytes.Contains(trixContainer.Payload, []byte("SECRET")))
|
|
assert.False(t, bytes.Contains(trixContainer.Payload, []byte("PATTERN")))
|
|
})
|
|
}
|