Году эдак в 2021-м, мне по служебной необходимости понадобилось переосмыслить подход к аутентификации по пин-коду и сделать эту процедуру максимально безопасной настолько, насколько это вообще возможно для мобильных устройств. Получилось нечто вроде RFC. Я уже упоминал этот документ в видео “Как взломать PIN-код | Теория, практика и векторы атаки”, но тогда его публиковать было нельзя. Теперь можно.
Приятного чтения!
Контекст
В мобильных операционных системах есть два типа аутентификации, которые нас интересуют: короткий код (пин-код) и биометрические данные. Безопасность этих типов аутентификации обеспечивается операционной системой, что делает их надежными. По крайней мере до тех пор, пока работают механизмы безопасности ОС и устройства.
В нашем случае, есть требование - реализовать аутентификацию по пин-коду внутри приложения, что полностью нивелирует системные механизмы безопасности и заставляет нас реализовывать свои.
Рассмотрим модель продвинутого злоумышленника и доступные ему векторы атак от которых мы пытаемся защититься описанными выше методами аутентификации.
Алиса оставила свой разблокированный смартфон на столе, чем и воспользовался Чак. Он сразу открыл наше приложение и увидел, что оно закрыто биометрической аутентификацией. При этом все еще оставалась возможность входа по короткому коду, который был реализован внутри приложения и подразумевал локальную проверку ввода. Далее Чак подключил смартфон Алисы к своему компьютеру и каким-то образом получил возможность динамической инструментации кода. После чего был написан скрипт для подбора пин-кода, который еще и обнулял счетчик попыток. Спустя некоторое время, пин-код был подобран и Чак успешно попал в приложение, перевел все деньги Алисы в “Фонд Озеленения Луны” и вернул смартфон на место. Алиса в этот момент доедала круассан в соседнем кафе…
Атака стала возможной потому что в отличие от биометрии, которая задействует системные механизмы безопасности, кастомная реализация пин-кода внутри приложения не может полагаться на системные механизмы безопасности, т.к. приложение полностью контролируется злоумышленником.
Особенности платформ
Android
Хранить данные безопасно можно только в зашифрованном виде, при этом ключи шифрования должны изначально генерироваться и храниться в аппаратном хранилище - AndroidKeyStore. В случае аутентификации по системному пин-коду или биометрии есть возможность ограничить доступ приложения к этим ключам шифрования. Для осуществления криптографической операции потребуется ввод системного пин-кода или биометрических данных. В случае кастомного пин-кода эта модель безопасности перестает работать, т.к. несмотря на недоступностью ключевого материала самому приложению, оно все еще может выполнять криптографические операции с этими ключами без каких-либо ограничений со стороны системы.
iOS
Здесь ситуация почти аналогичная. Keychain, является надежным хранилищем данных только тогда, когда доступ к этим данным ограничен системными механизмами аутентификации. Это задается с помощью атрибутов в роли которых могут выступать: TouchID/FaceID и системный пин- код. В нашем случае эти механизмы безопасности нивелируются кастомной реализацией пин-кода внутри приложения.
Решение
Все операции по созданию, проверке и изменению пин-кода нужно перенести на сервер. Вариантом локальной аутентификации должна остаться только биометрическая.
Изменения в процедуре регистрации
Потенциально существует два варианта реализации “серверного” пин-кода:
- Один пин-код на все устройства
- Для каждого устройства свой пин-код
Один пин-код на все устройства
В этом случае, пин-код создается один раз при регистрации пользователя и далее, при добавлении нового устройства, пользователю не потребуется создавать пин-код заново. После успешного прохождения этапа OTP, клиент получит флаг has_pin=true и переведет пользователя сразу на этап проверки пин-кода.
Для каждого устройства свой пин-код
Этот случай отличается от предыдущего тем, что нам на этапе отправки OTP нужно также передать информацию об устройстве пользователя, чтобы создать для него новый пин-код. Это должен быть какой-то стабильный device_id который мы четко сможем ассоциировать с конкретным устройством пользователя для дальнейших проверок пин-кода.
Описание сущности pin_token
Токен представляет из себя массив размером 32 байта, сгенерированный с помощью максимально безопасного из доступных на платформе ГСПЧ и закодированный с помощью Base64.
Пример такого токена:
zxgCGiNHeE4hHZ7tlJH5PAX6BYmB1XpdM86+rV0hfw4=
Должен быть сохранен на стороне клиента для дальнейшей аутентификации по пин-коду. С этим токеном можно вызвать следующие методы:
POST /pin/create HTTP/1.1
Host: backend.com
Authorization: Bearer zxgCGiNHeE4hHZ7tlJH5PAX6BYmB1XpdM86+rV0hfw4=
{
"pin_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0..."
}
---
HTTP/1.1 200 OK
POST /pin/verify HTTP/1.1
Host: backend.com
Authorization: Bearer zxgCGiNHeE4hHZ7tlJH5PAX6BYmB1XpdM86+rV0hfw4=
{
"pin_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0..."
}
---
HTTP/1.1 200 OK
{
"access_token": "..."
"refresh_token": "..."
}
Описание этих запросов можно найти в следующей секции.
Создание пин-кода
Mode: Argon2i
Time cost in iterations: 5
Memory cost in KBytes: 65 536
Parallelism: 2
Derived hash length: 128bit
Если пользователь отказался от установки биометрической аутентификации или она не поддерживается устройством, то сохранять токены access и refresh не нужно. Они остаются в памяти пока приложение активно и удаляются из нее когда оно сворачивается.
После удаления токенов из памяти, пользователю потребуется пройти повторную аутентификацию по пин-коду, в ходе которой он получит пару токенов.
Флаг has_pin вводится исключительно для удобства и его модификация или насильная установка ничего не даст злоумышленнику, т.к. в случае сброса этого флага он попадет на повторную аутентификацию по номеру телефона. После успешного ввода OTP с сервера придет флаг
{"has_pin": true}
и приложение перенаправит злоумышленника на экран входа по пин-коду, который он не знает. Имея полный контроль над приложением, злоумышленник может подменить ответ от сервера и убрать из него флаг has_pin заставив приложение перейти к процессу создания пин-кода. При этом сервер, получив запрос на создание нового когда при уже имеющемся может либо заблокировать пользователя, либо инициировать процесс замены пин-кода с обязательным вводом старого кода. Количество попыток замены должно быть ограничено. После исчерпания лимита пользователь блокируется. Разблокировка возможно только после звонка в техподдержку с подтверждением личности.
Этот подход будет работать только в том случае, если мы не позволим каждому устройству пользователя иметь отдельный пин-код, а применим подход описанный в секции “Один пин-код на все устройства”. В противном случае, злоумышленник, контролирующий приложение может полностью эмулировать процесс создание пин-кода на новом устройстве путем подмены текущего device_id на другой. Что позволит ему в конечном итоге получить токен доступа к данным.
Кроме этого, для реализации “мягкого подхода” потребуется разрешить делать запросы на смену пин-кода с pin_token-ом. Это не фатально, учитывая механизмы безопасности описанные выше, но лучше не давать этому токену дополнительных возможностей без острой необходимости.
Аутентификация по пин-коду
Смена пин-кода
При смене пин-кода нужно соблюсти два важных требования:
- Все запросы осуществляются только с access_token, т.е. из авторизованной зоны
- Передача старого пин-кода является строго обязательной
Пример запроса на смену пин-кода
POST /pin/change HTTP/1.1
Host: backend.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
{
"old_pin_hash": "03ac674216f3e15c761ee1a5e255f067953623c8b3..."
"new_pin_hash": "fe2592b42a727e977f055947385b709cc82b16b9a8..."
}
---
HTTP/1.1 200 OK
Операции для работы с пин-кодом
Запрос | Токен доступа |
---|---|
POST /pin/create | pin_token |
POST /pin/verify | pin_token |
POST /pin/change | access_token |
В рамках методов для работы с пин-кодом должно существовать четкое разделение с какими токенами какие методы можно вызывать. Это полезно в том числе для отслеживания нетипичного поведения, что может служить сигналом к включению усиленного логирования, жесткой блокировке пользователя или реализации концепции скрытого бана.
Биометрическая аутентификация
При использовании биометрии, у нас появляется фактор базирующийся на системных механизмах безопасности. Благодаря этому, можно сделать полноценную локальную аутентификацию с сохранением refresh_token-а.
Реализация на каждой платформе имеет свои особенности, но общий принцип остается одинаковым: сохранять и читать refresh_token можно только с применением биометрических данных. Такая реализация лишает злоумышленника возможности за разумное время получить доступ в приложение.
Android
Для сохранения токена его необходимо зашифровать. Ключи шифрования нужно сгенерировать в аппаратном хранилище StrongBox
или по крайней мере в TEE при помощи AndroidKeyStore
. Для применения биометрических ограничений, у ключа нужно установить параметр
setUserAuthenticationRequired(true)
. Все остальные необходимые параметры можно узнать в документации.
iOS
Токен может быть сохранен в Keychain
c атрибутом на доступ только по биометрии. В качестве атрибута доступа нужно использовать флаг
SecAccessControlCreateFlags.biometryCurrentSet . Подробности в документации.
Последствия
Если в будущем, мы захотим сделать в приложении полноценный оффлайн режим, то потребуется переосмысление этого подхода и введение дополнительных механизмов безопасности для защиты платежных транзакций и прочих “опасных” операций. Также, поскольку этот подход к аутентификации базируется на полном доверии серверу, потребуются дополнительные усилия для надежной реализации этого механизма с последующим тестированием. Для обеспечения хорошего UX потребуется адекватная реакция на ошибки сетевого взаимодействия, которые нужно будет обыгрывать на UI при вводе или создании пин-кода.