Svoboda | Graniru | BBC Russia | Golosameriki | Facebook

Защищенные чаты, шифрование end-to-end

Защищенный чат — непубличный чат, общение в котором ведется с помощью сообщений, зашифрованных ключом, который есть только у его участников. Из определения следует, что никакие третьи лица, не смогут получить доступ к расшифрованному содержимому без доступа к одному из устройств.
Новая сущность, появившаяся в восьмом слое API.

Генерация ключа

При генерации ключа используется протокол Диффи-Хеллмана. Подробнее см. Wikipedia

Рассмотрим ситуацию: пользователь A хочет начать защищенное общение с пользователем B.

Отправка запроса

Пользователь A выполняет метод messages.getDhConfig для получения конфигурационных параметров метода Диффи-Хеллмана: prime, primitive root.

Важно выполнять этот метод перед каждой новой процедурой генерации ключа. Имеет смысл кешировать значения параметров вместе с версией, чтобы не переполучать значения целиком каждый раз. Если версия, хранимая клиентом, по-прежнему актуальна, сервер вернет конструктор messages.dhConfigNotModified.

В случае, если на клиенте недостаточно хороший генератор случайных чисел, имеет смысл передавать параметр random_length > 0, чтобы сервер сгенерировал свою случайную последовательность random соответствующей длины.
Важно: использовать серверную случайную последовательность в чистом виде может быть небезопасно. Обязательно необходимо подмешивать клиентскую, например, сгененерировать своё случайное число той же длины client_random и использовать final_random := random XOR client_random

Клиент A вычисляет случайное 2048-битное число a (используя достаточно много энтропии или серверный random, см. выше), и выполняет метод messages.requestEncryption, передав туда g_a := pow(g, a) mod dh_prime.

Пользователю B на все привязанные авторизационные ключи (все авторизованные устройства) приходит обновление updateEncryption с конструктором чата encryptedChatRequested. Необходимо отобразить пользователю базовую информацию о пользователе A и предложить одобрить или отклонить запрос.

Подтверждение запроса

После того, как пользователь B в интерфейсе клиента подтверждает создание защищенного чата с A, клиент B также получает актуальные конфигурационные параметры метода Диффи-Хеллмана. После этого он генерирует случайное 2048-битное число b по правилам, аналогичным a.

Имея g_a, nonce полученные из обновления с encryptedChatRequested, он может сразу сгенерировать итоговый общий ключ: key = (pow(g_a, b) mod dh_prime) xor nonce.
Его отпечаток key_fingerprint равен младшим 64 битам SHA1 (key).

Клиент B выполняет метод messages.acceptEncryption, передав туда g_b := pow(g, b) mod dh_prime и key_fingerprint.

На все авторизованные устройства клиента B, кроме текущего, будут отправлены обновления updateEncryption с конструктором encryptedChatDiscarded. После этого с этим зашифрованным чатом сможет работать только устройство B, с которого был совершен вызов messages.acceptEncryption.

Пользователю A на авторизационный ключ, с которого был инициирован чат, приходит обновление updateEncryption с конструктором encryptedChat.

Клиент A, имея g_b, nonce из обновления, может также получить общий ключ key = (pow(g_b, a) mod dh_prime) xor nonce. Если отпечаток полученного ключа совпадает с переданным в encryptedChat, можно начать отправку и обработку входящих сообщений. В противном случае обязательно необходимо выполнить messages.discardEncryption и оповестить пользователя.

Отправка и получение сообщений внутри защищенного чата

Сериализация и шифрование исходящего сообщения

