How Telegram Desktop Implements MTProto Encryption and the MTP Sender Pipeline
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, the countKeyId method performs this calculation:
// 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 mixes the auth key with the payload hash:
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 bind the derived keys to these raw routines:
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. 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:
// 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 provides type-safe request builders through two entry points:
request(Request &&)returns a SpecificRequestBuilder for new requestsrequest(mtpRequestId)returns aSentRequestWrapfor canceling existing requests
The builder supports method chaining for configuration:
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:
// 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
// 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):
// 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:
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:
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:
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
AuthKeyclass manages 256-bit secrets and derives per-message AES-IGE keys using SHA-256, with encryption performed via OpenSSL's IGE mode inSessionPrivate::sendSecureRequest. - Builder Pattern:
MTP::Senderprovides a fluent API throughSpecificRequestBuilderfor configuring callbacks, target DCs, and error policies before dispatch. - Concurrency Model:
ConcurrentSenderregisters callbacks and forwards requests toInstance, which routes them to DC-specific threads runningSessionobjects. - 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(encryption),sender.h(API),mtproto_concurrent_sender.cpp(concurrency), andmtp_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 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 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 and implemented in 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →