Borg/rfc/RFC-005-STIM.md

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

  1. Password strength: Recommend 64+ bits entropy (12+ chars)
  2. Key derivation: SHA-256 only (no stretching) - use strong passwords
  3. Memory handling: Keys should be wiped after use
  4. Temp files: Use tmpfs when available, secure wipe after
  5. 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