Добавляем E2E шифрование в чат

Добавляем E2E шифрование в чат

Привет, хабр! В этой статье на примере простого чата реализуем E2E шифрование.

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

Немного теории

Эта статья нацелена на простое понимание и применение на практике. На Хабре полно материалов, которые отлично и досконально разбирают все математические аспекты криптографии. Мы же сосредоточимся на инженерной реализации, но для старта нам необходимо зафиксировать три базовых понятия:

Симметричное шифрование — это метод, при котором для шифрования и расшифровки данных используется один и тот же секретный ключ. Он работает быстро и идеально подходит для защиты больших объемов данных. Главная проблема: как безопасно передать этот ключ собеседнику через интернет, чтобы его никто не перехватил?

Симметричное шифрование — это метод, при котором для шифрования и расшифровки данных используется один и тот же секретный ключ. Он работает быстро и идеально подходит для защиты больших объемов данных. Главная проблема: как безопасно передать этот ключ собеседнику через интернет, чтобы его никто не перехватил?

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

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

Шифрование на эллиптических кривых (ECC / ECDH) — это современный подвид асимметричной криптографии, основанный на сложной математике точек на изогнутой кривой. У него есть два киллер-фичи: он работает в разы быстрее старого RSA, а его ключи гораздо короче при той же степени защиты (всего 32 байта для X25519 против огромных 4096 бит у RSA). Мы используем протокол ECDH (Elliptic Curve Diffie-Hellman): он позволяет двум людям, зная публичные ключи друг друга, за одну математическую операцию вычислить один и тот же общий секрет, вообще не передавая его по сети.

Шифрование на эллиптических кривых (ECC / ECDH) — это современный подвид асимметричной криптографии, основанный на сложной математике точек на изогнутой кривой. У него есть два киллер-фичи: он работает в разы быстрее старого RSA, а его ключи гораздо короче при той же степени защиты (всего 32 байта для X25519 против огромных 4096 бит у RSA). Мы используем протокол ECDH (Elliptic Curve Diffie-Hellman): он позволяет двум людям, зная публичные ключи друг друга, за одну математическую операцию вычислить один и тот же общий секрет, вообще не передавая его по сети.

Весь процесс простым языком:

Клиент генерирует AES (Advanced Encryption Standard) ключ (для симметричного шифрования), приватный и публичный ECDH (Elliptic Curve Diffie-Hellman) ключи.

Клиент генерирует AES (Advanced Encryption Standard) ключ (для симметричного шифрования), приватный и публичный ECDH (Elliptic Curve Diffie-Hellman) ключи.

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

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

С помощью полученного мастер-ключа каждый шифрует свой AES ключ и передает собеседнику.

С помощью полученного мастер-ключа каждый шифрует свой AES ключ и передает собеседнику.

После обмена симметричными ключами каждый расшифровывает ключ собеседника.

После обмена симметричными ключами каждый расшифровывает ключ собеседника.

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

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

Реализуем клиент

В прошлый раз мы ограничились реализацией серверной стороны, а в качестве клиента взяли telnet. Сейчас же нам нужно проработать и логику клиента.

Делаем структуру клиента:

Подключаемся к хосту:

Теперь дополним структуру нашего клиента для хранения собственных ключей и ключей собеседников из чата:

Деалем хэш-тиблицу, в которой ключ — адрес пользователя, значение мастер-ключ для обмена симметричными ключами между юзерами и сам симетричный ключ пользователя.

Далее прорабатываем все криптографические функции.

Генерация общего мастер-ключа:

Шифруем наш симметричный ключ:

Зачем нужен GCM?

Режим GCM — это аутентифицированное шифрование (AEAD).

Алгоритм делает две вещи:

Шифрует aesKey с использованием ключа и твоего случайного nonce.

Шифрует aesKey с использованием ключа и твоего случайного nonce.

Берет получившийся шифротекст, берет этот же самый nonce, смешивает их с секретным ключом и создает в самом конце 16-байтный тег подлинности (Auth Tag).

Берет получившийся шифротекст, берет этот же самый nonce, смешивает их с секретным ключом и создает в самом конце 16-байтный тег подлинности (Auth Tag).

Получается структура: [ 12 байт Nonce ] + [ Зашифрованный ключ ] + [ 16 байт Тега ]

Таким образом зашифрованный текст связывается с тегом подлинности, защищая пакет от модификации в пути.

Дешифруем переданный нам ключ собеседника:

Шифруем и дешифруем сообщение:

Чтобы реализовать контракт обмена между пользователями нам понадобятся следующие структуры:

TypeAuth ("AUTH") — отправляется клиентом при первом подключении; содержит его публичный ECDH-ключ для регистрации на сервере.

TypeAuth ("AUTH") — отправляется клиентом при первом подключении; содержит его публичный ECDH-ключ для регистрации на сервере.

TypePeerList ("PEER_LIST") — отправляется сервером лично новичку в ответ на авторизацию; содержит список адресов и публичных ключей всех участников, которые уже находятся в чате.

