Perfect Forward Secrecy

Telegram supports Perfect Forward Secrecy (PFS) in Secret Chats as of Layer 20. See updating to new layers.

In order to keep past communications safe, official Telegram clients will initiate re-keying once a key has been used to decrypt and encrypt more than 100 messages, or has been in use for more than one week, provided the key has been used to encrypt at least one message. Old keys are then securely discarded and cannot be reconstructed, even with access to the new keys currently in use.

Any client participating in a Secret Chat can initiate re-keying as soon as it perceives that the current key has been used for too long or for encrypting too many messages. Please note that you should never initiate a new instance of the re-keying protocol if an uncompleted instance exists, initiated by either party.

Note: third-party developers are required to maintain the same level of security. All clients with secret chat support must be able to initiate re-keying and accept relevant service messages. See Security Guidelines.

Re-keying protocol

New keys are generated by exchanging special messages, using previously established keys for encryption. The re-keying protocol between parties A and B normally consists of four steps:

1. decryptedMessageActionRequestKey

A (re-keying initiator) generates a new value of a, subject to the same limitations as for the initial Diffie-Hellman key exchange, and sends the value of pow(g,a) to B, embedded in a decryptedMessageService:

decryptedMessageActionRequestKey exchange_id:long g_a:string = DecryptedMessageAction;
  • exchange_id is a random number identifying this instance of the Re-Keying Protocol for both parties
  • g_a is the value of pow(g,a) mod p

Note that the same Diffie--Hellman parameters (p,g) as for the initial Diffie--Hellman key exchange in this secret chat are used. They do not need to be re-transmitted explicitly.

2. decryptedMessageActionAcceptKey

Upon receipt of the above service message, B checks its content, and generates a response with same exchange_id, for a newly generated value of b:

decryptedMessageActionAcceptKey exchange_id:long g_b:string key_fingerprint:long = DecryptedMessageAction;
  • exchange_id is the same as in the received decryptedMessageActionRequestKey
  • g_b is the value of pow(g,b) mod p
  • key_fingerprint is the 64-bit fingerprint of the newly generated key = pow(g_a, b) mod p, used as a sanity check of the implementation

At this stage, B can already compute the new key key = pow(g_a, b) mod p and its key_fingerprint (last 64 bits of its SHA-1). However, it continues using the previous key until the completion of the exchange.

Once side B sends decryptedMessageActionAcceptKey, it cannot abort the key exchange; it must be ready to switch to the new key immediately after a decryptedMessageActionCommitKey is received. Therefore, if side B wishes to delay the usage of new key, for example in order to fill some seq_no gaps first, it must delay the decryptedMessageActionAcceptKey answer accordingly.

3. decryptedMessageActionCommitKey

Once A receives a valid decryptedMessageActionAcceptKey, it performs all necessary checks, and "commits" the new key by means of the following service message:

decryptedMessageActionCommitKey exchange_id:long key_fingerprint:long = DecryptedMessageAction;
  • exchange_id is the same as in the two previous messages
  • key_fingerprint is the value of the hash (last 64 bits of SHA-1) of the new key computed by A, for implementation sanity check

After that, A can (and must) encrypt all following messages with the new key.

If side A wishes to delay installation of the new key, for example because there are some seq_no gaps that it wants to fill first, it must delay decryptedMessageActionCommitKey answer accordingly.

4. Final step

When B receives either a decryptedMessageActionCommitKey or a message encrypted by the new key, recognized by the value of key_fingerprint prepended to the encrypted message (it may happen that the decryptedMessageActionCommitKey has been lost and will be re-requested later), it assumes that A has started using the new key for encryption, and does the same.

However, the previous key may be kept until there are no gaps in received messages up to the switch to the new key. Once all the gaps have been filled, the old key must be securely discarded.

There is one exception to this rule — the SHA-1 of the original key (generated during the establishment of Secret Chat in question) is always stored, in order to show key visualizations on the clients.

Aborting protocol

Any of the parties may abort any instance of an uncompleted re-keying protocol, unless decryptedMessageActionCommitKey or decryptedMessageActionAcceptKey has been already sent by the party in question. In order to abort re-keying, send

decryptedMessageActionAbortKey exchange_id:long = DecryptedMessageAction;

This could be done, for example, if the party is already participating in a different instance of the re-keying protocol, or if the received values of g_a, g_b and other parameters do not pass security checks. In the latter case, it might be advisable to abort the Secret Chat altogether.

Discarding Previous Keys

Once B receives decryptedMessageActionCommitKey, it can safely discard the previous key provided there are no gaps. However, A may only discard the previous key after a message encrypted with the new key has been received. If no ordinary messages are scheduled to be sent, a special no-op message should sent by B for this purpose:

decryptedMessageActionNoop = DecryptedMessageAction;

Concurrent Re-Keying

It may happen that both parties concurrently initiate re-keying by sending decryptedMessageActionRequestKey without knowing that the other party has already done so. If each side aborts re-keying because it is already participating in another instance of the protocol initiated by itself, the re-keying will never happen.

Because of this possibility, we suggest that only the instance with the smaller exchange_id is aborted, with the option to re-use its (a,g_a) for the re-keying protocol instance with the larger exchange_id (when compared as a long, i.e. signed little-endian 64-bit integer).

In other words, if a decryptedMessageActionRequestKey is received after A has sent its decryptedMessageActionRequestKey, but has not yet received decryptedMessageActionAcceptKey, the following is to be done:

  • if exchange_id in the sent decryptedMessageActionRequestKey was larger than that in the decryptionActionRequestKey just received, abort the newly-suggested re-keying protocol instance without sending explicit decryptedMessageActionAbortKey (the other side will do the same according to the next rule).
  • if exchange_id in our decryptedMessageActionRequestKey was smaller, respond to the newly-received decryptedMessageActionRequestKey with a decryptedMessageActionAcceptKey, and participate only in the re-keying protocol instance initiated by the other side. It is possible to re-use at this stage the value of g_a (now called g_b) that was generated for the original decryptedMessageActionRequestKey, now abandoned, or totally new (b,g_b) can be generated.
  • in the unlikely (2^{-64}) case both exchange_id are equal, abort both instances without sending an explicit decryptedMessageActionAbortKey. The other side will do the same.
Key Visualization

Since all re-keying instances are carried over the secure channel established when the secret chat is created, it is necessary for the user to confirm that no MITM attack had taken place during the initial exchange. The key visualization on the clients uses the first 128-bits of the SHA-1 of the original key created when the Secret Chat was first established, followed by the first 160 bits of the SHA-256 of the key in use when the secret chat was updated to layer 46 (coincides with the original key if chat was created using layer 46).

Please note that the key_fingerprint parameter was introduced as a maintenance tool (with a misleading name) and is not related to key visualization on the clients.