TGCryptFS protects against:
- Server-side compromise: Telegram cannot read file contents (encrypted before upload)
- Database theft: SQLite files are encrypted with opaque schema - no useful information without the key
- Block reuse attacks: AAD binding prevents cross-context block substitution
- Password guessing: Argon2id with 64MB memory cost and 3 iterations
- Quantum computing: ML-KEM-768 for post-quantum key exchange
- Key compromise propagation: Separate keys for data, metadata, schema, integrity, wrapping, and deadman
- Physical seizure: Deadman option with configurable destruction triggers
TGCryptFS does NOT protect against:
- Active endpoint compromise: A compromised device with the password can read all data
- Rubber-hose cryptanalysis: The password holder can always decrypt
- Traffic analysis: Telegram can observe upload/download patterns and block sizes
- Telegram account seizure: If the Telegram account is compromised, encrypted blocks can be deleted
| Primitive | Algorithm | Purpose |
|---|---|---|
| AEAD | XChaCha20-Poly1305 | All data encryption |
| KDF (password) | Argon2id | Password to root key |
| KDF (hierarchy) | HKDF-SHA256 | Root key to sub-keys |
| Hash | BLAKE3 | Content addressing, integrity |
| KEM | ML-KEM-768 | Post-quantum key exchange |
| MAC | BLAKE3 keyed | Integrity verification |
Password (user input, never stored)
+ Salt (random 32 bytes, stored in volume config)
|
v [Argon2id: 64MB memory, 3 iterations, 4 threads]
|
Root Key (32 bytes)
|
+-- HKDF(root, "SentenceRefs.v1:data") -> Kdata
+-- HKDF(root, "SentenceRefs.v1:meta") -> Kmeta
+-- HKDF(root, "SentenceRefs.v1:schema") -> Kschema
+-- HKDF(root, "SentenceRefs.v1:ih") -> Kih
+-- HKDF(root, "SentenceRefs.v1:wrapping")-> Kwrap
+-- HKDF(root, "SentenceRefs.v1:deadman") -> Kdeadman
All key material is wrapped in zeroize::Zeroize + ZeroizeOnDrop:
- Keys are zeroed when dropped
- No key material is serialized to disk (only the salt and KDF params are stored)
- Temporary key buffers use stack-allocated
[u8; 32]arrays
For forward secrecy, data encryption uses epoch-scoped keys:
Kdata + epoch_number -> HKDF -> Kepoch
When rotating to a new epoch:
- Derive new epoch key
- Re-encrypt all blocks with new key
- Zero previous epoch key
- Previous data cannot be decrypted even with the root key if the epoch key is destroyed
Every AEAD encryption includes Additional Authenticated Data (AAD) that binds the ciphertext to its context:
| Context | AAD Format |
|---|---|
| File block | inode:{ino}:block:{offset} |
| Inode metadata | inode:{ino} |
| Policy | policy:{pid} |
| Volume config | volume:{volume_id} |
| Snapshot | snapshot:{snapshot_id} |
| User record | user:{uid} |
| Wrapped key | share:{volume_id}:{invite_id} |
This prevents:
- Moving a block from one inode to another
- Swapping metadata between inodes
- Replaying encrypted data in a different context
SQLite table and column names are derived deterministically:
opaque_name = hex(BLAKE3(Kschema || domain || ":" || logical_name))
Example:
- Logical:
inodes.ino-> Opaque:a7f3c2...4e.b91d0e...82 - Without Kschema, the schema structure is indistinguishable from random
ML-KEM-768 (Module Lattice Key Encapsulation Mechanism):
- NIST post-quantum standard (FIPS 203)
- 768-dimensional lattice provides 128-bit security level
- Resistant to Shor's algorithm on quantum computers
Flow:
Recipient: generate(ML-KEM-768) -> (dk, ek)
send ek to Owner
Owner: encapsulate(ek) -> (shared_secret, ciphertext)
encrypt(shared_secret, data_key) -> wrapped_key
send (ciphertext, wrapped_key) to Recipient
Recipient: decapsulate(dk, ciphertext) -> shared_secret
decrypt(shared_secret, wrapped_key) -> data_key