В этом посте я вкратце расскажу о внутреннем устройстве iMessage, изученном мной в процессе работы над pypush
— опенсорсным проектом воссоздания реализации iMessage.
Ради краткости и понятности я не буду вдаваться в технические подробности. Если вы хотите узнать, как конкретно всё реализовано, то изучите репозиторий pypush
.
Фундаментальный слой
Один из самых фундаментальных компонентов iMessage — это Apple Push Notification Service (APNs). Возможно, вы сталкивались с ним раньше, ведь это тот же сервис, который используется приложениями в App Store для получения уведомлений и обновлений в реальном времени, даже когда приложение закрыто.
Чего вы могли не знать о APN, так это то, что он двусторонний. Именно так: APNs можно использовать для отправки push-уведомлений, а также для их получения. Наверно, вы уже догадались, к чему всё идёт?
После подключения устройства к APNs оно получает «push-токен». Этот токен можно использовать для направления уведомлений на это конкретное устройство.
Примечание: технически этот токен отличается от токена, получаемого при использовании API application:didRegisterForRemoteNotificationsWithDeviceToken:
. Область действия этого токена ограничена одним приложением, и он запрашивается при помощи bundle ID приложения. Однако, по сути он используется с той же целью.
При отправке push-уведомлений в устройство необходимо также указать топик (topic) для которого отправляется сообщение. Обычно он выглядит как Bundle ID, и для iMessage это com.apple.madrid
. Когда устройство подключается к APNs, оно отправляет сообщение-фильтр, сообщая серверу, какие сообщения хочет получать.
Примечание: сервер APNs также называется APNs Courier. Сообщение-фильтр содержит множество списков топиков для каждого из возможных состояний. Оно может переключить топик в состояние enabled
, opertunistic
, paused
, или disabled
APNs используется не только для доставки сообщений iMessage. При помощи слоя псевдо-HTTP поверх APNs через этот сервис могут отправлять запросы и получать ответы IDS (о которых мы поговорим чуть ниже).
Стоит также заметить, что для подключения к APNs необходим клиентский сертификат, выпущенный сервером активации Albert.
Сервер ключей
Следующий фрагмент этого пазла — это IDS. Насколько я понял, это расшифровывается как IDentity Services, хотя и не смог найти этому официального подтверждения.
Примечание: также это может называться ESS. Это сбивает с толку, потому что топик APNs, который использует FaceTime, называется com.apple.ess
. Но не будем на этом останавливаться…
IDS используется как сервер ключей для iMessage, а также для некоторых других сервисов наподобие FaceTime. Стоит помнить, что iMessage имеет шифрование E2E, поэтому публичными ключами участники должны обмениваться защищённым образом.
Первый этап регистрации для IDS — это получение токена аутентификации. Для этого необходимо предоставить API имя пользователя и пароль Apple ID.
Примечание: так как 2FA теперь стал стандартом, его необходимо было внедрить в IDS API. Для этого существует две опции: легаси-опция, при которой код 2FA напрямую прикрепляется к паролю, и опция «GrandSlam». При использовании опции GrandSlam для подтверждения того, что пользователь остаётся тем же устройством и не требует повторного ввода кода 2FA, применяются «Anisette data». Затем пользователь получает Password Equivalent Token (PET), который можно использовать, как будто это пароль + код 2FA.
После получения токена аутентификации его нужно немедленно обменять на сертификат аутентификации с более долгим сроком службы. Этот сертификат позволяет зарегистрироваться в IDS, но его недостаточно для выполнения поиска ключей.
Наверно самый важный этап процесса настройки IDS — это регистрация. Это процесс загрузки на сервер ключей публичных ключей шифрования и подписания, а также других «клиентских данных» о том, какие функции поддерживает устройство.
При совершении запроса регистрации IDS требуется двоичный блоб, называющийся «validation data». По сути, это механизм верификации Apple, гарантирующий, что iMessage не могут пользоваться устройства, изготовленные не Apple.
Предупреждение: для генерации «validation data» используется такая информация об устройстве, как его серийный номер, модель и UUID диска. Это значит, что не все validation data могут обрабатываться эквивалентно: как и в случае с Hackintosh, возраст аккаунта и «score» определяют, можно ли использовать недопустимый серийный код, или же вы получите ошибку «customer code».
Примечание: двоичный файл, генерирующий эти «validation data» сильно обфусцирован. pypush
обходит эту проблему благодаря использованию собственного загрузчика mach-o и Unicorn Engine для эмуляции обфусцированного двоичного файла. Кроме того, pypush
объединяет свойства устройства (например, серийный номер) в файл data.plist, который передаёт эмулируемому двоичному файлу.
После регистрации IDS устройство получает «пару ключей идентификации». Эту пару ключей в дальнейшем можно использовать для выполнения поиска публичных ключей.
При выполнении поиска пользователь передаёт аккаунт(-ы), которые нужно искать, и получает список «идентификаций». Каждая из этих идентификаций соответствует устройству, зарегистрированному на аккаунт, и включает в себя такие важные подробности, как публичный ключи, push-токен и токен сессии.
Предупреждение: токены сессии необходимы для отправки сообщений в устройство. По сути, они доказывают, что пользователь недавно выполнил поиск, потому что токен сессии имеет срок службы. Токены сессий нельзя передавать, потому что их можно использовать только тем аккаунтом, который выполнил запрос поиска.
Шифрование сообщений
Итак, мы подготовили основы iMessage. Этого достаточно для того, чтобы поискать публичные ключи другого пользователя, а также публиковать собственные. Теперь всё это нужно объединить с APNs для отправки и получения сообщений!
Для получения сообщений достаточно просто отфильтровать соединение APNs с com.apple.madrid
и отправить пакет активного состояния.
В зависимости от того, о каких функциях сообщило устройство при регистрации IDS, а также от версии iOS отправляющего устройства, оно может получать сообщения в легаси-формате шифрования pair
(до iOS 13), или в новом формате pair-ec
. Хотя формат pair
гораздо подробнее задокументирован и проще в реализации, он не предоставляет прямую секретность при помощи «pre-keys» (аналогично Signal), как новый формат pair-ec
.
Затем можно расшифровать сообщение согласно описаниям во множестве статей и тому, как это реализовано в pypush
. Верификация сигнатуры сообщения необязательна, но очевидно важна, если вы хотите, чтобы клиент был защищён.
Отправка сообщений практически обратна их получению. Помните, что можно выбрать отдельную отправку сообщений каждому получателю или объединить всех получателей и их зашифрованные полезные нагрузки в огромный пакет, который потом разделит APNs.
Примечание: стоит также помнить, что это сообщение доставляется всем участникам беседы, в том числе другим устройствам под вашим аккаунтом.
И ещё при отправке сообщений не нужно забывать, что ключ AES не полностью случаен: он помечается при помощи HMAC. Если вы используете совершенно случайный ключ AES, то сообщение не расшифруется на новых устройствах.
Вот, собственно, и всё! Как я и говорил, этот пост должен был дать вам общее представление о работе протокола iMessage, чтобы при желании вы исследовали код pypush
.
Ресурсы и ссылки
В понимании iMessage мне помогли многие люди и предыдущие работы. Вот краткий список: