Содержание статьи
При разработке клиент‑серверных приложений под Android есть несколько очевидных способов сделать соединение безопаснее. Кажется, что к 2020 году уже все выучили аббревиатуру HTTPS как мантру, да и Google со своей стороны помогает, запрещая по умолчанию HTTP-трафик в новых версиях ОС. Чуть более продвинутые товарищи знают, что сам по себе HTTPS защищает не от всех векторов атак (привет, Мэллори!), и накручивают SSL Pinning (aka Certificate/Public Key Pinning). Чаще всего защита канала на этом заканчивается. Да и честно говоря, в большинстве случаев этой защиты достаточно. Особенно если с помощью шифрования пользовательских данных и проверки на недоверенное окружение ликвидируются другие векторы атаки.
Но бывает и по‑другому. Приложение вынуждено работать в недоверенной среде, а это значит, что зловред на клиентском устройстве может перехватить токены доступа к серверу прямо из памяти приложения. Далее, в зависимости от реализации механизма инвалидации этих токенов, злоумышленник какое‑то время может выполнять запросы от лица пользователя. У этой проблемы есть решение — вешать цифровую подпись на все запросы, выполняющиеся из авторизованной зоны. Как правило, это все запросы, которые не /
или /
. О том, как реализовать подпись запросов на клиенте и на сервере, а также о подводных камнях и ограничениях этой техники поговорим в статье.
Криптоликбез
Чтобы сделать повествование более системным, давай для начала синхронизируемся в понятиях и освежим знания криптографии, если они по какой‑то причине заплесневели.
Начнем с понятия цифровая подпись. Тема ЦП довольно обширная, поэтому ограничимся асимметричной схемой цифровой подписи, в которой участвуют открытый и закрытый ключи. В самом простом случае цифровая подпись работает по следующему алгоритму:
- Алиса шифрует документ своим закрытым ключом, тем самым подписывая его.
- Алиса отправляет подписанный документ Бобу.
- Боб расшифровывает документ с помощью открытого ключа Алисы, тем самым проверяя подпись.
Это работает, но есть проблема. Если документ, подписанный Алисой, — чек на некоторую сумму денег, то неблагонадежный Боб сможет обналичивать этот чек, пока у Алисы не закончатся деньги на счете или пока Боба не поймают. Для борьбы с этой проблемой применяются метки времени. Алиса добавляет к документу текущее время и шифрует его вместе с документом. Банк, в который Боб приносит этот чек и открытый ключ Алисы, расшифровывает документ и сохраняет метку времени. Теперь при попытке обналичить такой чек повторно банк заблокирует эту операцию, так как метки времени будут одинаковые.
Еще не заскучал? Потерпи, это все нам пригодится уже скоро, когда будем писать реализацию. Финальный аспект, который хочется обсудить, — производительность асимметричных криптосистем. Они оказываются довольно неэффективны на больших массивах данных, а значит, попытка применить этот подход для подписи объемных запросов будет нещадно жрать батарею смартфона и замедлять общение с сервером. Для ускорения всей этой машинерии принято использовать односторонние хеш‑функции. Итоговая версия алгоритма будет выглядеть так:
- Алиса вычисляет значение хеш‑функции для документа.
- Алиса шифрует это значение своим закрытым ключом, тем самым подписывая документ.
- Алиса посылает Бобу документ и подписанное хеш‑значение.
- Боб вычисляет значение хеш‑функции для документа, присланного Алисой.
- Боб расшифровывает значение хеш‑функции документа, присланного Алисой.
- Боб сравнивает это значение с вычисленным самостоятельно. Если они совпадают, то подпись подлинна.
Как видно из примеров — надежность механизма цифровой подписи базируется на двух предположениях:
- Закрытый ключ Алисы доступен только ей и больше никому.
- У Боба находится открытый ключ именно Алисы, а не кого‑то другого.
Реализация клиентской части
Теперь ты должен примерно представлять, как можно реализовать подпись запросов. Способов реализации больше одного, но я покажу самый, по моему мнению, простой и удобный.
Для начала определимся с генерацией ключей и с самим алгоритмом цифровой подписи. Очень не рекомендую писать это все руками, используя криптопримитивы из Android SDK. Лучше взять готовое и зарекомендовавшее себя решение — библиотеку Tink, написанную сумрачными гениями из Google. Она решает сразу несколько наших проблем:
- сохраняет ключи в Android Keystore, что практически исключает их насильственное извлечение с устройства. А значит, обеспечивает нам истинность первого предположения о надежности механизма цифровой подписи;
- предоставляет надежный алгоритм подписи на эллиптических кривых — ECDSA P-256;
- предоставляет удобные криптопримитивы и API для создания цифровой подписи.
Подключаем библиотеку к проекту (implementation
) и генерируем пару ключей, которые сразу будут сохранены в Android Keystore:
companion object { const val KEYSET_NAME = "master_signature_keyset" const val PREFERENCE_FILE = "master_signature_key_preference" const val MASTER_KEY_URI = "android-keystore://master_signature_key"}SignatureConfig.register()val privateKeysetHandle = AndroidKeysetManager.Builder() .withSharedPref(application, KEYSET_NAME, PREFERENCE_FILE) .withKeyTemplate(EcdsaSignKeyManager.ecdsaP256Template()) .withMasterKeyUri(MASTER_KEY_URI) .build() .keysetHandle
Чтобы сервер мог проверить нашу цифровую подпись, ему нужно как‑то передать публичный ключ от той пары, которую мы сгенерировали выше. Делать это правильнее всего на этапе авторизации. Публичный ключ не секретный, поэтому мы вполне можем передать его прямо в запросе вместе с логином и паролем пользователя, предварительно закодировав в Base64:
val bos = ByteArrayOutputStream()val w = BinaryKeysetWriter.withOutputStream(bos)privateKeysetHandle.publicKeysetHandle.writeNoSecret(w)val response = api.login( LoginRequest( username, password, Base64.encodeToString(bos.toByteArray(), Base64.DEFAULT) ))bos.close()
Tink не позволяет работать с ключевым материалом напрямую. Вместо этого библиотека предлагает концепцию Reader/Writer’ов, которые позволяют читать и писать ключи в JSON-представлении или в бинарном. Подробности есть в документации.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»