303 lines
8.4 KiB
Markdown
303 lines
8.4 KiB
Markdown
# RFC-005: STIM Encrypted Container Format
|
|
|
|
**Status**: Draft
|
|
**Author**: [Snider](https://github.com/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
|
|
|
|
```go
|
|
// 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:
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
Header: map[string]interface{}{
|
|
"encryption_algorithm": "chacha20poly1305",
|
|
"tim": true,
|
|
"config_size": len(encConfig),
|
|
"rootfs_size": len(encRootFS),
|
|
"version": "1.0",
|
|
}
|
|
```
|
|
|
|
## 7. CLI Usage
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|