# How Telegram Desktop Implements MTProto Encryption and the MTP Sender Pipeline

> Discover how Telegram Desktop uses MTProto encryption with AES-IGE and manages its MTP Sender pipeline via a type-safe builder pattern for efficient request handling.

- Repository: [Telegram Desktop/tdesktop](https://github.com/telegramdesktop/tdesktop)
- Tags: internals
- Published: 2026-04-05

---

**Telegram Desktop implements MTProto encryption by deriving AES-IGE keys from a 256-bit auth key to encrypt payloads, while the MTP::Sender pipeline manages request lifecycle through a type-safe builder pattern that routes encrypted packets to DC-specific threads.**

The `telegramdesktop/tdesktop` repository contains a sophisticated implementation of Telegram's MTProto protocol that separates cryptographic operations from network orchestration. This architecture combines low-level AES-IGE encryption with a concurrent sender pipeline to ensure secure, reliable communication with Telegram's data centers.

## MTProto Cryptographic Layer

The cryptographic foundation relies on the **AuthKey** class to manage 256-bit secrets and derive per-message encryption keys using SHA-256 based key derivation.

### Auth-Key Handling and Key ID Generation

The **auth key** is a 256-bit secret established during the initial key-exchange handshake and stored in `AuthKey::Data`. Each key receives a unique identifier calculated from the lower 64 bits of the SHA-1 hash of the auth key itself.

In [`Telegram/SourceFiles/mtproto/mtproto_auth_key.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/mtproto/mtproto_auth_key.cpp), the `countKeyId` method performs this calculation:

```cpp
// mtproto_auth_key.cpp lines 44-48
uint64 AuthKey::countKeyId(const void *key) {
    auto sha1 = hashSha1(key, 256);
    return *reinterpret_cast<const uint64*>(sha1.data() + 12);
}

```

This `_keyId` is transmitted with every encrypted message to allow the server to identify which auth key should decrypt the payload.

### AES-IGE Key Derivation

For each outbound message, Telegram Desktop derives a 128-bit **msg-key** and subsequent AES-IGE parameters using two primary methods: `prepareAES` for MTProto 2.0 and `prepareAES_oldmtp` for legacy compatibility. Both methods write 256-bit **aesKey** and **aesIV** buffers.

The `prepareAES` method in [`mtproto_auth_key.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtproto_auth_key.cpp) mixes the auth key with the payload hash:

```cpp
void AuthKey::prepareAES(const MTPint128 &msgKey,
                         MTPint256 &aesKey,
                         MTPint256 &aesIV,
                         bool send) const {
    uint32 x = send ? 0 : 8;
    // SHA-256 calculations combining auth key segments with msgKey
    // ...
}

```

### Encryption Primitives and OpenSSL Wrappers

The actual encryption utilizes **AES-IGE** (Infinite Garble Extension) mode through OpenSSL. The low-level primitives `aesIgeEncryptRaw` and `aesIgeDecryptRaw` wrap `AES_ige_encrypt` with appropriate direction flags.

Inline helpers in [`mtproto_auth_key.h`](https://github.com/telegramdesktop/tdesktop/blob/main/mtproto_auth_key.h) bind the derived keys to these raw routines:

```cpp
inline void aesIgeEncrypt(const void *src, void *dst, uint32 len,
                          const AuthKeyPtr &authKey, 
                          const MTPint128 &msgKey) {
    MTPint256 aesKey, aesIV;
    authKey->prepareAES(msgKey, aesKey, aesIV, true);
    aesIgeEncryptRaw(src, dst, len, 
                     static_cast<const void*>(&aesKey),
                     static_cast<const void*>(&aesIV));
}

```

These helpers are utilized for both network encryption and local data protection when generating temporary keys.

### Secure Request Transmission

Encryption is applied in `SessionPrivate::sendSecureRequest` within [`session_private.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/session_private.cpp). After constructing the MTProto header, the method calculates the msg-key (SHA-256 of the auth-key segment concatenated with the payload), then invokes `aesIgeEncrypt`:

```cpp
// session_private.cpp excerpt
aesIgeEncrypt(request->constData(),
              &packet[prefix],
              fullSize * sizeof(mtpPrime),
              _encryptionKey,
              msgKey);

```

The final packet structure contains the 8-byte key ID, 16-byte msg-key, and the AES-IGE ciphertext, ready for TCP or HTTP transport.

## The MTP::Sender Pipeline Architecture

The public API for dispatching MTProto calls is the **MTP::Sender** class, which implements a builder pattern for request construction and manages concurrency through the **ConcurrentSender** wrapper.

### The Sender Builder API

The `Sender` class in [`sender.h`](https://github.com/telegramdesktop/tdesktop/blob/main/sender.h) provides type-safe request builders through two entry points:

- `request(Request &&)` returns a **SpecificRequestBuilder** for new requests
- `request(mtpRequestId)` returns a `SentRequestWrap` for canceling existing requests

The builder supports method chaining for configuration:

```cpp
auto request = instance.sender().request(MTPmessages_GetHistory(...))
    .toDC(dcId)                // target data center
    .afterDelay(200)           // milliseconds to wait
    .done([](const auto &result, mtpRequestId id){ /* handle success */ })
    .fail([](const Error &e, mtpRequestId id){ /* handle failure */ })
    .handleFloodErrors()       // enable automatic flood-wait handling
    .send();                   // dispatch to network layer

```

The builder constructs `DoneHandler` and `FailHandler` objects via `MakeDoneHandler` and `MakeFailHandler`, which invoke user callbacks and notify the `Sender` upon completion through `senderRequestHandled`.

### Request Registration and Concurrency

`ConcurrentSender` acts as a thread-safe intermediary that forwards serialized requests to the appropriate `Instance`. When `SpecificRequestBuilder::send()` completes construction, it registers the request ID and callbacks:

```cpp
// mtproto_concurrent_sender.cpp
_sender->senderRequestRegister(requestId, std::move(_handlers));
_sender->with_instance([=](not_null<Instance*> instance) mutable {
    instance->sendSerialized(requestId,
                            std::move(request),
                            ResponseHandler{std::move(done), std::move(fail)},
                            dcId, msCanWait, afterRequestId);
});

```

The `ConcurrentSender` maintains a callback map keyed by request ID. When responses arrive, `Instance::Private::processCallback` executes the appropriate handler and unregisters the request via `unregisterRequest`.

### Error Handling and Retry Logic

The `Instance::Private::rpcErrorOccured` method analyzes `Error` objects for specific patterns:

- **Migration errors** (e.g., `FILE_MIGRATE_2`): Automatically change the target DC and resend
- **Flood waits** (e.g., `FLOOD_WAIT_10`): Schedule delayed retries with exponential backoff capped at 60 seconds

```cpp
// mtproto_instance.cpp flood handling
if ((m1 = FloodWaitRegExp.match(type)).hasMatch()) {
    secs = m1.captured(1).toInt();
}
_delayedRequests.insert(...);   // schedule retry with calculated delay

```

The **FailSkipPolicy** configured on the builder (`handleFloodErrors`, `handleAllErrors`) determines whether built-in error handling applies or if errors propagate directly to the user's `fail` lambda.

### Threading and DC Allocation

Each data center operates on its own **QThread**. `Instance::Private::getThreadForDc` selects the appropriate thread based on DC shifts (`kBaseDownloadDcShift`, `kBaseUploadDcShift`):

```cpp
// mtproto_instance.cpp thread selection
if (shiftedDcId == BareDcId(shiftedDcId))
    return EnsureStarted(_mainSessionThread, []{ return "MTP Main Session"; });

```

`Session` objects reside on these threads; the `Sender` posts requests to the correct `Session` through `Instance::Private::getSession`, ensuring thread-safe network operations without blocking the main UI thread.

## Practical Implementation Examples

### Sending a Simple Request

Fetching a user profile demonstrates the basic builder pattern:

```cpp
auto requestId = instance.sender()
    .request(MTPusers_GetFullUser(MTP_inputUserSelf()))
    .done([](const MTPUserFull &result, mtpRequestId id) {
        qDebug() << "Got user:" << result;
    })
    .fail([](const Error &e, mtpRequestId id) {
        qWarning() << "Failed:" << e.type();
    })
    .send();

```

The builder creates a `SpecificRequestBuilder<MTPusers_GetFullUser>`, registers the request with the `ConcurrentSender`, and hands the encrypted packet to the session layer for transmission.

### Handling Flood Errors Automatically

To enable automatic retry logic for rate limiting:

```cpp
auto id = instance.sender()
    .request(MTPmessages_SendMessage(...))
    .handleFloodErrors()            // auto-retry after FLOOD_WAIT
    .done([](const MTPUpdates &u, mtpRequestId) { /* process update */ })
    .fail([](const Error &e, mtpRequestId) { /* final failure handling */ })
    .send();

```

Setting `FailSkipPolicy::HandleFlood` instructs `rpcErrorOccured` to swallow flood errors and queue the request in `_delayedRequests` rather than invoking the fail callback immediately.

### Creating Request Dependencies

The pipeline supports ordered execution through the `afterRequest` method:

```cpp
mtpRequestId first = instance.sender()
    .request(MTPauth_ExportAuthorization(MTP_int(dc)))
    .done([](const MTPauth_ExportedAuthorization &auth) { /* store auth */ })
    .send();

instance.sender()
    .request(MTPauth_ImportAuthorization(/* ... */))
    .afterRequest(first)            // executes only after 'first' succeeds
    .done([](const MTPauth_Authorization &result) { /* proceed */ })
    .send();

```

The second request enters `_dependentRequests` and remains queued until the first request's success callback executes.

## Summary

- **Cryptographic Foundation**: The `AuthKey` class manages 256-bit secrets and derives per-message AES-IGE keys using SHA-256, with encryption performed via OpenSSL's IGE mode in `SessionPrivate::sendSecureRequest`.
- **Builder Pattern**: `MTP::Sender` provides a fluent API through `SpecificRequestBuilder` for configuring callbacks, target DCs, and error policies before dispatch.
- **Concurrency Model**: `ConcurrentSender` registers callbacks and forwards requests to `Instance`, which routes them to DC-specific threads running `Session` objects.
- **Resilience**: Automatic handling of migration errors, flood waits with exponential backoff, and configurable fail policies ensure robust network communication.
- **Source Locations**: Core logic resides in [`mtproto_auth_key.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtproto_auth_key.cpp) (encryption), [`sender.h`](https://github.com/telegramdesktop/tdesktop/blob/main/sender.h) (API), [`mtproto_concurrent_sender.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtproto_concurrent_sender.cpp) (concurrency), and [`mtp_instance.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtp_instance.cpp) (orchestration).

## Frequently Asked Questions

### What encryption algorithm does Telegram Desktop use for MTProto?

Telegram Desktop uses **AES-IGE** (Infinite Garble Extension) with 256-bit keys derived from the auth key. The implementation in [`mtproto_auth_key.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtproto_auth_key.cpp) generates per-message keys using SHA-256 mixes of the auth key and message payload, producing a 128-bit msg-key and 256-bit AES key/IV pairs for each transmission.

### How does the MTP::Sender handle network errors automatically?

The `Instance::Private::rpcErrorOccured` method in [`mtp_instance.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtp_instance.cpp) inspects error codes for patterns like `FILE_MIGRATE_X` or `FLOOD_WAIT_X`. Migration errors trigger immediate resend to the new DC, while flood errors schedule retries in `_delayedRequests` with delays doubling up to 60 seconds. The `FailSkipPolicy` setting on the builder determines whether the user's fail callback receives these errors or if internal handling applies.

### Where is the auth key stored in Telegram Desktop's source code?

The auth key is encapsulated in the `AuthKey` class defined in [`Telegram/SourceFiles/mtproto/mtproto_auth_key.h`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/mtproto/mtproto_auth_key.h) and implemented in [`mtproto_auth_key.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtproto_auth_key.cpp). The 256-bit secret lives in `AuthKey::Data`, while the 64-bit key ID (lower bits of SHA-1 hash) is computed by `AuthKey::countKeyId` and stored in `_keyId`.

### How are requests routed to the correct data center thread?

`Instance::Private::getThreadForDc` in [`mtp_instance.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/mtp_instance.cpp) maps DC IDs to specific `QThread` objects, distinguishing between main, download, and upload DCs using constants like `kBaseDownloadDcShift`. Once the thread is determined, `getSession` retrieves or creates the appropriate `Session` object, and the encrypted request is posted to that session's event queue for transmission.