Аутентификация по короткому коду с поддержкой биометрии. Почти RFC - (не)Уникальный опыт

Аутентификация по короткому коду с поддержкой биометрии. Почти RFC


Году эдак в 2021-м, мне по служебной необходимости понадобилось переосмыслить подход к аутентификации по пин-коду и сделать эту процедуру максимально безопасной настолько, насколько это вообще возможно для мобильных устройств. Получилось нечто вроде RFC. Я уже упоминал этот документ в видео “Как взломать PIN-код | Теория, практика и векторы атаки”, но тогда его публиковать было нельзя. Теперь можно.

Приятного чтения!

/img/pin-code-authentication/kdpv.png



Контекст

В мобильных операционных системах есть два типа аутентификации, которые нас интересуют: короткий код (пин-код) и биометрические данные. Безопасность этих типов аутентификации обеспечивается операционной системой, что делает их надежными. По крайней мере до тех пор, пока работают механизмы безопасности ОС и устройства.

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

Рассмотрим модель продвинутого злоумышленника и доступные ему векторы атак от которых мы пытаемся защититься описанными выше методами аутентификации.

Алиса оставила свой разблокированный смартфон на столе, чем и воспользовался Чак. Он сразу открыл наше приложение и увидел, что оно закрыто биометрической аутентификацией. При этом все еще оставалась возможность входа по короткому коду, который был реализован внутри приложения и подразумевал локальную проверку ввода. Далее Чак подключил смартфон Алисы к своему компьютеру и каким-то образом получил возможность динамической инструментации кода. После чего был написан скрипт для подбора пин-кода, который еще и обнулял счетчик попыток. Спустя некоторое время, пин-код был подобран и Чак успешно попал в приложение, перевел все деньги Алисы в “Фонд Озеленения Луны” и вернул смартфон на место. Алиса в этот момент доедала круассан в соседнем кафе…

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

Особенности платформ

Android

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

iOS

Здесь ситуация почти аналогичная. Keychain, является надежным хранилищем данных только тогда, когда доступ к этим данным ограничен системными механизмами аутентификации. Это задается с помощью атрибутов в роли которых могут выступать: TouchID/FaceID и системный пин- код. В нашем случае эти механизмы безопасности нивелируются кастомной реализацией пин-кода внутри приложения.

Решение

Все операции по созданию, проверке и изменению пин-кода нужно перенести на сервер. Вариантом локальной аутентификации должна остаться только биометрическая.

Изменения в процедуре регистрации

/img/pin-code-authentication/reg.svg
После этапа с OTP, вместо выдачи пары токенов access_token & refresh_token, добавляется этап установки пин-кода, для которого выдается отдельный pin_token, имеющий разрешение на вызов методов касающихся только создания и пин-кода и верификации.


🙈
Зачем нужен еще один токен? На этапе создания пин-кода еще не применены все требуемые механизмы безопасности (пин-код и/или биометрия), поэтому выдавать на этом этапе токен дающий доступ ко всем данным - плохая идея.

Потенциально существует два варианта реализации “серверного” пин-кода:

  • Один пин-код на все устройства
  • Для каждого устройства свой пин-код

Один пин-код на все устройства

В этом случае, пин-код создается один раз при регистрации пользователя и далее, при добавлении нового устройства, пользователю не потребуется создавать пин-код заново. После успешного прохождения этапа OTP, клиент получит флаг has_pin=true и переведет пользователя сразу на этап проверки пин-кода.

/img/pin-code-authentication/one_pin.svg

Для каждого устройства свой пин-код


🧨
Этот подход не рекомендуется к использованию, т.к. уязвим к вектору атаки, описанному в секции "Создание пин-кода". Оставлен в качестве возможной альтернативы, которая потребует дополнительных доработок модели безопасности для снижения описанных рисков.

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

/img/pin-code-authentication/many_pins.svg

Описание сущности 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": "..."
}

Описание этих запросов можно найти в следующей секции.


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

Создание пин-кода

/img/pin-code-authentication/create_pin.svg
Для повышения безопасности всего подхода, нужно предотвратить передачу оригинального пин-кода на сервер и его сохранение там в чистом виде. Чтобы этого добиться необходимо хэшировать пин-код пользователя вместе с солью, которая никогда не передается на сервер. Хэш пин-кода вычисляется с помощью KDF функции, где в качестве парольной фразы выступает сам пин-код пользователя, в а качестве соли - sha256 хэш от номера телефона пользователя. Это позволяет получить стабильный хэш для данного пользователя на всех его устройствах. Тип KDF функции и ее параметры необходимо выбирать опираясь на рекомендации по безопасности для конкретной платформы.
💡
Наиболее безопасной KDF на момент составления документа является Argon2 с такими параметрами (для мобильных платформ):
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 заставив приложение перейти к процессу создания пин-кода. При этом сервер, получив запрос на создание нового когда при уже имеющемся может либо заблокировать пользователя, либо инициировать процесс замены пин-кода с обязательным вводом старого кода. Количество попыток замены должно быть ограничено. После исчерпания лимита пользователь блокируется. Разблокировка возможно только после звонка в техподдержку с подтверждением личности.
/img/pin-code-authentication/attack.svg

Этот подход будет работать только в том случае, если мы не позволим каждому устройству пользователя иметь отдельный пин-код, а применим подход описанный в секции “Один пин-код на все устройства”. В противном случае, злоумышленник, контролирующий приложение может полностью эмулировать процесс создание пин-кода на новом устройстве путем подмены текущего device_id на другой. Что позволит ему в конечном итоге получить токен доступа к данным.

Кроме этого, для реализации “мягкого подхода” потребуется разрешить делать запросы на смену пин-кода с pin_token-ом. Это не фатально, учитывая механизмы безопасности описанные выше, но лучше не давать этому токену дополнительных возможностей без острой необходимости.

‼️
При создании и изменении пин-кода неплохо бы предостеречь, а то и запретить пользователям создавать заведомо слабые пин-коды. Список слабых кодов можно взять например здесь

Аутентификация по пин-коду

/img/pin-code-authentication/pin_auth.svg

Смена пин-кода

/img/pin-code-authentication/pin_change.svg

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

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

Пример запроса на смену пин-кода

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-а.

/img/pin-code-authentication/biometry.svg

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

Android

Для сохранения токена его необходимо зашифровать. Ключи шифрования нужно сгенерировать в аппаратном хранилище StrongBox или по крайней мере в TEE при помощи AndroidKeyStore . Для применения биометрических ограничений, у ключа нужно установить параметр setUserAuthenticationRequired(true). Все остальные необходимые параметры можно узнать в документации.

iOS

Токен может быть сохранен в Keychain c атрибутом на доступ только по биометрии. В качестве атрибута доступа нужно использовать флаг SecAccessControlCreateFlags.biometryCurrentSet . Подробности в документации.

Последствия

Если в будущем, мы захотим сделать в приложении полноценный оффлайн режим, то потребуется переосмысление этого подхода и введение дополнительных механизмов безопасности для защиты платежных транзакций и прочих “опасных” операций. Также, поскольку этот подход к аутентификации базируется на полном доверии серверу, потребуются дополнительные усилия для надежной реализации этого механизма с последующим тестированием. Для обеспечения хорошего UX потребуется адекватная реакция на ошибки сетевого взаимодействия, которые нужно будет обыгрывать на UI при вводе или создании пин-кода.


Смотрите также