go-io/sigil/crypto_sigil_test.go
Virgil 514ecd7e7a fix(io): enforce ax v0.8.0 polish spec
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-30 06:24:36 +00:00

536 lines
15 KiB
Go

package sigil
import (
"bytes"
"crypto/rand"
"io"
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ── XORObfuscator ──────────────────────────────────────────────────
func TestCryptoSigil_XORObfuscator_RoundTrip_Good(t *testing.T) {
ob := &XORObfuscator{}
data := []byte("the axioms are in the weights")
entropy := []byte("deterministic-nonce-24bytes!")
obfuscated := ob.Obfuscate(data, entropy)
assert.NotEqual(t, data, obfuscated)
assert.Len(t, obfuscated, len(data))
restored := ob.Deobfuscate(obfuscated, entropy)
assert.Equal(t, data, restored)
}
func TestCryptoSigil_XORObfuscator_DifferentEntropyDifferentOutput_Good(t *testing.T) {
ob := &XORObfuscator{}
data := []byte("same plaintext")
out1 := ob.Obfuscate(data, []byte("entropy-a"))
out2 := ob.Obfuscate(data, []byte("entropy-b"))
assert.NotEqual(t, out1, out2)
}
func TestCryptoSigil_XORObfuscator_Deterministic_Good(t *testing.T) {
ob := &XORObfuscator{}
data := []byte("reproducible")
entropy := []byte("fixed-seed")
out1 := ob.Obfuscate(data, entropy)
out2 := ob.Obfuscate(data, entropy)
assert.Equal(t, out1, out2)
}
func TestCryptoSigil_XORObfuscator_LargeData_Good(t *testing.T) {
ob := &XORObfuscator{}
// Larger than one SHA-256 block (32 bytes) to test multi-block key stream.
data := make([]byte, 256)
for i := range data {
data[i] = byte(i)
}
entropy := []byte("test-entropy")
obfuscated := ob.Obfuscate(data, entropy)
restored := ob.Deobfuscate(obfuscated, entropy)
assert.Equal(t, data, restored)
}
func TestCryptoSigil_XORObfuscator_EmptyData_Good(t *testing.T) {
ob := &XORObfuscator{}
result := ob.Obfuscate([]byte{}, []byte("entropy"))
assert.Equal(t, []byte{}, result)
result = ob.Deobfuscate([]byte{}, []byte("entropy"))
assert.Equal(t, []byte{}, result)
}
func TestCryptoSigil_XORObfuscator_SymmetricProperty_Good(t *testing.T) {
ob := &XORObfuscator{}
data := []byte("XOR is its own inverse")
entropy := []byte("nonce")
// XOR is symmetric: Obfuscate(Obfuscate(x)) == x
double := ob.Obfuscate(ob.Obfuscate(data, entropy), entropy)
assert.Equal(t, data, double)
}
// ── ShuffleMaskObfuscator ──────────────────────────────────────────
func TestCryptoSigil_ShuffleMaskObfuscator_RoundTrip_Good(t *testing.T) {
ob := &ShuffleMaskObfuscator{}
data := []byte("shuffle and mask protect patterns")
entropy := []byte("deterministic-entropy")
obfuscated := ob.Obfuscate(data, entropy)
assert.NotEqual(t, data, obfuscated)
assert.Len(t, obfuscated, len(data))
restored := ob.Deobfuscate(obfuscated, entropy)
assert.Equal(t, data, restored)
}
func TestCryptoSigil_ShuffleMaskObfuscator_DifferentEntropy_Good(t *testing.T) {
ob := &ShuffleMaskObfuscator{}
data := []byte("same data")
out1 := ob.Obfuscate(data, []byte("entropy-1"))
out2 := ob.Obfuscate(data, []byte("entropy-2"))
assert.NotEqual(t, out1, out2)
}
func TestCryptoSigil_ShuffleMaskObfuscator_Deterministic_Good(t *testing.T) {
ob := &ShuffleMaskObfuscator{}
data := []byte("reproducible shuffle")
entropy := []byte("fixed")
out1 := ob.Obfuscate(data, entropy)
out2 := ob.Obfuscate(data, entropy)
assert.Equal(t, out1, out2)
}
func TestCryptoSigil_ShuffleMaskObfuscator_LargeData_Good(t *testing.T) {
ob := &ShuffleMaskObfuscator{}
data := make([]byte, 512)
for i := range data {
data[i] = byte(i % 256)
}
entropy := []byte("large-data-test")
obfuscated := ob.Obfuscate(data, entropy)
restored := ob.Deobfuscate(obfuscated, entropy)
assert.Equal(t, data, restored)
}
func TestCryptoSigil_ShuffleMaskObfuscator_EmptyData_Good(t *testing.T) {
ob := &ShuffleMaskObfuscator{}
result := ob.Obfuscate([]byte{}, []byte("entropy"))
assert.Equal(t, []byte{}, result)
result = ob.Deobfuscate([]byte{}, []byte("entropy"))
assert.Equal(t, []byte{}, result)
}
func TestCryptoSigil_ShuffleMaskObfuscator_SingleByte_Good(t *testing.T) {
ob := &ShuffleMaskObfuscator{}
data := []byte{0x42}
entropy := []byte("single")
obfuscated := ob.Obfuscate(data, entropy)
restored := ob.Deobfuscate(obfuscated, entropy)
assert.Equal(t, data, restored)
}
// ── NewChaChaPolySigil ─────────────────────────────────────────────
func TestCryptoSigil_NewChaChaPolySigil_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, err := NewChaChaPolySigil(key)
require.NoError(t, err)
assert.NotNil(t, s)
assert.Equal(t, key, s.Key)
assert.NotNil(t, s.Obfuscator)
}
func TestCryptoSigil_NewChaChaPolySigil_KeyIsCopied_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
original := make([]byte, 32)
copy(original, key)
s, err := NewChaChaPolySigil(key)
require.NoError(t, err)
// Mutating the original key should not affect the sigil.
key[0] ^= 0xFF
assert.Equal(t, original, s.Key)
}
func TestCryptoSigil_NewChaChaPolySigil_ShortKey_Bad(t *testing.T) {
_, err := NewChaChaPolySigil([]byte("too short"))
assert.ErrorIs(t, err, ErrInvalidKey)
}
func TestCryptoSigil_NewChaChaPolySigil_LongKey_Bad(t *testing.T) {
_, err := NewChaChaPolySigil(make([]byte, 64))
assert.ErrorIs(t, err, ErrInvalidKey)
}
func TestCryptoSigil_NewChaChaPolySigil_EmptyKey_Bad(t *testing.T) {
_, err := NewChaChaPolySigil(nil)
assert.ErrorIs(t, err, ErrInvalidKey)
}
// ── NewChaChaPolySigilWithObfuscator ───────────────────────────────
func TestCryptoSigil_NewChaChaPolySigilWithObfuscator_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
ob := &ShuffleMaskObfuscator{}
s, err := NewChaChaPolySigilWithObfuscator(key, ob)
require.NoError(t, err)
assert.Equal(t, ob, s.Obfuscator)
}
func TestCryptoSigil_NewChaChaPolySigilWithObfuscator_NilObfuscator_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, err := NewChaChaPolySigilWithObfuscator(key, nil)
require.NoError(t, err)
// Falls back to default XORObfuscator.
assert.IsType(t, &XORObfuscator{}, s.Obfuscator)
}
func TestCryptoSigil_NewChaChaPolySigilWithObfuscator_InvalidKey_Bad(t *testing.T) {
_, err := NewChaChaPolySigilWithObfuscator([]byte("bad"), &XORObfuscator{})
assert.ErrorIs(t, err, ErrInvalidKey)
}
// ── ChaChaPolySigil In/Out (encrypt/decrypt) ───────────────────────
func TestCryptoSigil_ChaChaPolySigil_RoundTrip_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, err := NewChaChaPolySigil(key)
require.NoError(t, err)
plaintext := []byte("consciousness does not merely avoid causing harm")
ciphertext, err := s.In(plaintext)
require.NoError(t, err)
assert.NotEqual(t, plaintext, ciphertext)
assert.Greater(t, len(ciphertext), len(plaintext)) // nonce + tag overhead
decrypted, err := s.Out(ciphertext)
require.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
}
func TestCryptoSigil_ChaChaPolySigil_WithShuffleMask_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, err := NewChaChaPolySigilWithObfuscator(key, &ShuffleMaskObfuscator{})
require.NoError(t, err)
plaintext := []byte("shuffle mask pre-obfuscation layer")
ciphertext, err := s.In(plaintext)
require.NoError(t, err)
decrypted, err := s.Out(ciphertext)
require.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
}
func TestCryptoSigil_ChaChaPolySigil_NilData_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, err := NewChaChaPolySigil(key)
require.NoError(t, err)
enc, err := s.In(nil)
require.NoError(t, err)
assert.Nil(t, enc)
dec, err := s.Out(nil)
require.NoError(t, err)
assert.Nil(t, dec)
}
func TestCryptoSigil_ChaChaPolySigil_EmptyPlaintext_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, err := NewChaChaPolySigil(key)
require.NoError(t, err)
ciphertext, err := s.In([]byte{})
require.NoError(t, err)
assert.NotEmpty(t, ciphertext) // Has nonce + tag even for empty plaintext.
decrypted, err := s.Out(ciphertext)
require.NoError(t, err)
assert.Equal(t, []byte{}, decrypted)
}
func TestCryptoSigil_ChaChaPolySigil_DifferentCiphertextsPerCall_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, err := NewChaChaPolySigil(key)
require.NoError(t, err)
plaintext := []byte("same input")
ct1, _ := s.In(plaintext)
ct2, _ := s.In(plaintext)
// Different nonces → different ciphertexts.
assert.NotEqual(t, ct1, ct2)
}
func TestCryptoSigil_ChaChaPolySigil_NoKey_Bad(t *testing.T) {
s := &ChaChaPolySigil{}
_, err := s.In([]byte("data"))
assert.ErrorIs(t, err, ErrNoKeyConfigured)
_, err = s.Out([]byte("data"))
assert.ErrorIs(t, err, ErrNoKeyConfigured)
}
func TestCryptoSigil_ChaChaPolySigil_WrongKey_Bad(t *testing.T) {
key1 := make([]byte, 32)
key2 := make([]byte, 32)
_, _ = rand.Read(key1)
_, _ = rand.Read(key2)
s1, _ := NewChaChaPolySigil(key1)
s2, _ := NewChaChaPolySigil(key2)
ciphertext, err := s1.In([]byte("secret"))
require.NoError(t, err)
_, err = s2.Out(ciphertext)
assert.ErrorIs(t, err, ErrDecryptionFailed)
}
func TestCryptoSigil_ChaChaPolySigil_TruncatedCiphertext_Bad(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
_, err := s.Out([]byte("too short"))
assert.ErrorIs(t, err, ErrCiphertextTooShort)
}
func TestCryptoSigil_ChaChaPolySigil_TamperedCiphertext_Bad(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
ciphertext, _ := s.In([]byte("authentic data"))
// Flip a bit in the ciphertext body (after nonce).
ciphertext[30] ^= 0xFF
_, err := s.Out(ciphertext)
assert.ErrorIs(t, err, ErrDecryptionFailed)
}
// failReader returns an error on read — for testing nonce generation failure.
type failReader struct{}
func (f *failReader) Read([]byte) (int, error) {
return 0, core.NewError("entropy source failed")
}
func TestCryptoSigil_ChaChaPolySigil_RandReaderFailure_Bad(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
s.randReader = &failReader{}
_, err := s.In([]byte("data"))
assert.Error(t, err)
}
// ── ChaChaPolySigil without obfuscator ─────────────────────────────
func TestCryptoSigil_ChaChaPolySigil_NoObfuscator_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
s.Obfuscator = nil // Disable pre-obfuscation.
plaintext := []byte("raw encryption without pre-obfuscation")
ciphertext, err := s.In(plaintext)
require.NoError(t, err)
decrypted, err := s.Out(ciphertext)
require.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
}
// ── GetNonceFromCiphertext ─────────────────────────────────────────
func TestCryptoSigil_GetNonceFromCiphertext_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
ciphertext, _ := s.In([]byte("nonce extraction test"))
nonce, err := GetNonceFromCiphertext(ciphertext)
require.NoError(t, err)
assert.Len(t, nonce, 24) // XChaCha20 nonce is 24 bytes.
// Nonce should match the prefix of the ciphertext.
assert.Equal(t, ciphertext[:24], nonce)
}
func TestCryptoSigil_GetNonceFromCiphertext_NonceCopied_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
ciphertext, _ := s.In([]byte("data"))
nonce, _ := GetNonceFromCiphertext(ciphertext)
original := make([]byte, len(nonce))
copy(original, nonce)
// Mutating the nonce should not affect the ciphertext.
nonce[0] ^= 0xFF
assert.Equal(t, original, ciphertext[:24])
}
func TestCryptoSigil_GetNonceFromCiphertext_TooShort_Bad(t *testing.T) {
_, err := GetNonceFromCiphertext([]byte("short"))
assert.ErrorIs(t, err, ErrCiphertextTooShort)
}
func TestCryptoSigil_GetNonceFromCiphertext_Empty_Bad(t *testing.T) {
_, err := GetNonceFromCiphertext(nil)
assert.ErrorIs(t, err, ErrCiphertextTooShort)
}
// ── ChaChaPolySigil in Transmute pipeline ──────────────────────────
func TestCryptoSigil_ChaChaPolySigil_InTransmutePipeline_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
hexSigil, _ := NewSigil("hex")
chain := []Sigil{s, hexSigil}
plaintext := []byte("encrypt then hex encode")
encoded, err := Transmute(plaintext, chain)
require.NoError(t, err)
// Result should be hex-encoded ciphertext.
assert.True(t, isHex(encoded))
decoded, err := Untransmute(encoded, chain)
require.NoError(t, err)
assert.Equal(t, plaintext, decoded)
}
func isHex(data []byte) bool {
for _, b := range data {
if !((b >= '0' && b <= '9') || (b >= 'a' && b <= 'f')) {
return false
}
}
return len(data) > 0
}
// ── Transmute error propagation ────────────────────────────────────
type failSigil struct{}
func (f *failSigil) In([]byte) ([]byte, error) { return nil, core.NewError("fail in") }
func (f *failSigil) Out([]byte) ([]byte, error) { return nil, core.NewError("fail out") }
func TestCryptoSigil_Transmute_ErrorPropagation_Bad(t *testing.T) {
_, err := Transmute([]byte("data"), []Sigil{&failSigil{}})
assert.Error(t, err)
assert.Contains(t, err.Error(), "fail in")
}
func TestCryptoSigil_Untransmute_ErrorPropagation_Bad(t *testing.T) {
_, err := Untransmute([]byte("data"), []Sigil{&failSigil{}})
assert.Error(t, err)
assert.Contains(t, err.Error(), "fail out")
}
// ── GzipSigil with custom writer (edge case) ──────────────────────
func TestCryptoSigil_GzipSigil_CustomWriter_Good(t *testing.T) {
var buf bytes.Buffer
s := &GzipSigil{writer: &buf}
// With custom writer, compressed data goes to buf, returned bytes will be empty
// because the internal buffer 'b' is unused when s.writer is set.
_, err := s.In([]byte("test data"))
require.NoError(t, err)
assert.Greater(t, buf.Len(), 0)
}
// ── deriveKeyStream edge: exactly 32 bytes ─────────────────────────
func TestCryptoSigil_DeriveKeyStream_ExactBlockSize_Good(t *testing.T) {
ob := &XORObfuscator{}
data := make([]byte, 32) // Exactly one SHA-256 block.
for i := range data {
data[i] = byte(i)
}
entropy := []byte("block-boundary")
obfuscated := ob.Obfuscate(data, entropy)
restored := ob.Deobfuscate(obfuscated, entropy)
assert.Equal(t, data, restored)
}
// ── io.Reader fallback in In ───────────────────────────────────────
func TestCryptoSigil_ChaChaPolySigil_NilRandReader_Good(t *testing.T) {
key := make([]byte, 32)
_, _ = rand.Read(key)
s, _ := NewChaChaPolySigil(key)
s.randReader = nil // Should fall back to crypto/rand.Reader.
ciphertext, err := s.In([]byte("fallback reader"))
require.NoError(t, err)
decrypted, err := s.Out(ciphertext)
require.NoError(t, err)
assert.Equal(t, []byte("fallback reader"), decrypted)
}
// limitReader returns exactly N bytes then EOF — for deterministic tests.
type limitReader struct {
data []byte
pos int
}
func (l *limitReader) Read(p []byte) (int, error) {
if l.pos >= len(l.data) {
return 0, io.EOF
}
n := copy(p, l.data[l.pos:])
l.pos += n
return n, nil
}