8.7 KiB
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
4.1 ChaCha20-Poly1305 (Recommended)
// 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
- Use strong passwords: 15+ characters due to no key stretching
- Prefer ChaCha: Use
ToTrixChaChaover legacy PGP - Key backup: Securely backup private keys
- 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