This document describes the security architecture, threat model, and cryptographic design of tgcryptfs.
tgcryptfs is designed to achieve the following security properties:
- Confidentiality: File contents, names, and directory structure are hidden from Telegram and network observers
- Integrity: Any tampering with stored data is detected
- Authenticity: Data can only be created by someone with the correct password
- Forward Secrecy: Compromise of current keys doesn't expose historical data (with snapshots)
| Threat | Mitigation |
|---|---|
| Telegram reading your data | All data encrypted before upload |
| Telegram modifying your data | AES-GCM authentication detects tampering |
| Network eavesdroppers | TLS to Telegram + our encryption layer |
| Local disk theft (cache) | Cache contains decrypted data (see limitations) |
| Password brute-forcing | Argon2id with high memory/time cost |
| Threat | Reason |
|---|---|
| Compromised local machine | If attacker has root, game over |
| Malicious tgcryptfs binary | Supply chain attacks not addressed |
| Side-channel attacks | Not hardened against timing/power analysis |
| Rubber-hose cryptanalysis | Can't help with physical coercion |
User Password
│
▼
┌───────────────────────┐
│ Argon2id │
│ (salt, params) │
└───────────────────────┘
│
▼
Master Key (256 bits)
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Metadata │ │ Chunk 1 │ │ Chunk N │
│ Key │ │ Key │ │ Key │
└──────────┘ └──────────┘ └──────────┘
Algorithm: Argon2id (winner of Password Hashing Competition)
Parameters (configurable):
- Memory: 64 MiB (default)
- Iterations: 3
- Parallelism: 4
- Output: 256 bits
Why Argon2id?
- Resistant to GPU/ASIC attacks (memory-hard)
- Resistant to side-channel attacks (data-independent)
- Modern, well-analyzed algorithm
Salt: 32 bytes, randomly generated on first initialization, stored in config
Algorithm: HKDF-SHA256
Purpose-specific derivation:
Metadata Key = HKDF(Master Key, salt, "tgcryptfs-metadata-v1")
Chunk Key = HKDF(Master Key, salt, "tgcryptfs-chunk-v1:<chunk_id>")
Why per-chunk keys?
- Limits exposure if a single chunk key is compromised
- Enables future key rotation per-chunk
- Chunk ID (content hash) provides unique context
Algorithm: AES-256-GCM (AEAD)
Parameters:
- Key: 256 bits (from HKDF)
- Nonce: 96 bits, randomly generated per encryption
- Tag: 128 bits, appended to ciphertext
Properties:
- Confidentiality: AES in counter mode
- Integrity: GHASH polynomial authentication
- Authenticity: Verifies data came from key holder
Nonce handling:
- Fresh random nonce for every encryption
- Never reused (random 96-bit collision probability negligible)
- Stored with ciphertext
Algorithm: BLAKE3
Usage:
- Chunk identification (content-addressing)
- File integrity verification
- Deduplication detection
Why BLAKE3?
- Faster than SHA-256
- Cryptographically secure
- Parallelizable
- No length extension attacks
| Data | Encryption | Storage |
|---|---|---|
| File contents | AES-256-GCM per chunk | Telegram |
| Inode metadata | AES-256-GCM with metadata key | Local sled DB |
| Directory structure | Encoded in encrypted inodes | Local sled DB |
| Chunk manifest | Part of encrypted inode | Local sled DB |
| Snapshot data | AES-256-GCM with metadata key | Local (exportable) |
| Configuration | Plaintext (no secrets except salt) | Local JSON |
Telegram only receives:
- Encrypted blobs (random-looking bytes)
- File names like
tgfs_chunk_<random>(no real filenames) - File sizes (chunk sizes, not original file sizes)
- Upload/download patterns (timing metadata)
Telegram cannot determine:
- File contents
- Original file names
- Directory structure
- Which chunks belong to which files
- How many files you have
Metadata database (sled):
- All values encrypted before storage
- Keys are plaintext (inode numbers, chunk IDs)
- Protected by filesystem permissions
Cache directory:
- Contains decrypted chunk data for performance
- Protected only by filesystem permissions
- Clear with
tgcryptfs cache --clear
Configuration:
- Contains salt (not secret, but needed)
- Contains Telegram credentials (protect this file!)
- No encryption keys stored
1. User provides password
2. Generate random 32-byte salt
3. Derive master key via Argon2id
4. Store salt in configuration (not the key!)
5. Initialize root inode with derived metadata key
1. User provides password
2. Load salt from configuration
3. Derive master key via Argon2id (same params)
4. Derive metadata key
5. Attempt to decrypt root inode
6. If decryption fails → wrong password
7. If successful → mount filesystem
There's no stored "password hash" to verify against. Instead:
- Derive keys from provided password
- Attempt to decrypt existing metadata
- GCM authentication failure = wrong password
This provides implicit verification without storing password-equivalent data.
Files are split into chunks, each identified by its BLAKE3 hash.
Security consideration: Identical content produces identical chunk IDs.
Implications:
- An attacker who knows plaintext can check if it exists
- Mitigated: Chunks are encrypted, so hash is of encrypted data
- Actually: We hash plaintext for dedup, then encrypt
Current design:
Chunk ID = BLAKE3(plaintext_chunk)
Stored = Encrypt(plaintext_chunk, ChunkKey(chunk_id))
Trade-off:
- Pro: Deduplication across files and time
- Con: Potential for confirmation attacks on known plaintext
Alternative (not implemented):
Chunk ID = BLAKE3(encrypted_chunk)
This would prevent confirmation attacks but break cross-session deduplication.
The local cache stores decrypted data for performance. This means:
- Cached data is readable by local processes
- Disk forensics could recover cached data
- Use full-disk encryption for defense in depth
Mitigation:
- Clear cache:
tgcryptfs cache --clear - Disable cache (not implemented, future feature)
- Use encrypted filesystem for cache directory
While file contents are encrypted, some metadata leaks:
- Chunk count: Reveals approximate file sizes
- Access patterns: Telegram sees which chunks are accessed when
- Timing: Operation timing could reveal activity patterns
All security derives from one password:
- Password compromise = total compromise
- No multi-user support
- No password recovery mechanism
We trust Telegram for:
- Availability: They could delete your data
- Durability: They could lose your data
- Ordering: Message IDs are trusted for chunk references
We don't trust Telegram for:
- Confidentiality (encrypted)
- Integrity (authenticated encryption)
Choose a strong, unique password:
- 16+ characters recommended
- Use a password manager
- Don't reuse from other services
Protect your local machine:
- Full-disk encryption
- Screen lock
- Secure configuration file permissions
Consider that:
- Telegram could ban your account
- Telegram could lose data
- You could forget your password
Keep offline backups of critical data.
Protect ~/.config/tgcryptfs/config.json:
chmod 600 ~/.config/tgcryptfs/config.jsonThis file contains your Telegram API credentials.
| Purpose | Library | Rationale |
|---|---|---|
| Key derivation | argon2 | Pure Rust, well-audited |
| Encryption | ring | BoringSSL-backed, audited |
| HKDF | ring | Consistent with encryption |
| Hashing | blake3 | Fast, secure, Rust-native |
| Random | rand | ChaCha20-based, secure |
- Key rotation: Ability to re-encrypt with new key
- Cache encryption: Encrypt local cache
- Secure memory: Use mlock for key material
- Yubikey support: Hardware key derivation
- Split knowledge: Require multiple passwords
- Plausible deniability: Hidden volumes
- Post-quantum: Hybrid encryption schemes
If you discover a security vulnerability:
- Do NOT open a public issue
- Email security@[project-domain] with details
- Allow reasonable time for fix before disclosure