Auth keys & encryption
Every byte teleproto sends after the handshake is AES-IGE-encrypted under an auth key. This page covers where that key comes from, why you have several, how temporary keys add forward secrecy, and what the salts and msg_ids in every frame are actually checking.
What an auth key is#
A 2048-bit shared secret between your client and one Telegram data center, produced by a Diffie-Hellman exchange during the first connect. It is notyour login — it's the transport key. Login (the phone-code dance) happens insidethe encrypted channel and binds your account to that key afterwards. Both halves live in the session; that's why a saved session skips both the handshake and the login.
The DH exchange itself is three round-trips: reqPqMulti (server proves itself with an RSA fingerprint), reqDHParams (client sends its half under RSA), setClientDHParams (both sides derive the key and verify with a hash). teleproto runs it once per DC and never again for the life of the session.
One key per DC#
Auth keys don't roam. A key negotiated with DC 2 is meaningless to DC 4, so a client that downloads media from three DCs holds three (or more) keys. Telegram also caps how many keys an account can create in a window — burn through the budget with a crash loop that re-handshakes every boot and new connects start failing.
teleproto therefore treats keys as durable state: the per-DC key map persists in StoreSession / MemorySession, and all senders talking to the same DC — API, uploads, downloads — share that DC's one permanent key instead of negotiating private copies. Server salts are shared per-DC the same way.
// Persistent per-DC auth keys — negotiated once, reused every boot.
// StoreSession and MemorySession both keep the full per-DC key map,
// so a restart costs zero handshakes and zero of Telegram's
// per-account key budget.
const session = new StoreSession("account-main");
const client = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
});
await client.connect(); // no DH exchange if the key map is populatedTemporary keys & PFS#
A permanent key that encrypts years of traffic is a fat target, so MTProto supports perfect forward secrecy: negotiate a short-lived temporary key over a variant of the same DH exchange, then bind it to the permanent key with auth.bindTempAuthKey. Traffic runs under the temp key; compromise of a captured stream doesn't unlock past sessions.
teleproto uses temp keys for media connections. Two implementation details that matter operationally: since v1.228 a temp key is owned by its connection object and is discarded with it — a reconnected media lane always re-binds a fresh key, which killed a family of "downloads deadlock after the server drops an auth key" bugs (a transport-404 on a stale temp key now triggers re-binding instead of hanging the transfer).
Salts, msg_ids, replay checks#
Inside the encrypted envelope every message carries three anti-abuse fields, and teleproto validates all of them on the read path (enforced since v1.228):
- server_salt — a 64-bit value the server rotates roughly every half hour. Wrong salt gets a
bad_server_saltanswer containing the right one; teleproto swaps it in and re-sends without surfacing anything. Salts are per-DC and shared across that DC's senders. - msg_id— time-based (unixtime scaled into the upper 32 bits), must be monotonically increasing within a session, must not be too far in the past or future. Answers are matched to requests by it. teleproto tracks the server's ids and rejects duplicates and out-of-window ids — that's the replay check.
- session_id + seq_no — a random 64-bit session marker plus a counter that increments for content-related messages. Together they stop a captured frame from being re-injected into another session.
AES-IGE in pure JS#
MTProto uses AES-256 in IGE mode — a chaining mode Node's crypto module doesn't expose, so every JS MTProto library implements it in userland. Naive IGE re-creates the AES cipher context per 16-byte block; teleproto keeps one context per key and reuses it, which alone tripled encryption throughput (v1.227.3). Decrypt goes further: IGE decryption is built from the encrypt direction of native AES-CBC plus a T-table step in JS (v1.227.4), so the hot loop runs in OpenSSL, not in the interpreter.
Combined with single-copy encryption on the send path — the frame is encrypted where it was assembled instead of being copied into a scratch buffer first — crypto stopped being the bottleneck for file transfer; the socket is.
Where keys live#
In the session, unencrypted. StringSession encodes the current DC's key in the base64 blob; StoreSession writes the whole per-DC map to disk. Which is the concrete reason the session-handling page keeps repeating itself: whoever reads the session file holds the transport key and the login bound to it. There is no second factor at the protocol level — possession of the key is possession of the account, until the key is revoked from another device.