Skip to content
teleproto

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.

key-reuse.ts
// 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 populated

Temporary 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.