Merge pull request #9 from Snider/generic-trix-format
Implement Generic .trix File Format
This commit is contained in:
commit
e21c910f91
4 changed files with 260 additions and 0 deletions
30
docs/trix_format.md
Normal file
30
docs/trix_format.md
Normal 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
85
examples/main.go
Normal 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
113
trix/trix.go
Normal 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
32
trix/trix_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue