8.4 KiB
RFC-005: STIM Encrypted Container Format
Status: Draft Author: Snider Created: 2026-01-13 License: EUPL-1.2 Depends On: RFC-003, RFC-004
Abstract
STIM (Secure TIM) is an encrypted container format that wraps TIM bundles using ChaCha20-Poly1305 authenticated encryption. It enables secure distribution and execution of containers without exposing the contents.
1. Overview
STIM provides:
- Encrypted TIM containers
- ChaCha20-Poly1305 authenticated encryption
- Separate encryption of config and rootfs
- Direct execution without persistent decryption
2. Format Name
ChaChaPolySigil - The internal name for the STIM format, using:
- ChaCha20-Poly1305 algorithm (via Enchantrix library)
- Trix container wrapper with "STIM" magic
3. File Structure
3.1 Container Format
STIM uses the Trix container format from Enchantrix library:
┌─────────────────────────────────────────┐
│ Magic: "STIM" (4 bytes ASCII) │
├─────────────────────────────────────────┤
│ Trix Header (Gob-encoded JSON) │
│ - encryption_algorithm: "chacha20poly1305"
│ - tim: true │
│ - config_size: uint32 │
│ - rootfs_size: uint32 │
│ - version: "1.0" │
├─────────────────────────────────────────┤
│ Trix Payload: │
│ [config_size: 4 bytes BE uint32] │
│ [encrypted config] │
│ [encrypted rootfs tar] │
└─────────────────────────────────────────┘
3.2 Payload Structure
Offset Size Field
------ ----- ------------------------------------
0 4 Config size (big-endian uint32)
4 N Encrypted config (includes nonce + tag)
4+N M Encrypted rootfs tar (includes nonce + tag)
3.3 Encrypted Component Format
Each encrypted component (config and rootfs) follows Enchantrix format:
[24-byte XChaCha20 nonce][ciphertext][16-byte Poly1305 tag]
Critical: Nonces are embedded in the ciphertext, not transmitted separately.
4. Encryption
4.1 Algorithm
XChaCha20-Poly1305 (extended nonce variant)
| Parameter | Value |
|---|---|
| Key size | 32 bytes |
| Nonce size | 24 bytes (embedded) |
| Tag size | 16 bytes |
4.2 Key Derivation
// pkg/trix/trix.go:64-67
func DeriveKey(password string) []byte {
hash := sha256.Sum256([]byte(password))
return hash[:] // 32 bytes
}
4.3 Dual Encryption
Config and RootFS are encrypted separately with independent nonces:
// pkg/tim/tim.go:217-232
func (m *TerminalIsolationMatrix) ToSigil(password string) ([]byte, error) {
// 1. Derive key
key := trix.DeriveKey(password)
// 2. Create sigil
sigil, _ := enchantrix.NewChaChaPolySigil(key)
// 3. Encrypt config (generates fresh nonce automatically)
encConfig, _ := sigil.In(m.Config)
// 4. Serialize rootfs to tar
rootfsTar, _ := m.RootFS.ToTar()
// 5. Encrypt rootfs (generates different fresh nonce)
encRootFS, _ := sigil.In(rootfsTar)
// 6. Build payload
payload := make([]byte, 4+len(encConfig)+len(encRootFS))
binary.BigEndian.PutUint32(payload[:4], uint32(len(encConfig)))
copy(payload[4:4+len(encConfig)], encConfig)
copy(payload[4+len(encConfig):], encRootFS)
// 7. Create Trix container with STIM magic
// ...
}
Rationale for dual encryption:
- Config can be decrypted separately for inspection
- Allows streaming decryption of large rootfs
- Independent nonces prevent any nonce reuse
5. Decryption Flow
// pkg/tim/tim.go:255-308
func FromSigil(data []byte, password string) (*TerminalIsolationMatrix, error) {
// 1. Decode Trix container with magic "STIM"
t, _ := trix.Decode(data, "STIM", nil)
// 2. Derive key from password
key := trix.DeriveKey(password)
// 3. Create sigil
sigil, _ := enchantrix.NewChaChaPolySigil(key)
// 4. Parse payload: extract configSize from first 4 bytes
configSize := binary.BigEndian.Uint32(t.Payload[:4])
// 5. Validate bounds
if int(configSize) > len(t.Payload)-4 {
return nil, ErrInvalidStimPayload
}
// 6. Extract encrypted components
encConfig := t.Payload[4 : 4+configSize]
encRootFS := t.Payload[4+configSize:]
// 7. Decrypt config (nonce auto-extracted by Enchantrix)
config, err := sigil.Out(encConfig)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrDecryptionFailed, err)
}
// 8. Decrypt rootfs
rootfsTar, err := sigil.Out(encRootFS)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrDecryptionFailed, err)
}
// 9. Reconstruct DataNode from tar
rootfs, _ := datanode.FromTar(rootfsTar)
return &TerminalIsolationMatrix{Config: config, RootFS: rootfs}, nil
}
6. Trix Header
Header: map[string]interface{}{
"encryption_algorithm": "chacha20poly1305",
"tim": true,
"config_size": len(encConfig),
"rootfs_size": len(encRootFS),
"version": "1.0",
}
7. CLI Usage
# Create encrypted container
borg compile -f Borgfile -e "password" -o container.stim
# Run encrypted container
borg run container.stim -p "password"
# Decode (extract) encrypted container
borg decode container.stim -p "password" --i-am-in-isolation -o container.tar
# Inspect without decrypting (shows header metadata only)
borg inspect container.stim
# Output:
# Format: STIM
# encryption_algorithm: chacha20poly1305
# config_size: 1234
# rootfs_size: 567890
8. Cache API
// Create cache with master password
cache, err := tim.NewCache("/path/to/cache", masterPassword)
// Store TIM (encrypted automatically as .stim)
err := cache.Store("name", tim)
// Load TIM (decrypted automatically)
tim, err := cache.Load("name")
// List cached containers
names, err := cache.List()
9. Execution Security
// Secure execution flow
func RunEncrypted(path, password string) error {
// 1. Create secure temp directory
tmpDir, _ := os.MkdirTemp("", "borg-run-*")
defer os.RemoveAll(tmpDir) // Secure cleanup
// 2. Read and decrypt
data, _ := os.ReadFile(path)
tim, _ := FromSigil(data, password)
// 3. Extract to temp
tim.ExtractTo(tmpDir)
// 4. Execute with runc
return runRunc(tmpDir)
}
10. Security Properties
10.1 Confidentiality
- Contents encrypted with ChaCha20-Poly1305
- Password-derived key never stored
- Nonces are random, never reused
10.2 Integrity
- Poly1305 MAC prevents tampering
- Decryption fails if modified
- Separate MACs for config and rootfs
10.3 Error Detection
| Error | Cause |
|---|---|
ErrPasswordRequired |
Empty password provided |
ErrInvalidStimPayload |
Payload < 4 bytes or invalid size |
ErrDecryptionFailed |
Wrong password or corrupted data |
11. Comparison to TRIX
| Feature | STIM | TRIX |
|---|---|---|
| Algorithm | ChaCha20-Poly1305 | PGP/AES or ChaCha |
| Content | TIM bundles | DataNode (raw files) |
| Structure | Dual encryption | Single blob |
| Magic | "STIM" | "TRIX" |
| Use case | Container execution | General encryption, accounts |
STIM is for containers. TRIX is for general file encryption and accounts.
12. Implementation Reference
- Encryption:
pkg/tim/tim.go(ToSigil, FromSigil) - Key derivation:
pkg/trix/trix.go(DeriveKey) - Cache:
pkg/tim/cache.go - CLI:
cmd/run.go,cmd/decode.go,cmd/compile.go - Enchantrix:
github.com/Snider/Enchantrix
13. Security Considerations
- Password strength: Recommend 64+ bits entropy (12+ chars)
- Key derivation: SHA-256 only (no stretching) - use strong passwords
- Memory handling: Keys should be wiped after use
- Temp files: Use tmpfs when available, secure wipe after
- Side channels: Enchantrix uses constant-time crypto operations
14. Future Work
- Hardware key support (YubiKey, TPM)
- Key stretching (Argon2)
- Multi-recipient encryption
- Streaming decryption for large rootfs