TypePeerList ("PEER_LIST") — отправляется сервером лично новичку в ответ на авторизацию; содержит список адресов и публичных ключей всех участников, которые уже находятся в чате.

TypeNewPeer ("NEW_PEER") — рассылается сервером (бродкастом) всем «старичкам» в чате; уведомляет их о подключении нового пользователя и передает его публичный ключ.

TypeNewPeer ("NEW_PEER") — рассылается сервером (бродкастом) всем «старичкам» в чате; уведомляет их о подключении нового пользователя и передает его публичный ключ.

TypeGetPeerList ("GET_PEER_LIST") — сервисный запрос от клиента к серверу с требованием принудительно обновить список участников (используется как фолбек, если таблица топологии не синхронизирована).

TypeGetPeerList ("GET_PEER_LIST") — сервисный запрос от клиента к серверу с требованием принудительно обновить список участников (используется как фолбек, если таблица топологии не синхронизирована).

TypeSenderKey ("SENDER_KEY") — отправляется между клиентами через сервер; содержит собственный симметричный AES-ключ отправителя, зашифрованный общим секретом ECDH для конкретного получателя.

TypeSenderKey ("SENDER_KEY") — отправляется между клиентами через сервер; содержит собственный симметричный AES-ключ отправителя, зашифрованный общим секретом ECDH для конкретного получателя.

TypeChatMsg ("CHAT_MSG") — пакет с обычным текстовым сообщением; само содержимое (Message) зашифровано AES-ключом отправителя, а сервер просто пересылает его остальным участникам.

TypeChatMsg ("CHAT_MSG") — пакет с обычным текстовым сообщением; само содержимое (Message) зашифровано AES-ключом отправителя, а сервер просто пересылает его остальным участникам.

Системный пакет для генерации пакетов:

Генерируем 3 ключа клиента:

Возвращаемся к логике клиента:

Читаем поток пакетов, которые передает сервер.

Разберем обработку полученного пакета по каждому кейсу отдельно:

Сервер сигнализирует, что подключился новый пользователь. Генерируем мастер-ключ с помощью переданного публичного ключа нового юзера и собственного приватного ключа данного пользователя. Генерирует ответный пакет для нового пользователя с своим зашифрованным AES ключем, сохраняет мастер ключ в свою хэш-таблицу:

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

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

И последний тип пакета, который обрабатывает клиент — пул публичных ключей участников чата, который передает сам сервер.

Мелкие системные методы:

enterToChat() (Вход в чат) — стартовый метод рукопожатия. Формирует приветственный пакет, упаковывает в него наш публичный ECDH-ключ и отправляет на сервер, заявляя о своем присутствии в сети.

enterToChat() (Вход в чат) — стартовый метод рукопожатия. Формирует приветственный пакет, упаковывает в него наш публичный ECDH-ключ и отправляет на сервер, заявляя о своем присутствии в сети.

createTopologyTable(peers) (Инициализация таблицы связей) — вызывается новичком при получении списка участников от сервера. Метод итерируется по всем «старичкам», перемножает наш приватный ECDH-ключ с их публичными ключами, вычисляет уникальные общие секреты (shared secrets) для каждой пары и сохраняет их в TopologyTable.

createTopologyTable(peers) (Инициализация таблицы связей) — вызывается новичком при получении списка участников от сервера. Метод итерируется по всем «старичкам», перемножает наш приватный ECDH-ключ с их публичными ключами, вычисляет уникальные общие секреты (shared secrets) для каждой пары и сохраняет их в TopologyTable.

BroadcastAES() (Рассылка ключа чата) — отправляет наш главный сессионный AES-ключ всем участникам. Метод проходит по таблице связей, шифрует наш AESKey уникальным общим секретом (SecretECDH) отдельно для каждого пира и отправляет персональные пакеты TypeSenderKey.

BroadcastAES() (Рассылка ключа чата) — отправляет наш главный сессионный AES-ключ всем участникам. Метод проходит по таблице связей, шифрует наш AESKey уникальным общим секретом (SecretECDH) отдельно для каждого пира и отправляет персональные пакеты TypeSenderKey.

getP2PKeys(user) (Получение крипто-ключей собеседника) — внутренний менеджер ключей. Он достает сохраненные ключи (ECDH-секрет и AES-ключ) для конкретного пользователя. Если пользователя почему-то нет в таблице, метод делает фолбек-запрос к серверу (getTopologyTable) для обновления списка.

getP2PKeys(user) (Получение крипто-ключей собеседника) — внутренний менеджер ключей. Он достает сохраненные ключи (ECDH-секрет и AES-ключ) для конкретного пользователя. Если пользователя почему-то нет в таблице, метод делает фолбек-запрос к серверу (getTopologyTable) для обновления списка.

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

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

