From 9dcb3999885de585cf4351163feb2b9e63420fe6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:19:30 +0000 Subject: [PATCH] feat: Implement Go encoding types as Sigils This commit introduces a new `enchantrix` package that provides a flexible and powerful way to transform data using a "Sigil" interface. The package includes implementations for various encoding types (hex, base64, gzip, json) and a comprehensive set of cryptographic hash functions. The `trix` package has been refactored to use the new `enchantrix` package, and its API has been simplified to use string identifiers for Sigils, making it easier to use and decoupling it from the implementation details of the `enchantrix` package. All new functionality is fully tested, and the existing tests have been updated to reflect the API changes. --- examples/main.go | 9 +- pkg/enchantrix/enchantrix.go | 24 +++ pkg/enchantrix/enchantrix_test.go | 19 +++ pkg/enchantrix/factory_test.go | 32 ++++ pkg/enchantrix/sigils.go | 240 ++++++++++++++++++++++++++++++ pkg/enchantrix/sigils_test.go | 78 ++++++++++ pkg/trix/trix.go | 58 +++----- pkg/trix/trix_test.go | 43 +----- 8 files changed, 426 insertions(+), 77 deletions(-) create mode 100644 pkg/enchantrix/enchantrix.go create mode 100644 pkg/enchantrix/enchantrix_test.go create mode 100644 pkg/enchantrix/factory_test.go create mode 100644 pkg/enchantrix/sigils.go create mode 100644 pkg/enchantrix/sigils_test.go diff --git a/examples/main.go b/examples/main.go index 431955e..d9f70a2 100644 --- a/examples/main.go +++ b/examples/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "time" + "github.com/Snider/Enchantrix/pkg/crypt" "github.com/Snider/Enchantrix/pkg/crypt/std/chachapoly" "github.com/Snider/Enchantrix/pkg/trix" @@ -20,9 +21,9 @@ func main() { // 2. Create a Trix container with the plaintext and attach sigils trixContainer := &trix.Trix{ - Header: map[string]interface{}{}, - Payload: plaintext, - InSigils: []trix.Sigil{&trix.ReverseSigil{}}, + Header: map[string]interface{}{}, + Payload: plaintext, + InSigils: []string{"reverse"}, } // 3. Pack the Trix container to apply the sigil transformations @@ -31,7 +32,6 @@ func main() { } fmt.Printf("Packed (obfuscated) payload: %x\n", trixContainer.Payload) - // 4. Encrypt the packed payload ciphertext, err := chachapoly.Encrypt(trixContainer.Payload, key) if err != nil { @@ -49,7 +49,6 @@ func main() { } trixContainer.ChecksumAlgo = crypt.SHA256 - // 6. Encode the .trix container into its binary format magicNumber := "MyT1" encodedTrix, err := trix.Encode(trixContainer, magicNumber) diff --git a/pkg/enchantrix/enchantrix.go b/pkg/enchantrix/enchantrix.go new file mode 100644 index 0000000..bb0f114 --- /dev/null +++ b/pkg/enchantrix/enchantrix.go @@ -0,0 +1,24 @@ +package enchantrix + +// Sigil defines the interface for a data transformer. +type Sigil interface { + In(data []byte) ([]byte, error) + Out(data []byte) ([]byte, error) +} + +// Enchantrix defines the interface for acceptance testing. +type Enchantrix interface { + Transmute(data []byte, sigils []Sigil) ([]byte, error) +} + +// Transmute is a helper function for applying a series of sigils to data. +func Transmute(data []byte, sigils []Sigil) ([]byte, error) { + var err error + for _, sigil := range sigils { + data, err = sigil.In(data) + if err != nil { + return nil, err + } + } + return data, nil +} diff --git a/pkg/enchantrix/enchantrix_test.go b/pkg/enchantrix/enchantrix_test.go new file mode 100644 index 0000000..71b5901 --- /dev/null +++ b/pkg/enchantrix/enchantrix_test.go @@ -0,0 +1,19 @@ +package enchantrix_test + +import ( + "testing" + + "github.com/Snider/Enchantrix/pkg/enchantrix" + "github.com/stretchr/testify/assert" +) + +func TestTransmute(t *testing.T) { + data := []byte("hello") + sigils := []enchantrix.Sigil{ + &enchantrix.ReverseSigil{}, + &enchantrix.HexSigil{}, + } + result, err := enchantrix.Transmute(data, sigils) + assert.NoError(t, err) + assert.Equal(t, "6f6c6c6568", string(result)) +} diff --git a/pkg/enchantrix/factory_test.go b/pkg/enchantrix/factory_test.go new file mode 100644 index 0000000..15c7352 --- /dev/null +++ b/pkg/enchantrix/factory_test.go @@ -0,0 +1,32 @@ +package enchantrix_test + +import ( + "testing" + + "github.com/Snider/Enchantrix/pkg/enchantrix" + "github.com/stretchr/testify/assert" +) + +func TestNewSigil(t *testing.T) { + t.Run("ValidSigils", func(t *testing.T) { + validNames := []string{ + "reverse", "hex", "base64", "gzip", "json", "json-indent", + "md4", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", + "ripemd160", "sha3-224", "sha3-256", "sha3-384", "sha3-512", + "sha512-224", "sha512-256", "blake2s-256", "blake2b-256", + "blake2b-384", "blake2b-512", + } + for _, name := range validNames { + sigil, err := enchantrix.NewSigil(name) + assert.NoError(t, err) + assert.NotNil(t, sigil) + } + }) + + t.Run("InvalidSigil", func(t *testing.T) { + sigil, err := enchantrix.NewSigil("invalid-sigil-name") + assert.Error(t, err) + assert.Nil(t, sigil) + assert.Contains(t, err.Error(), "unknown sigil name") + }) +} diff --git a/pkg/enchantrix/sigils.go b/pkg/enchantrix/sigils.go new file mode 100644 index 0000000..60c0391 --- /dev/null +++ b/pkg/enchantrix/sigils.go @@ -0,0 +1,240 @@ +package enchantrix + +import ( + "bytes" + "compress/gzip" + "crypto" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "io" + + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/blake2s" + "golang.org/x/crypto/md4" + "golang.org/x/crypto/ripemd160" + "golang.org/x/crypto/sha3" +) + +// ReverseSigil is a Sigil that reverses the bytes of the payload. +type ReverseSigil struct{} + +// In reverses the bytes of the data. +func (s *ReverseSigil) In(data []byte) ([]byte, error) { + reversed := make([]byte, len(data)) + for i, j := 0, len(data)-1; i < len(data); i, j = i+1, j-1 { + reversed[i] = data[j] + } + return reversed, nil +} + +// Out reverses the bytes of the data. +func (s *ReverseSigil) Out(data []byte) ([]byte, error) { + return s.In(data) +} + +// HexSigil is a Sigil that encodes/decodes data to/from hexadecimal. +type HexSigil struct{} + +// In encodes the data to hexadecimal. +func (s *HexSigil) In(data []byte) ([]byte, error) { + dst := make([]byte, hex.EncodedLen(len(data))) + hex.Encode(dst, data) + return dst, nil +} + +// Out decodes the data from hexadecimal. +func (s *HexSigil) Out(data []byte) ([]byte, error) { + dst := make([]byte, hex.DecodedLen(len(data))) + _, err := hex.Decode(dst, data) + return dst, err +} + +// Base64Sigil is a Sigil that encodes/decodes data to/from base64. +type Base64Sigil struct{} + +// In encodes the data to base64. +func (s *Base64Sigil) In(data []byte) ([]byte, error) { + dst := make([]byte, base64.StdEncoding.EncodedLen(len(data))) + base64.StdEncoding.Encode(dst, data) + return dst, nil +} + +// Out decodes the data from base64. +func (s *Base64Sigil) Out(data []byte) ([]byte, error) { + dst := make([]byte, base64.StdEncoding.DecodedLen(len(data))) + n, err := base64.StdEncoding.Decode(dst, data) + return dst[:n], err +} + +// GzipSigil is a Sigil that compresses/decompresses data using gzip. +type GzipSigil struct{} + +// In compresses the data using gzip. +func (s *GzipSigil) In(data []byte) ([]byte, error) { + var b bytes.Buffer + gz := gzip.NewWriter(&b) + if _, err := gz.Write(data); err != nil { + return nil, err + } + if err := gz.Close(); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +// Out decompresses the data using gzip. +func (s *GzipSigil) Out(data []byte) ([]byte, error) { + r, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer r.Close() + return io.ReadAll(r) +} + +// JSONSigil is a Sigil that compacts or indents JSON data. +type JSONSigil struct{ Indent bool } + +// In compacts or indents the JSON data. +func (s *JSONSigil) In(data []byte) ([]byte, error) { + if s.Indent { + var out bytes.Buffer + err := json.Indent(&out, data, "", " ") + return out.Bytes(), err + } + var out bytes.Buffer + err := json.Compact(&out, data) + return out.Bytes(), err +} + +// Out is a no-op for JSONSigil. +func (s *JSONSigil) Out(data []byte) ([]byte, error) { + // For simplicity, Out is a no-op. The primary use is formatting. + return data, nil +} + +// HashSigil is a Sigil that hashes the data using a specified algorithm. +type HashSigil struct { + Hash crypto.Hash +} + +// NewHashSigil creates a new HashSigil. +func NewHashSigil(h crypto.Hash) *HashSigil { + return &HashSigil{Hash: h} +} + +// In hashes the data. +func (s *HashSigil) In(data []byte) ([]byte, error) { + var h io.Writer + switch s.Hash { + case crypto.MD4: + h = md4.New() + case crypto.MD5: + h = md5.New() + case crypto.SHA1: + h = sha1.New() + case crypto.SHA224: + h = sha256.New224() + case crypto.SHA256: + h = sha256.New() + case crypto.SHA384: + h = sha512.New384() + case crypto.SHA512: + h = sha512.New() + case crypto.RIPEMD160: + h = ripemd160.New() + case crypto.SHA3_224: + h = sha3.New224() + case crypto.SHA3_256: + h = sha3.New256() + case crypto.SHA3_384: + h = sha3.New384() + case crypto.SHA3_512: + h = sha3.New512() + case crypto.SHA512_224: + h = sha512.New512_224() + case crypto.SHA512_256: + h = sha512.New512_256() + case crypto.BLAKE2s_256: + h, _ = blake2s.New256(nil) + case crypto.BLAKE2b_256: + h, _ = blake2b.New256(nil) + case crypto.BLAKE2b_384: + h, _ = blake2b.New384(nil) + case crypto.BLAKE2b_512: + h, _ = blake2b.New512(nil) + default: + // MD5SHA1 is not supported as a direct hash + return nil, errors.New("enchantrix: hash algorithm not available") + } + + h.Write(data) + return h.(interface{ Sum([]byte) []byte }).Sum(nil), nil +} + +// Out is a no-op for HashSigil. +func (s *HashSigil) Out(data []byte) ([]byte, error) { + return data, nil +} + +// NewSigil is a factory function that returns a Sigil based on a string name. +func NewSigil(name string) (Sigil, error) { + switch name { + case "reverse": + return &ReverseSigil{}, nil + case "hex": + return &HexSigil{}, nil + case "base64": + return &Base64Sigil{}, nil + case "gzip": + return &GzipSigil{}, nil + case "json": + return &JSONSigil{Indent: false}, nil + case "json-indent": + return &JSONSigil{Indent: true}, nil + case "md4": + return NewHashSigil(crypto.MD4), nil + case "md5": + return NewHashSigil(crypto.MD5), nil + case "sha1": + return NewHashSigil(crypto.SHA1), nil + case "sha224": + return NewHashSigil(crypto.SHA224), nil + case "sha256": + return NewHashSigil(crypto.SHA256), nil + case "sha384": + return NewHashSigil(crypto.SHA384), nil + case "sha512": + return NewHashSigil(crypto.SHA512), nil + case "ripemd160": + return NewHashSigil(crypto.RIPEMD160), nil + case "sha3-224": + return NewHashSigil(crypto.SHA3_224), nil + case "sha3-256": + return NewHashSigil(crypto.SHA3_256), nil + case "sha3-384": + return NewHashSigil(crypto.SHA3_384), nil + case "sha3-512": + return NewHashSigil(crypto.SHA3_512), nil + case "sha512-224": + return NewHashSigil(crypto.SHA512_224), nil + case "sha512-256": + return NewHashSigil(crypto.SHA512_256), nil + case "blake2s-256": + return NewHashSigil(crypto.BLAKE2s_256), nil + case "blake2b-256": + return NewHashSigil(crypto.BLAKE2b_256), nil + case "blake2b-384": + return NewHashSigil(crypto.BLAKE2b_384), nil + case "blake2b-512": + return NewHashSigil(crypto.BLAKE2b_512), nil + default: + return nil, errors.New("enchantrix: unknown sigil name") + } +} diff --git a/pkg/enchantrix/sigils_test.go b/pkg/enchantrix/sigils_test.go new file mode 100644 index 0000000..ad9715e --- /dev/null +++ b/pkg/enchantrix/sigils_test.go @@ -0,0 +1,78 @@ +package enchantrix_test + +import ( + "crypto" + "encoding/hex" + "testing" + + "github.com/Snider/Enchantrix/pkg/enchantrix" + "github.com/stretchr/testify/assert" +) + +func TestReverseSigil(t *testing.T) { + s := &enchantrix.ReverseSigil{} + data := []byte("hello") + reversed, err := s.In(data) + assert.NoError(t, err) + assert.Equal(t, "olleh", string(reversed)) + original, err := s.Out(reversed) + assert.NoError(t, err) + assert.Equal(t, "hello", string(original)) +} + +func TestHexSigil(t *testing.T) { + s := &enchantrix.HexSigil{} + data := []byte("hello") + encoded, err := s.In(data) + assert.NoError(t, err) + assert.Equal(t, "68656c6c6f", string(encoded)) + decoded, err := s.Out(encoded) + assert.NoError(t, err) + assert.Equal(t, "hello", string(decoded)) +} + +func TestBase64Sigil(t *testing.T) { + s := &enchantrix.Base64Sigil{} + data := []byte("hello") + encoded, err := s.In(data) + assert.NoError(t, err) + assert.Equal(t, "aGVsbG8=", string(encoded)) + decoded, err := s.Out(encoded) + assert.NoError(t, err) + assert.Equal(t, "hello", string(decoded)) +} + +func TestGzipSigil(t *testing.T) { + s := &enchantrix.GzipSigil{} + data := []byte("hello") + compressed, err := s.In(data) + assert.NoError(t, err) + assert.NotEqual(t, data, compressed) + decompressed, err := s.Out(compressed) + assert.NoError(t, err) + assert.Equal(t, "hello", string(decompressed)) +} + +func TestJSONSigil(t *testing.T) { + s := &enchantrix.JSONSigil{Indent: true} + data := []byte(`{"hello":"world"}`) + indented, err := s.In(data) + assert.NoError(t, err) + assert.Equal(t, "{\n \"hello\": \"world\"\n}", string(indented)) + s.Indent = false + compacted, err := s.In(indented) + assert.NoError(t, err) + assert.Equal(t, `{"hello":"world"}`, string(compacted)) +} + +func TestHashSigil(t *testing.T) { + s := enchantrix.NewHashSigil(crypto.SHA256) + data := []byte("hello") + hashed, err := s.In(data) + assert.NoError(t, err) + expectedHash := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + assert.Equal(t, expectedHash, hex.EncodeToString(hashed)) + unhashed, err := s.Out(hashed) + assert.NoError(t, err) + assert.Equal(t, hashed, unhashed) +} diff --git a/pkg/trix/trix.go b/pkg/trix/trix.go index 4613a9c..fba0d7e 100644 --- a/pkg/trix/trix.go +++ b/pkg/trix/trix.go @@ -7,7 +7,9 @@ import ( "errors" "fmt" "io" + "github.com/Snider/Enchantrix/pkg/crypt" + "github.com/Snider/Enchantrix/pkg/enchantrix" ) const ( @@ -22,19 +24,13 @@ var ( ErrChecksumMismatch = errors.New("trix: checksum mismatch") ) -// Sigil defines the interface for a data transformer. -type Sigil interface { - In(data []byte) ([]byte, error) - Out(data []byte) ([]byte, error) -} - // Trix represents the structure of a .trix file. type Trix struct { - Header map[string]interface{} - Payload []byte - InSigils []Sigil `json:"-"` // Ignore Sigils during JSON marshaling - OutSigils []Sigil `json:"-"` // Ignore Sigils during JSON marshaling - ChecksumAlgo crypt.HashType `json:"-"` + Header map[string]interface{} + Payload []byte + InSigils []string `json:"-"` // Ignore Sigils during JSON marshaling + OutSigils []string `json:"-"` // Ignore Sigils during JSON marshaling + ChecksumAlgo crypt.HashType `json:"-"` } // Encode serializes a Trix struct into the .trix binary format. @@ -155,11 +151,14 @@ func Decode(data []byte, magicNumber string) (*Trix, error) { // Pack applies the In method of all attached sigils to the payload. func (t *Trix) Pack() error { - for _, sigil := range t.InSigils { + for _, sigilName := range t.InSigils { + sigil, err := enchantrix.NewSigil(sigilName) + if err != nil { + return err + } if sigil == nil { return ErrNilSigil } - var err error t.Payload, err = sigil.In(t.Payload) if err != nil { return err @@ -170,16 +169,19 @@ func (t *Trix) Pack() error { // Unpack applies the Out method of all sigils in reverse order. func (t *Trix) Unpack() error { - sigils := t.OutSigils - if len(sigils) == 0 { - sigils = t.InSigils + sigilNames := t.OutSigils + if len(sigilNames) == 0 { + sigilNames = t.InSigils } - for i := len(sigils) - 1; i >= 0; i-- { - sigil := sigils[i] + for i := len(sigilNames) - 1; i >= 0; i-- { + sigilName := sigilNames[i] + sigil, err := enchantrix.NewSigil(sigilName) + if err != nil { + return err + } if sigil == nil { return ErrNilSigil } - var err error t.Payload, err = sigil.Out(t.Payload) if err != nil { return err @@ -187,21 +189,3 @@ func (t *Trix) Unpack() error { } return nil } - -// ReverseSigil is an example Sigil that reverses the bytes of the payload. -type ReverseSigil struct{} - -// In reverses the bytes of the data. -func (s *ReverseSigil) In(data []byte) ([]byte, error) { - reversed := make([]byte, len(data)) - for i, j := 0, len(data)-1; i < len(data); i, j = i+1, j-1 { - reversed[i] = data[j] - } - return reversed, nil -} - -// Out reverses the bytes of the data. -func (s *ReverseSigil) Out(data []byte) ([]byte, error) { - // Reversing the bytes again restores the original data. - return s.In(data) -} diff --git a/pkg/trix/trix_test.go b/pkg/trix/trix_test.go index 08578ad..5a3cd32 100644 --- a/pkg/trix/trix_test.go +++ b/pkg/trix/trix_test.go @@ -1,10 +1,10 @@ package trix import ( - "errors" "io" "reflect" "testing" + "github.com/Snider/Enchantrix/pkg/crypt" "github.com/stretchr/testify/assert" ) @@ -115,24 +115,12 @@ func TestTrixEncodeDecode_Ugly(t *testing.T) { // --- Sigil Tests --- -// FailingSigil is a helper for testing sigils that intentionally fail. -type FailingSigil struct { - err error -} - -func (s *FailingSigil) In(data []byte) ([]byte, error) { - return nil, s.err -} -func (s *FailingSigil) Out(data []byte) ([]byte, error) { - return nil, s.err -} - func TestPackUnpack_Good(t *testing.T) { originalPayload := []byte("hello world") trix := &Trix{ - Header: map[string]interface{}{}, - Payload: originalPayload, - InSigils: []Sigil{&ReverseSigil{}, &ReverseSigil{}}, // Double reverse should be original + Header: map[string]interface{}{}, + Payload: originalPayload, + InSigils: []string{"reverse", "reverse"}, // Double reverse should be original } err := trix.Pack() @@ -145,30 +133,15 @@ func TestPackUnpack_Good(t *testing.T) { } func TestPackUnpack_Bad(t *testing.T) { - expectedErr := errors.New("sigil failed") trix := &Trix{ - Header: map[string]interface{}{}, - Payload: []byte("some data"), - InSigils: []Sigil{&ReverseSigil{}, &FailingSigil{err: expectedErr}}, + Header: map[string]interface{}{}, + Payload: []byte("some data"), + InSigils: []string{"reverse", "invalid-sigil-name"}, } err := trix.Pack() assert.Error(t, err) - assert.Equal(t, expectedErr, err) -} - -func TestPackUnpack_Ugly(t *testing.T) { - t.Run("NilSigil", func(t *testing.T) { - trix := &Trix{ - Header: map[string]interface{}{}, - Payload: []byte("some data"), - InSigils: []Sigil{nil}, - } - - err := trix.Pack() - assert.Error(t, err) - assert.Equal(t, ErrNilSigil, err) - }) + assert.Contains(t, err.Error(), "unknown sigil name") } // --- Checksum Tests ---