Security Protocol¶
TeleVault uses a zero-trust model: your plaintext never leaves your machine. Telegram only ever sees encrypted ciphertext.
Encrypt-then-Upload Pipeline¶
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────┐
│ Original │───▶│ Chunk │───▶│ BLAKE3 Hash │───▶│ zstd │───▶│ AES-256 │
│ File │ │ (256MB) │ │ (original) │ │ Compress │ │ GCM │
└──────────┘ └──────────┘ └──────────────┘ └────────────┘ └──────────┘
│
▼
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────┐
│ Telegram │◀───│ Upload │◀───│ BLAKE3 Hash │◀───│ Ciphertext │ │ Encrypted│
│ Channel │ │ File Msg │ │ (encrypted) │ │ + Tag │ │ Chunk │
└──────────┘ └──────────┘ └──────────────┘ └────────────┘ └──────────┘
Every stage is verified:
- Pre-processing hash — BLAKE3 of the raw chunk before any transformation
- Post-processing hash — BLAKE3 of the encrypted, compressed chunk
- File-level hash — BLAKE3 of the entire reassembled file after download
AES-256-GCM¶
Each chunk is encrypted independently with its own key material:
| Parameter | Value |
|---|---|
| Cipher | AES-256-GCM |
| Key Size | 256 bits (32 bytes) |
| Salt | 16 bytes (random, per chunk) |
| Nonce | 12 bytes (random, per chunk) |
| Auth Tag | 16 bytes (GCM) |
| Overhead | 44 bytes per chunk |
Chunk Structure on Disk¶
The salt and nonce are prepended to the ciphertext. During decryption, they are extracted from the first 28 bytes of the chunk data.
Key Derivation: scrypt¶
key = scrypt(
password=user_password,
salt=random_16_bytes,
N=131072, # 2^17
r=8,
p=1,
dklen=32 # 256-bit key
)
The scrypt parameters are chosen to balance security and performance:
- N = 2^17 — Memory-hard, requires ~1 MB RAM per derivation
- r = 8 — Block size parameter
- p = 1 — Parallelization factor
Each chunk gets its own random salt, so even identical plaintext chunks produce different ciphertext.
BLAKE3 Integrity¶
BLAKE3 is used at three levels:
| Level | What | When |
|---|---|---|
original_hash |
Raw chunk data (pre-processing) | Before compression/encryption |
ChunkInfo.hash |
Encrypted chunk data (post-processing) | After compression/encryption |
FileMetadata.hash |
Entire file (all chunks concatenated) | After download completes |
Why Two Hashes Per Chunk¶
The original_hash catches a subtle attack: a wrong password can produce ciphertext that passes GCM tag verification but decrypts to garbage. By verifying the pre-processing hash after decryption, TeleVault catches this case immediately:
The ChunkInfo.hash verifies the encrypted data hasn't been corrupted during upload or download.
Zero-Trust Model¶
- Password never leaves your machine — it is used only for local scrypt key derivation
- No server-side encryption — Telegram stores only ciphertext
- No key escrow — there is no recovery mechanism. If you lose your password, your data is unrecoverable
- No metadata leakage — file names are stored in the VaultIndex (encrypted in the index message), not in Telegram message metadata
- No local database — all state is on Telegram, so there is no local file to steal
Crash Safety¶
The upload and delete sequences are designed to be safe against crashes at any point:
Upload Sequence¶
- Upload metadata message → get
metadata_msg_id - Upload all chunks in parallel
- On failure: delete all uploaded chunks + metadata (cleanup)
- Update metadata with chunk info
- Save index (3 retries with backoff)
If the process crashes after step 2 but before step 5, the data exists on Telegram but is not in the index. Run tvt gc --clean-partials to clean up.
Delete Sequence¶
- Remove file from index first
- Read metadata to collect chunk message IDs
- Delete all messages (metadata + chunks)
If the process crashes after step 1, the file is no longer referenced. tvt gc finds and cleans the orphaned messages.
Resumable Downloads¶
Progress is saved to a .progress file after each chunk, with CRC32 integrity checking:
If the progress file is corrupted (CRC32 mismatch), the download starts fresh. Partial data (.partial file) is preserved across retries.