sendMessage(packet) (Низкоуровневая отправка по TCP) — транспортный узел клиента. Сериализует любой готовый пакет message.Packet в JSON-байты, принудительно добавляет в конец символ переноса строки \n (чтобы bufio.Scanner на стороне сервера четко понимал, где заканчивается пакет) и пуляет данные в сокет.

sendMessage(packet) (Низкоуровневая отправка по TCP) — транспортный узел клиента. Сериализует любой готовый пакет message.Packet в JSON-байты, принудительно добавляет в конец символ переноса строки \n (чтобы bufio.Scanner на стороне сервера четко понимал, где заканчивается пакет) и пуляет данные в сокет.

addECDH(user, ecdhKey) (Сохранение ECDH-секрета) — безопасный хелпер для атомарного обновления таблицы. Находит структуру пира в мапе, аккуратно записывает туда вычисленный асимметричный секрет и сохраняет измененную копию обратно в TopologyTable.

addECDH(user, ecdhKey) (Сохранение ECDH-секрета) — безопасный хелпер для атомарного обновления таблицы. Находит структуру пира в мапе, аккуратно записывает туда вычисленный асимметричный секрет и сохраняет измененную копию обратно в TopologyTable.

addAES(user, aesKey) (Сохранение AES-ключа собеседника) — финальный шаг крипто-настройки. Вызывается, когда нам прилетает расшифрованный AES-ключ от соседа. Находит нужного пользователя в мапе, «впаивает» его симметричный ключ в поле .AES и перезаписывает структуру в мапе, подготавливая клиента к чтению сообщений от этого юзера.

addAES(user, aesKey) (Сохранение AES-ключа собеседника) — финальный шаг крипто-настройки. Вызывается, когда нам прилетает расшифрованный AES-ключ от соседа. Находит нужного пользователя в мапе, «впаивает» его симметричный ключ в поле .AES и перезаписывает структуру в мапе, подготавливая клиента к чтению сообщений от этого юзера.

Дорабатываем сервер

Теперь нам нужно проработать логику сервера, чтобы он корректно хранил публичные ключи пользователей, обрабатывал пакеты и поддерживал рукопожатие.

Структура сервера:

В структуру пользователя добавили его публичный ключ.

В структуру пользователя добавили его публичный ключ.

канал теперь будет передавать пакеты.

канал теперь будет передавать пакеты.

Добавляем рукопожатие и переносим в него регистрацию пользователя:

В рукопожатии ждем первый пакет авторизации от нового кользопателя. Регистрируем его на сервере вместо с публичным ключем. Передаем пакет с типом TypeNewPeer. Передаем его в обработку пакетов. Далее создаем пакет со списком всех участников чата и их публичных ключей для нового пользователя.

Генерируем пакет со списком публичных ключей:

Обработка полученных пакетов:

Если пакет с публичным ключем нового пользователя или текстовое сообщение, то отправляем его на широковещательную рассылку:

Если пакет нацелен на конкретного пользователя (передается список публичных ключей участников чата или передается зашифрованный симметричный ключ), тогда шлем напрямую:

Запрос пользователя на принудительную отправку пулы публичных ключей:

Процесс шифрования готов! Добавим на сторону сервера попытку прочитать пакет с сообщением:

Подключаемся к серверу с двух клиентов и отправляем сообщение:

Успешно читаем на другом клиенте:

Что видит сервер (мешанину из байт):

Определенный успех для показательного примера внедрения End to End шифрования.

Мы успешно реализовали честное End-to-End шифрование для нашего TCP-чата. Да, наш сервер всё еще координирует сеть и пересылает пакеты (выполняет роль сигнального сервера и роутера), но он больше не имеет доступа к самим сообщениям. Как видно из логов, попытка сервера десериализовать сообщение падает с ошибкой — для него это просто массив случайных байт.

Мы построили гибридную схему:

ECDH на кривой X25519 безопасно связал клиентов и помог им выработать общий секрет без риска перехвата.

ECDH на кривой X25519 безопасно связал клиентов и помог им выработать общий секрет без риска перехвата.

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

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

Наш чат стал приватным, но пока не стал по-настоящему надежным и безопасным от более изощренных атак. В следующих статьях мы разберем:

Защиту от MITM (Man-in-the-Middle): Сервер не может прочитать сообщения, но что мешает вредоносному серверу подменить публичные ключи участников в момент рукопожатия и читать всё? Будем решать проблему доверия к ключам.

Защиту от MITM (Man-in-the-Middle): Сервер не может прочитать сообщения, но что мешает вредоносному серверу подменить публичные ключи участников в момент рукопожатия и читать всё? Будем решать проблему доверия к ключам.

Аутентификацию пользователей: Добавим полноценную сессию, подписи пакетов и разберемся, как привязать постоянный профиль пользователя к его временным сессионным ключам.

Аутентификацию пользователей: Добавим полноценную сессию, подписи пакетов и разберемся, как привязать постоянный профиль пользователя к его временным сессионным ключам.

← Cybersecurity