From 36884ecb5306578eb1f2df6e123abf77c66faaef 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 01:06:23 +0000 Subject: [PATCH] feat: Implement generic .trix file format This commit introduces a new, generic `.trix` file format, which is decoupled from any specific encryption algorithm. The format is defined in `docs/trix_format.md` and consists of a magic number, version, a flexible JSON header, and a raw data payload. A new `trix` Go package is implemented to handle the encoding and decoding of this format. Unit tests are included to verify the implementation. An example file, `examples/main.go`, is also added to demonstrate how to use the `.trix` container to store data encrypted with the `chachapoly` package, showcasing the intended decoupled design. --- docs/trix_format.md | 30 ++++++++++++ examples/main.go | 85 +++++++++++++++++++++++++++++++++ trix/trix.go | 113 ++++++++++++++++++++++++++++++++++++++++++++ trix/trix_test.go | 32 +++++++++++++ 4 files changed, 260 insertions(+) create mode 100644 docs/trix_format.md create mode 100644 examples/main.go create mode 100644 trix/trix.go create mode 100644 trix/trix_test.go diff --git a/docs/trix_format.md b/docs/trix_format.md new file mode 100644 index 0000000..d5c81af --- /dev/null +++ b/docs/trix_format.md @@ -0,0 +1,30 @@ +# .trix File Format v2.0 + +The `.trix` file format is a generic and flexible binary container for storing an arbitrary data payload alongside structured metadata. + +## Structure + +The file is structured as follows: + +| Field | Size (bytes) | Description | +|----------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Magic Number** | 4 | A constant value, `TRIX`, to identify the file as a `.trix` file. | +| **Version** | 1 | The version of the `.trix` file format (currently `2`). | +| **Header Length**| 4 | A 32-bit unsigned integer specifying the length of the JSON Header in bytes. This allows for flexible and extensible metadata. | +| **JSON Header** | `Header Length` | A UTF-8 encoded JSON object containing metadata about the payload. Common keys include `content_type`, `encryption_algorithm`, `nonce`, `tag`, and `created_at`. | +| **Payload** | variable | The raw binary data. This can be plaintext, ciphertext, or any other data. The interpretation of this data is guided by the metadata in the JSON Header. | + +## Example JSON Header + +Here is an example of what the JSON header might look like for a file encrypted with ChaCha20-Poly1305: + +```json +{ + "content_type": "application/octet-stream", + "encryption_algorithm": "chacha20poly1305", + "nonce": "AAECAwQFBgcICQoLDA0ODxAREhMUFRY=", + "created_at": "2025-10-30T12:00:00Z" +} +``` + +This decoupled design ensures that the `.trix` container is not tied to any specific encryption scheme, allowing for greater flexibility and future-proofing. diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..26c5a9c --- /dev/null +++ b/examples/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/base64" + "fmt" + "log" + "time" + + "github.com/Snider/Enchantrix/chachapoly" + "github.com/Snider/Enchantrix/trix" +) + +func main() { + // 1. Original plaintext + plaintext := []byte("This is a super secret message!") + key := make([]byte, 32) // In a real application, use a secure key + for i := range key { + key[i] = 1 + } + + // 2. Encrypt the data using the chachapoly package + // The ciphertext from chachapoly includes the nonce. + ciphertext, err := chachapoly.Encrypt(plaintext, key) + if err != nil { + log.Fatalf("Failed to encrypt: %v", err) + } + + // For the .trix header, we need to separate the nonce from the ciphertext. + // chacha20poly1305.NewX nonce size is 24 bytes. + nonce := ciphertext[:24] + actualCiphertext := ciphertext[24:] + + // 3. Create a .trix container for the encrypted data + header := map[string]interface{}{ + "content_type": "application/octet-stream", + "encryption_algorithm": "chacha20poly1305", + "nonce": base64.StdEncoding.EncodeToString(nonce), + "created_at": time.Now().UTC().Format(time.RFC3339), + } + + trixContainer := &trix.Trix{ + Header: header, + Payload: actualCiphertext, + } + + // 4. Encode the .trix container into its binary format + encodedTrix, err := trix.Encode(trixContainer) + if err != nil { + log.Fatalf("Failed to encode .trix container: %v", err) + } + + fmt.Println("Successfully created .trix container.") + + // 5. Decode the .trix container to retrieve the encrypted data + decodedTrix, err := trix.Decode(encodedTrix) + if err != nil { + log.Fatalf("Failed to decode .trix container: %v", err) + } + + // 6. Reassemble the ciphertext (nonce + payload) and decrypt + retrievedNonceStr, ok := decodedTrix.Header["nonce"].(string) + if !ok { + log.Fatalf("Nonce not found or not a string in header") + } + retrievedNonce, err := base64.StdEncoding.DecodeString(retrievedNonceStr) + if err != nil { + log.Fatalf("Failed to decode nonce: %v", err) + } + retrievedCiphertext := append(retrievedNonce, decodedTrix.Payload...) + + decrypted, err := chachapoly.Decrypt(retrievedCiphertext, key) + if err != nil { + log.Fatalf("Failed to decrypt: %v", err) + } + + // 7. Verify the result + fmt.Printf("Original plaintext: %s\n", plaintext) + fmt.Printf("Decrypted plaintext: %s\n", decrypted) + + if string(plaintext) == string(decrypted) { + fmt.Println("\nSuccess! The message was decrypted correctly.") + } else { + fmt.Println("\nFailure! The decrypted message does not match the original.") + } +} diff --git a/trix/trix.go b/trix/trix.go new file mode 100644 index 0000000..575b55e --- /dev/null +++ b/trix/trix.go @@ -0,0 +1,113 @@ +package trix + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "io" +) + +const ( + MagicNumber = "TRIX" + Version = 2 +) + +var ( + ErrInvalidMagicNumber = errors.New("trix: invalid magic number") + ErrInvalidVersion = errors.New("trix: invalid version") +) + +// Trix represents the structure of a .trix file. +type Trix struct { + Header map[string]interface{} + Payload []byte +} + +// Encode serializes a Trix struct into the .trix binary format. +func Encode(trix *Trix) ([]byte, error) { + headerBytes, err := json.Marshal(trix.Header) + if err != nil { + return nil, err + } + headerLength := uint32(len(headerBytes)) + + buf := new(bytes.Buffer) + + // Write Magic Number + if _, err := buf.WriteString(MagicNumber); err != nil { + return nil, err + } + + // Write Version + if err := buf.WriteByte(byte(Version)); err != nil { + return nil, err + } + + // Write Header Length + if err := binary.Write(buf, binary.BigEndian, headerLength); err != nil { + return nil, err + } + + // Write JSON Header + if _, err := buf.Write(headerBytes); err != nil { + return nil, err + } + + // Write Payload + if _, err := buf.Write(trix.Payload); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// Decode deserializes the .trix binary format into a Trix struct. +func Decode(data []byte) (*Trix, error) { + buf := bytes.NewReader(data) + + // Read and Verify Magic Number + magic := make([]byte, 4) + if _, err := io.ReadFull(buf, magic); err != nil { + return nil, err + } + if string(magic) != MagicNumber { + return nil, ErrInvalidMagicNumber + } + + // Read and Verify Version + version, err := buf.ReadByte() + if err != nil { + return nil, err + } + if version != Version { + return nil, ErrInvalidVersion + } + + // Read Header Length + var headerLength uint32 + if err := binary.Read(buf, binary.BigEndian, &headerLength); err != nil { + return nil, err + } + + // Read JSON Header + headerBytes := make([]byte, headerLength) + if _, err := io.ReadFull(buf, headerBytes); err != nil { + return nil, err + } + var header map[string]interface{} + if err := json.Unmarshal(headerBytes, &header); err != nil { + return nil, err + } + + // Read Payload + payload, err := io.ReadAll(buf) + if err != nil { + return nil, err + } + + return &Trix{ + Header: header, + Payload: payload, + }, nil +} diff --git a/trix/trix_test.go b/trix/trix_test.go new file mode 100644 index 0000000..d46695f --- /dev/null +++ b/trix/trix_test.go @@ -0,0 +1,32 @@ +package trix + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecode(t *testing.T) { + header := map[string]interface{}{ + "content_type": "application/octet-stream", + "encryption_algorithm": "chacha20poly1035", + "nonce": "AAECAwQFBgcICQoLDA0ODxAREhMUFRY=", + "created_at": "2025-10-30T12:00:00Z", + } + payload := []byte("This is a secret message.") + + trix := &Trix{ + Header: header, + Payload: payload, + } + + encoded, err := Encode(trix) + assert.NoError(t, err) + + decoded, err := Decode(encoded) + assert.NoError(t, err) + + assert.True(t, reflect.DeepEqual(trix.Header, decoded.Header)) + assert.Equal(t, trix.Payload, decoded.Payload) +}