Borg/rfc/RFC-006-TRIX.md

8.7 KiB

RFC-006: TRIX PGP Encryption Format

Status: Draft Author: Snider Created: 2026-01-13 License: EUPL-1.2 Depends On: RFC-003


Abstract

TRIX is a PGP-based encryption format for DataNode archives and account credentials. It provides symmetric and asymmetric encryption using OpenPGP standards and ChaCha20-Poly1305, enabling secure data exchange and identity management.

1. Overview

TRIX provides:

  • PGP symmetric encryption for DataNode archives
  • ChaCha20-Poly1305 modern encryption
  • PGP armored keys for account/identity management
  • Integration with Enchantrix library

2. Public API

2.1 Key Derivation

// pkg/trix/trix.go:64-67
func DeriveKey(password string) []byte {
    hash := sha256.Sum256([]byte(password))
    return hash[:]  // 32 bytes
}
  • Input: password string (any length)
  • Output: 32-byte key (256 bits)
  • Algorithm: SHA-256 hash of UTF-8 bytes
  • Deterministic: identical passwords → identical keys

2.2 Legacy PGP Encryption

// Encrypt DataNode to TRIX (PGP symmetric)
func ToTrix(dn *datanode.DataNode, password string) ([]byte, error)

// Decrypt TRIX to DataNode (DISABLED for encrypted payloads)
func FromTrix(data []byte, password string) (*datanode.DataNode, error)

Note: FromTrix with a non-empty password returns error "decryption disabled: cannot accept encrypted payloads". This is intentional to prevent accidental password use.

2.3 Modern ChaCha20-Poly1305 Encryption

// Encrypt with ChaCha20-Poly1305
func ToTrixChaCha(dn *datanode.DataNode, password string) ([]byte, error)

// Decrypt ChaCha20-Poly1305
func FromTrixChaCha(data []byte, password string) (*datanode.DataNode, error)

2.4 Error Variables

var (
    ErrPasswordRequired = errors.New("password is required for encryption")
    ErrDecryptionFailed = errors.New("decryption failed (wrong password?)")
)

3. File Format

3.1 Container Structure

[4 bytes]   Magic: "TRIX" (ASCII)
[Variable]  Gob-encoded Header (map[string]interface{})
[Variable]  Payload (encrypted or unencrypted tarball)

3.2 Header Examples

Unencrypted:

Header: map[string]interface{}{}  // Empty map

ChaCha20-Poly1305:

Header: map[string]interface{}{
    "encryption_algorithm": "chacha20poly1305",
}

3.3 ChaCha20-Poly1305 Payload

[24 bytes]  XChaCha20 Nonce (embedded)
[N bytes]   Encrypted tar archive
[16 bytes]  Poly1305 authentication tag

Note: Nonces are embedded in the ciphertext by Enchantrix, not stored separately.

4. Encryption Workflows

// Encryption
func ToTrixChaCha(dn *datanode.DataNode, password string) ([]byte, error) {
    // 1. Validate password is non-empty
    if password == "" {
        return nil, ErrPasswordRequired
    }

    // 2. Serialize DataNode to tar
    tarball, _ := dn.ToTar()

    // 3. Derive 32-byte key
    key := DeriveKey(password)

    // 4. Create sigil and encrypt
    sigil, _ := enchantrix.NewChaChaPolySigil(key)
    encrypted, _ := sigil.In(tarball)  // Generates nonce automatically

    // 5. Create Trix container
    t := &trix.Trix{
        Header:  map[string]interface{}{"encryption_algorithm": "chacha20poly1305"},
        Payload: encrypted,
    }

    // 6. Encode with TRIX magic
    return trix.Encode(t, "TRIX", nil)
}

4.2 Decryption

func FromTrixChaCha(data []byte, password string) (*datanode.DataNode, error) {
    // 1. Validate password
    if password == "" {
        return nil, ErrPasswordRequired
    }

    // 2. Decode TRIX container
    t, _ := trix.Decode(data, "TRIX", nil)

    // 3. Derive key and decrypt
    key := DeriveKey(password)
    sigil, _ := enchantrix.NewChaChaPolySigil(key)
    tarball, err := sigil.Out(t.Payload)  // Extracts nonce, verifies MAC
    if err != nil {
        return nil, fmt.Errorf("%w: %v", ErrDecryptionFailed, err)
    }

    // 4. Deserialize DataNode
    return datanode.FromTar(tarball)
}

4.3 Legacy PGP (Disabled Decryption)