Формируется TL-объект типа DecryptedMessage, содержащий сообщение в открытом виде. Для обратной совместимости объект необходимо оборачивать в конструктор decryptedMessageLayer с указанием поддерживаемого слоя (начиная с 8).
Полученная конструкция сериализуется в массив байтов по общим правилам TL. В начало полученного массива дописывается 4 байта длины массива, не включая эти 4 байта.
Вычисляется ключ сообщения msg_key, как младшие 128 бит от SHA1 полученных на предыдущем шаге данных.
Массив байт дополняется случайными данными до кратности длины 16 байта.
Вычисляется AES ключ и инициализационный вектор ( key — общий ключ, полученный на этапе Генерация ключа, x = 0 ):

  • sha1_a = SHA1 (msg_key + substr (key, x, 32));
  • sha1_b = SHA1 (substr (key, 32+x, 16) + msg_key + substr (key, 48+x, 16));
  • sha1_с = SHA1 (substr (key, 64+x, 32) + msg_key);
  • sha1_d = SHA1 (msg_key + substr (key, 96+x, 32));
  • aes_key = substr (sha1_a, 0, 8) + substr (sha1_b, 8, 12) + substr (sha1_c, 4, 12);
  • aes_iv = substr (sha1_a, 8, 12) + substr (sha1_b, 0, 8) + substr (sha1_c, 16, 4) + substr (sha1_d, 0, 8);

Данные шифруются 128-битным ключом aes_key и 128-битным инициализионным вектором aes_iv, посредством шифра AES-256 в сцепленном режиме (IGE). В начало полученного массива байт дописывается ключ сообщения msg_key.

Расшифровка входящего сообщения

В обратном порядке выполняются действия, приведенные выше.
При получении зашифрованного сообщения надо обязательно проверить, что msg_key действительно равен младшим 128 битам SHA1 от расшифрованного сообщения.
В случае, если слой сообщения больше поддерживаемого клиентом, необходимо сообщить пользователю об устаревшей версии клиента и предложить обновиться.

Отправка зашифрованных файлов

Все файлы, отправляемые в защищенные чаты шифруются одноразовыми ключами, никак не связанными с общим ключом чата. Под отправкой зашифрованного файла подразумевается прикрепление адреса зашифрованного файла снаружи к зашифрованному сообщению, с помощью параметра file метода messages.sendEncryptedFile, а также передача ключа для непосредственной расшифровки внутри тела сообщения (параметр key в конструкторах decryptedMessageMediaPhoto, decryptedMessageMediaVideo и decryptedMessageMediaFile.

Перед началом отправки файла в защищенный чат вычисляется 2 случайных 256-битных числа, которые будут являться AES ключом и инициализационным вектором, с помощью которых файл шифруется. Аналогично, используется шифр AES-256 в сцепленном режиме (IGE).

Отпечаток ключа вычисляется следующим образом:

  • digest = md5(key + iv)
  • fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)

Зашифрованное содержимое файла сохраняется на сервере аналогично незашифрованному, кусками с помощью вызовов upload.saveFilePart.
Последующий вызов messages.sendEncryptedFile присвоит сохраненному файлу идентификатор и отправит адрес вместе с сообщением. Получателю придет обновление с encryptedMessage, в параметр file содержит информацию о файле.

Полученные и отправленные зашифрованные файлы можно пересылать в другие защищенные чаты с помощью конструктора inputEncryptedFile, чтобы дважды не сохранять одно и то же содержимое на сервер.

Работа с ящиком обновлений

Защищенные чаты привязаны не к пользователям, а к конкретным устройствам (а точней, к авторизационным ключам). Обычный ящик сообщений, который использует pts, как описание состояния клиента, не годится, поскольку предзначен для длительного хранения сообщений и доступа к ним с разных устройств.

Для решения проблемы вводится дополнительная временная очередь обновлений. При отправке обновления о сообщении из защищенного чата, передается новое значение qts, который позволяет восстановить разницу при длительном отсутствии соединения или потере обновления.

С увеличением количества событий, значение qts монотонно возрастает (не всегда на 1). Начальное значение может (и будет) не равняться 0.

Факт получения и сохранения событий из временной очереди клиентом подтверждается явно вызовом метода messages.receivedQueue или неявно запросом updates.getDifference (значение переданного qts, а не итовое состояние). Все сообщения, факт доставки которых клиент подтвердил, а также сообщения старше 7 дней, могут быть (и будут) удалены с сервера.

При разавторизации очередь событий соответствующего устройства будет принудительно очищена, значение qts станет неактуальным.