End-to-End Encrypted Voice and Video Calls

This article describes the end-to-end encryption used for Telegram voice and video calls.

Related Articles

Establishing Calls

Before a call is ready, some preliminary actions have to be performed. The calling party needs to contact the party to be called and check whether it is ready to accept the call. Besides that, the parties have to negotiate the protocols to be used, learn the IP addresses of each other or of the Telegram relay servers to be used (so-called reflectors), and generate a one-time encryption key for this voice call with the aid of Diffie--Hellman key exchange. All of this is accomplished in parallel with the aid of several Telegram API methods and related notifications. This document covers details related to key generation, encryption and security.

Key Generation



  • A performs all the usual security checks on g_b and a, computes the Diffie--Hellman key key:=power(g_b,a) mod p and its fingerprint key_fingerprint:long, equal to the lower 64 bits of SHA1(key), the same as with secret chats. Then A invokes the phone.confirmCall method, containing g_a:bytes and key_fingerprint:long.

At this point, the Diffie--Hellman key exchange is complete, and both parties have a 256-byte shared secret key key which is used to encrypt all further exchanges between A and B.



This document describes encryption in voice and video calls as implemented in Telegram apps with versions 7.0 and above. See this document for details on encryption used in voice calls in app versions released before August 14, 2020.

The Telegram Voice and Video Call Library uses an optimized version of MTProto 2.0 to send and receive packets, consisting of one or more end-to-end encrypted messages of various types (ice candidates list, video formats, remote video status, audio stream data, video stream data, message ack or empty).

This document describes only the encryption process, leaving out encoding and network-dependent parts.

The library starts working with:

  • An encryption key key shared between the parties, as generated above.
  • Information whether the call is outgoing or incoming.
  • Two data transfer channels: signaling, offered by the Telegram API, and transport based on WebRTC.

Both data transfer channels are unreliable (messages may get lost), but signaling is slower and more reliable.

Encrypting Call Data

The body of a packet (decrypted_body) consists of several messages and their respective seq numbers concatenated together.

  • decrypted_body = message_seq1 + message_body1 + message_seq2 + message_body2

Each decrypted_body is unique because no two seq numbers of the first message can be the same. If only old messages need to be re-sent, an empty message with new unique seq is added to the packet first.

The encryption key key is used to compute a 128-bit msg_key and then a 256-bit aes_key and a 128-bit aes_iv:

  • msg_key_large = SHA256 (substr(key, 88+x, 32) + decrypted_body);
  • msg_key = substr (msg_key_large, 8, 16);
  • sha256_a = SHA256 (msg_key + substr (key, x, 36));
  • sha256_b = SHA256 (substr (key, 40+x, 36) + msg_key);
  • aes_iv = substr (sha256_b, 0, 4) + substr (sha256_a, 8, 8) + substr (sha256_b, 24, 4);

x depends on whether the call is outgoing or incoming and on the connection type:

  • x = 0 for outgoing + transport
  • x = 8 for incoming + transport
  • x = 128 for outgoing + signaling
  • x = 136 for incoming + signaling

This allows apps to decide which packet types will be sent to which connections and work in these connections independently (with each having its own seq counter).

The resulting aes_key and aes_iv are used to encrypt decrypted_body:

  • encrypted_body = AES_CTR (decrypted_body, aes_key, aes_iv)

The packet that gets sent consists of msg_key and encrypted_body:

  • packet_bytes = msg_key + encrypted_body

When received, the packet gets decrypted using key and msg_key, after which msg_key is checked against the relevant SHA256 substring. If the check fails, the packet must be discarded.

Protecting Against Replay Attacks

Each of the peers maintains its own 32-bit monotonically increasing counter for outgoing messages, seq, starting with 1. This seq counter is prepended to each sent message and increased by 1 for each new message. No two seq numbers of the first message in a packet can be the same. If only old messages need to be re-sent, an empty message with a new unique seq is added to the packet first. When the seq counter reaches 2^30, the call must be aborted. Each peer stores seq values of all the messages it has received (and processed) which are larger than max_received_seq - 64, where max_received_seq is the largest seq number received so far.

If a packet is received, the first message of which has a seq that is smaller or equal to max_received_seq - 64 or its seq had already been received, the message is discarded. Otherwise, the seq values of all incoming messages are memorized and max_received_seq is adjusted. This guarantees that no two packets will be processed twice.

Key Verification



  • A : computes key (g_b)^a




If some impostor is pretending to be either A or B and tries to perform a Man-in-the-Middle Attack on this Diffie--Hellman key exchange, the above still holds. Party A will generate a shared key with B -- or whoever pretends to be B -- without having a second chance to change its exponent a depending on the value g_b received from the other side; and the impostor will not have a chance to adapt his value of b depending on g_a, because it has to commit to a value of g_b before learning g_a. The same is valid for the key generation between the impostor and the party B.