func ToTrix(dn *datanode.DataNode, password string) ([]byte, error) {
    tarball, _ := dn.ToTar()

    var payload []byte
    if password != "" {
        // PGP symmetric encryption
        cryptService := crypt.NewService()
        payload, _ = cryptService.SymmetricallyEncryptPGP([]byte(password), tarball)
    } else {
        payload = tarball
    }

    t := &trix.Trix{Header: map[string]interface{}{}, Payload: payload}
    return trix.Encode(t, "TRIX", nil)
}

func FromTrix(data []byte, password string) (*datanode.DataNode, error) {
    // Security: Reject encrypted payloads
    if password != "" {
        return nil, errors.New("decryption disabled: cannot accept encrypted payloads")
    }

    t, _ := trix.Decode(data, "TRIX", nil)
    return datanode.FromTar(t.Payload)
}

5. Enchantrix Library

5.1 Dependencies

import (
    "github.com/Snider/Enchantrix/pkg/trix"      // Container format
    "github.com/Snider/Enchantrix/pkg/crypt"     // PGP operations
    "github.com/Snider/Enchantrix/pkg/enchantrix" // AEAD sigils
)

5.2 Trix Container

type Trix struct {
    Header  map[string]interface{}
    Payload []byte
}

func Encode(t *Trix, magic string, extra interface{}) ([]byte, error)
func Decode(data []byte, magic string, extra interface{}) (*Trix, error)

5.3 ChaCha20-Poly1305 Sigil

// Create sigil with 32-byte key
sigil, err := enchantrix.NewChaChaPolySigil(key)

// Encrypt (generates random 24-byte nonce)
ciphertext, err := sigil.In(plaintext)

// Decrypt (extracts nonce, verifies MAC)
plaintext, err := sigil.Out(ciphertext)

6. Account System Integration

6.1 PGP Armored Keys

-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBGX...base64...
-----END PGP PUBLIC KEY BLOCK-----

6.2 Key Storage

~/.borg/
├── identity.pub     # PGP public key (armored)
├── identity.key     # PGP private key (armored, encrypted)
└── keyring/         # Trusted public keys

7. CLI Usage

# Encrypt with TRIX (PGP symmetric)
borg collect github repo https://github.com/user/repo \
    --format trix \
    --password "password"

# Decrypt unencrypted TRIX
borg decode archive.trix -o decoded.tar

# Inspect without decrypting
borg inspect archive.trix
# Output:
#   Format: TRIX
#   encryption_algorithm: chacha20poly1305 (if present)
#   Payload Size: N bytes

8. Format Comparison

Format Extension Algorithm Use Case
datanode .tar None Uncompressed archive
tim .tim None Container bundle
trix .trix PGP/AES or ChaCha Encrypted archives, accounts
stim .stim ChaCha20-Poly1305 Encrypted containers
smsg .smsg ChaCha20-Poly1305 Encrypted media

9. Security Analysis

9.1 Key Derivation Limitations

Current implementation: SHA-256 (single round)

Metric Value
Algorithm SHA-256
Iterations 1
Salt None
Key stretching None

Implications:

  • GPU brute force: ~10 billion guesses/second
  • 8-character password: ~10 seconds to break
  • Recommendation: Use 15+ character passwords

9.2 ChaCha20-Poly1305 Properties

Property Status
Authentication Poly1305 MAC (16 bytes)
Key size 256 bits
Nonce size 192 bits (XChaCha)
Standard RFC 7539 compliant

10. Test Coverage

Test Description
DeriveKey length Output is exactly 32 bytes
DeriveKey determinism Same password → same key
DeriveKey uniqueness Different passwords → different keys
ToTrix without password Valid TRIX with "TRIX" magic
ToTrix with password PGP encryption applied
FromTrix unencrypted Round-trip preserves files
FromTrix password rejection Returns error
ToTrixChaCha success Valid TRIX created
ToTrixChaCha empty password Returns ErrPasswordRequired
FromTrixChaCha round-trip Preserves nested directories
FromTrixChaCha wrong password Returns ErrDecryptionFailed
FromTrixChaCha large data 1MB file processed

11. Implementation Reference

  • Source: pkg/trix/trix.go
  • Tests: pkg/trix/trix_test.go
  • Enchantrix: github.com/Snider/Enchantrix v0.0.2

12. Security Considerations

  1. Use strong passwords: 15+ characters due to no key stretching
  2. Prefer ChaCha: Use ToTrixChaCha over legacy PGP
  3. Key backup: Securely backup private keys
  4. Interoperability: TRIX files with GPG require password

13. Future Work

  • Key stretching (Argon2 option in DeriveKey)
  • Public key encryption support
  • Signature support
  • Key expiration metadata
  • Multi-recipient encryption