Threat Model
Cloudstic’s encryption is designed to protect against:At-rest compromise: If your Backblaze B2 account or S3 bucket is compromised, the attacker cannot read your backup data without the encryption key.
- Storage provider breach - B2, S3, PostgreSQL data is unreadable without keys
- Tenant isolation (SaaS) - Even if database RLS is bypassed, each tenant’s data is encrypted with a unique key
- Confirmation-of-a-file attacks - Keyed HMAC prevents storage providers from confirming file contents by hashing known plaintext
- Key loss (recovery keys) - BIP39 mnemonic recovery keys provide an offline backup path
docs/encryption.md:9-17
Ciphertext Format
Every encrypted object uses a consistent binary format:| Version | Nonce | Ciphertext | GCM Tag |
|---|---|---|---|
| 1 byte | 12 bytes | Variable | 16 bytes |
- Version
0x01: AES-256-GCM with 12-byte random nonce - Nonce: Randomly generated for each encryption (never reused)
- GCM Tag: 16-byte authentication tag (detects tampering)
pkg/crypto/crypto.go:23-29 and docs/encryption.md:18-29
Encryption Process
pkg/crypto/crypto.go:38-54
Decryption Process
pkg/crypto/crypto.go:59-80
AES-256-GCM provides both encryption (confidentiality) and authentication (integrity). Tampering with the ciphertext causes decryption to fail.
Key Hierarchy
Cloudstic uses a multi-tier key hierarchy to separate key management from data encryption: Location:docs/encryption.md:36-58
Master Key
- 256-bit random key generated from
crypto/randat repository initialization - Never stored in plaintext
- Wrapped by one or more key slots (password, platform key, KMS, recovery key)
- Unique per tenant (CLI repo or SaaS tenant)
docs/encryption.md:60-63
Key Derivation
The master key is not used directly for encryption. Instead, keys are derived using HKDF-SHA256:Encryption Key
pkg/crypto/crypto.go:90-97 and docs/encryption.md:106-116
This key is used for AES-256-GCM encryption of all backup objects (chunks, metadata, snapshots).
Dedup HMAC Key
pkg/crypto/crypto.go:124 and docs/encryption.md:118-127
This key is used for HMAC-SHA256 chunk addressing to prevent confirmation-of-a-file attacks (see Content Addressing).
Key Slots
A key slot stores the master key encrypted (“wrapped”) by a wrapping key. Multiple slots can coexist, each using a different wrapping mechanism.| Slot Type | Wrapping Key Source | Purpose |
|---|---|---|
platform | PLATFORM_ENCRYPTION_KEY env var | Legacy platform recovery (plaintext hex key) |
kms-platform | AWS KMS CMK | HSM-backed platform recovery (envelope encryption) |
password | Argon2id(user password) | Zero-knowledge user access |
recovery | Random 256-bit key (BIP39 mnemonic) | Offline backup (24-word seed phrase) |
docs/encryption.md:65-77
Key Slot Storage
Key slots are stored in two locations:- PostgreSQL (
app.encryption_key_slotstable) - Primary source for SaaS web application - Object store (
keys/<type>-<label>objects) - Best-effort copy for CLI access and disaster recovery
Key slot objects are stored unencrypted in the object store. The
EncryptedStore decorator passes through any object under the keys/ prefix without encrypting it.docs/encryption.md:79-104 and AGENTS.md:91-93
Key Slot Format
wrapped_key is base64-encoded ciphertext:
docs/encryption.md:89-97
Key Slot Types
Password Slot
User-chosen password wrapped with Argon2id key derivation:pkg/crypto/crypto.go:140-157 and docs/encryption.md:42-43
Argon2id is a memory-hard password hashing algorithm designed to resist GPU/ASIC attacks. The default parameters (~64MB memory, ~1 second on modern hardware) provide strong protection against brute-force attacks.
Platform Slot (Legacy)
The platform key is a 32-byte hex-encoded key stored in thePLATFORM_ENCRYPTION_KEY environment variable:
docs/encryption.md:311-319
KMS-Platform Slot
Uses AWS KMS envelope encryption for HSM-backed key protection: Process:- Call
kms:GenerateDataKeyto get a random DEK and its KMS-encrypted form - Use the plaintext DEK to wrap the master key with AES-GCM
- Discard the plaintext DEK
- Store the encrypted DEK and wrapped master key in the slot
- Call
kms:Decryptto decrypt the DEK - Use the plaintext DEK to unwrap the master key
- Discard the plaintext DEK
- Plaintext wrapping key never leaves the KMS HSM
- KMS audit logs track all decrypt operations
- IAM policies control access
- Automatic annual key rotation
docs/encryption.md:298-310 and pkg/crypto/kms.go
Recovery Key Slot
A BIP39 24-word mnemonic that wraps the master key. Provides an offline backup mechanism for key recovery.Generation
docs/encryption.md:219-232
Recovery
docs/encryption.md:234-239
CLI Usage
docs/encryption.md:241-259
Encryption Store Layer
TheEncryptedStore is a decorator in the store stack:
Location: docs/encryption.md:136-149
Put Operation
pkg/store/encrypted.go
Get Operation
pkg/store/encrypted.go and docs/encryption.md:145-148
The
EncryptedStore gracefully handles legacy unencrypted data by checking the version byte. If data doesn’t start with 0x01, it’s returned as-is.docs/encryption.md:30-34
Content Addressing with Encryption
Encryption uses random nonces, so encrypting the same plaintext twice produces different ciphertext. How does deduplication still work?Deduplication happens before encryption by checking if the content-addressed key already exists.
Chunk Deduplication Flow
internal/engine/chunker.go:166-186 and docs/encryption.md:157-170
Because the key is derived from plaintext (via HMAC), identical chunks produce identical keys. The Exists() check short-circuits the upload.
Cross-Tenant Deduplication
Each tenant/repository has a unique dedup key, so cross-tenant deduplication does not occur. This is intentional for privacy.
- Tenant A’s
chunk/a3f5b8...is HMAC-SHA256(keyA, data) - Tenant B’s
chunk/7d9e2c...is HMAC-SHA256(keyB, data) - Even if
datais identical, the keys differ
docs/encryption.md:164-169
Key Rotation
Platform Key Rotation
When rotating the platform key (e.g.,PLATFORM_ENCRYPTION_KEY env var changes):
docs/encryption.md:172-183
Master Key Rotation (Rare)
Process:- Generate new master key
- Create new key slots with new master key
- Keep old key in memory for dual-key reads
- New writes use new key; reads try new key first, fall back to old key on authentication failure
- Background job re-encrypts all objects with new key
- Once complete, retire old key slots
docs/encryption.md:185-197
Security Best Practices
For CLI Users
For SaaS Deployments
Location:docs/encryption.md:288-325
CLI Key Resolution Flow
When the CLI opens an encrypted repository: Location:docs/encryption.md:287-294
Further Reading
- Using Profiles - Store-level secret references (
*_secret) for runtime credentials - Managing Encryption Keys - How profile secret references differ from repository key slots
- Content Addressing - How HMAC-keyed hashing prevents confirmation-of-a-file attacks
- Architecture - How EncryptedStore fits into the storage stack
- Key Slots CLI Reference - Command-line encryption options