Merge pull request #9 from Snider/generic-trix-format

Implement Generic .trix File Format
This commit is contained in:
Snider 2025-10-31 01:08:00 +00:00 committed by GitHub
commit e21c910f91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 260 additions and 0 deletions

30
docs/trix_format.md Normal file
View file

@ -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.

85
examples/main.go Normal file
View file

@ -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.")
}
}

113
trix/trix.go Normal file
View file

@ -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
}

32
trix/trix_test.go Normal file
View file

@ -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)
}