Цифровая подпись запросов к серверу — это не какая‑то черная магия или удел избранных сумрачных безопасников. Реализация этой функциональности в мобильном приложении вполне по силам любому хорошему разработчику — при условии, что он знает правильные инструменты и подход к этой задаче. И если хорошим разработчиком тебе придется становиться самостоятельно, то о правильных инструментах и подходах я расскажу в этой статье.
При разработке клиент‑серверных приложений под Android есть несколько очевидных способов сделать соединение безопаснее. Кажется, что к 2020 году уже все выучили аббревиатуру HTTPS как мантру, да и Google со своей стороны помогает, запрещая по умолчанию HTTP-трафик в новых версиях ОС. Чуть более продвинутые товарищи знают, что сам по себе HTTPS защищает не от всех векторов атак (привет, Мэллори!), и накручивают SSL Pinning (aka Certificate/Public Key Pinning). Чаще всего защита канала на этом заканчивается. Да и честно говоря, в большинстве случаев этой защиты достаточно. Особенно если с помощью шифрования пользовательских данных и проверки на недоверенное окружение ликвидируются другие векторы атаки.
Но бывает и по‑другому. Приложение вынуждено работать в недоверенной среде, а это значит, что зловред на клиентском устройстве может перехватить токены доступа к серверу прямо из памяти приложения. Далее, в зависимости от реализации механизма инвалидации этих токенов, злоумышленник какое‑то время может выполнять запросы от лица пользователя. У этой проблемы есть решение — вешать цифровую подпись на все запросы, выполняющиеся из авторизованной зоны. Как правило, это все запросы, которые не /login
или /register
. О том, как реализовать подпись запросов на клиенте и на сервере, а также о подводных камнях и ограничениях этой техники поговорим в статье.
Чтобы сделать повествование более системным, давай для начала синхронизируемся в понятиях и освежим знания криптографии, если они по какой‑то причине заплесневели.
Начнем с понятия цифровая подпись. Тема ЦП довольно обширная, поэтому ограничимся асимметричной схемой цифровой подписи, в которой участвуют открытый и закрытый ключи. В самом простом случае цифровая подпись работает по следующему алгоритму:
Это работает, но есть проблема. Если документ, подписанный Алисой, — чек на некоторую сумму денег, то неблагонадежный Боб сможет обналичивать этот чек, пока у Алисы не закончатся деньги на счете или пока Боба не поймают. Для борьбы с этой проблемой применяются метки времени. Алиса добавляет к документу текущее время и шифрует его вместе с документом. Банк, в который Боб приносит этот чек и открытый ключ Алисы, расшифровывает документ и сохраняет метку времени. Теперь при попытке обналичить такой чек повторно банк заблокирует эту операцию, так как метки времени будут одинаковые.
Еще не заскучал? Потерпи, это все нам пригодится уже скоро, когда будем писать реализацию. Финальный аспект, который хочется обсудить, — производительность асимметричных криптосистем. Они оказываются довольно неэффективны на больших массивах данных, а значит, попытка применить этот подход для подписи объемных запросов будет нещадно жрать батарею смартфона и замедлять общение с сервером. Для ускорения всей этой машинерии принято использовать односторонние хеш‑функции. Итоговая версия алгоритма будет выглядеть так:
Как видно из примеров — надежность механизма цифровой подписи базируется на двух предположениях:
Теперь ты должен примерно представлять, как можно реализовать подпись запросов. Способов реализации больше одного, но я покажу самый, по моему мнению, простой и удобный.
Для начала определимся с генерацией ключей и с самим алгоритмом цифровой подписи. Очень не рекомендую писать это все руками, используя криптопримитивы из Android SDK. Лучше взять готовое и зарекомендовавшее себя решение — библиотеку Tink, написанную сумрачными гениями из Google. Она решает сразу несколько наших проблем:
Подключаем библиотеку к проекту (implementation 'com.google.crypto.tink:tink-android:1.5.0'
) и генерируем пару ключей, которые сразу будут сохранены в 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-представлении или в бинарном. Подробности есть в документации.
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
1 год7690 р. |
1 месяц720 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости