Представим, что перед вами стоит задача организовать аутентификацию пользователя (в мобильном приложении, в первую очередь) так, как это сделано в Telegram/Viber/WhatsApp. А именно реализовать в API возможность осуществить следующие шаги:
- Пользователь вводит свой номер телефона и ему на телефон приходит СМС с кодом.
- Пользователь вводит код из СМС и приложение его аутентифицирует и авторизует.
- Пользователь открывает приложение повторно, и он уже аутентифицирован и авторизован.
Мне потребовалось некоторое количество времени, чтобы осознать, как правильно это сделать. Моя задача — поделиться наработанным с вами в надежде, что это сэкономит кому-то времени.
Я постараюсь кратко изложить выработанный подход к этому вопросу. Подразумевается, что у вас API, HTTPS и, вероятно, REST. Какой у вас там набор остальных технологий неважно. Если интересно — добро пожаловать под кат.
Мы поговорим о тех изменениях, которые следует проделать в API, о том, как реализовать одноразовые пароли на сервере, как обеспечить безопасность (в т.ч. защиту от перебора) и в какую сторону смотреть при реализации это функциональности на мобильном клиенте.
Изменения в API
В сущности требуется добавить три метода в ваше API:
1. Запросить СМС с кодом на номер, в ответ — токен для последующих действий.
Действие соответствует CREATE в CRUD.
POST /api/sms_authentications/
Параметры на вход:
phone
Параметры на выход:
token
Если всё прошло, как ожидается, возвращаем код состояния 200.
Если же нет, то есть одно разумное исключение (помимо стандартной 500 ошибки при проблемах на сервере и т.п. — некорректно указан телефон. В этом случае:
HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: PHONE_NUMBER_INVALID.
2. Подтвердить токен с помощью кода из СМС.
Действие соответствует UPDATE в CRUD.
PUT /api/sms_authentications/<token>/
Параметры на вход:
sms_code
Аналогично. Если всё ок — код 200.
Если же нет, то варианты исключений:
- Некорректный токен: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: TOKEN_INVALID.
- Некорректный код: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: SMS_CODE_INVALID.
3. Форсированная отправка кода повторно.
PUT /api/sms_authentications/<token>/resend
Аналогично. Если всё ок — код 200.
Если же нет, то варианты исключений:
- Некорректный токен: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: TOKEN_INVALID.
- Слишком частая отправка (скажем, прошлая отправка была не позднее чем 60 секунд назад): HTTP код состояния: 400 (BAD_REQUEST), в теле ответа: TOO_OFTEN.
Помимо этого, каждый метод API, который требует аутентифицированного пользователя должен получать на вход дополнительный параметр token
, который связан с пользователем.
Литература:
- Образец для подражания — API Telegram: https://core.telegram.org/methods
- Дискуссия на SOF: http://stackoverflow.com/questions/12401255/sms-registration-like-in-the-mobile-app-whatsapp
Особенности реализации одноразовых паролей
Вам потребуется хранить специальный ключ для проверки СМС-кодов. Существует алгоритм TOTP, который, цитирую Википедию:
OATH-алгоритм создания одноразовых паролей для защищенной аутентификации, являющийся улучшением HOTP (HMAC-Based One-Time Password Algorithm). Является алгоритмом односторонней аутентификации — сервер удостоверяется в подлинности клиента. Главное отличие TOTP от HOTP это генерация пароля на основе времени, то есть время является параметром[1]. При этом обычно используется не точное указание времени, а текущий интервал с установленными заранее границами (например, 30 секунд).
Грубо говоря, алгоритм позволяет создать одноразовый пароль, отправить его в СМС, и проверить, что присланный пароль верен. Причём сгенерированный пароль будет работать заданное количество времени. При всём при этом не надо хранить эти бесконечные одноразовые пароли и время, когда они будут просрочены, всё это уже заложено в алгоритм и вы храните только ключ.
Пример кода на руби, чтобы было понятно о чём речь:
totp = ROTP::TOTP.new("base32secret3232")
totp.now # => "492039"
# OTP verified for current time
totp.verify("492039") # => true
sleep 30
totp.verify("492039") # => false
Алгоритм описан в стандарте RFC6238, и существует масса реализацией этого алгоритма для многих языков: для Ruby и Rails, для Python, для PHP и т.д..
Строго говоря, Telegram и компания не используют TOTP, т.к. при регистрации там, вас не ограничивают по времени 30-ю секундами. В связи с этим предлагается рассмотреть альтернативный алгоритм OTP, который выдает разные пароли, базируясь на неком счётчике, но не на времени. Встречаем, HOTP:
HOTP (HMAC-Based One-Time Password Algorithm) — алгоритм защищенной аутентификации с использованием одноразового пароля (One Time Password, OTP). Основан на HMAC (SHA-1). Является алгоритмом односторонней аутентификации, а именно: сервер производит аутентификацию клиента.
…
HOTP генерирует ключ на основе разделяемого секрета и не зависящего от времени счетчика.
HOTP описан в стандарте RFC4226 и поддерживается тем же набором библиотек, что представлен выше. Пример кода на руби:
hotp = ROTP::HOTP.new("base32secretkey3232")
hotp.at(0) # => "260182"
hotp.at(1) # => "055283"
hotp.at(1401) # => "316439"
# OTP verified with a counter
hotp.verify("316439", 1401) # => true
hotp.verify("316439", 1402) # => false
Безопасность решения
Первое непреложное само собой разумеющееся правило: ваше API, где туда-сюда гуляют данные и, самое главное, token
должно быть завернуто в SSL. Поэтому только HTTPS, никакого HTTP.
Далее, самым очевидным вектором атаки является прямой перебор. Вот что пишут в параграфе 7.3 авторы стандарта HOTP (на котором базируется TOTP) на эту тему:
Truncating the HMAC-SHA-1 value to a shorter value makes a brute force attack possible. Therefore, the authentication server needs to detect and stop brute force attacks.
We RECOMMEND setting a throttling parameter T, which defines the maximum number of possible attempts for One-Time Password validation. The validation server manages individual counters per HOTP device in order to take note of any failed attempt. We RECOMMEND T not to be too large, particularly if the resynchronization method used on the server is window-based, and the window size is large. T SHOULD be set as low as possible, while still ensuring that usability is not significantly impacted.
Another option would be to implement a delay scheme to avoid a brute force attack. After each failed attempt A, the authentication server would wait for an increased T*A number of seconds, e.g., say T = 5, then after 1 attempt, the server waits for 5 seconds, at the second failed attempt, it waits for 5*2 = 10 seconds, etc.
The delay or lockout schemes MUST be across login sessions to prevent attacks based on multiple parallel guessing techniques.
Если кратко, то от прямого перебора алгоритм априори не защищает и надо такие вещи предотвращать на уровне сервера. Авторы предлагают несколько решений:
Отслеживать число неудачных попыток ввода кода, и блокировать возможность аутентификации по превышению некоторого максимального лимита. Лимит предлагают делать настолько маленьким, насколько ещё будет комфортно пользоваться сервисом.
- Установить задержку после неудачной попытки ввода. Причём увеличивать задержку линейно по числу неудачных попыток. К примеру, после первой попытки — установить задержку в 5 секунд, после второй в 10 и т.п..
Мнение, что можно полагаться только на то, что код живёт ограниченное число секунд, и будет безопасно, т.к. код сбрасывается — ошибочно. Даже, если есть фиксированное ограничение на число попыток в секунду.
Посмотрим на примере. Пусть код TOTP состоит из 6 цифр — это 1000000 возможных вариантов. И пусть разрешено вводить 1 код в 1 секунду, а код живёт 30 секунд.
Шанс, что за 30 попыток в 30 секунд будет угадан код — 3/100000 ~ 0.003%. Казалось бы мало. Однако, таких 30-ти секундных окон в сутках — 2880 штук. Итого, у нас вероятность угадать код (даже несмотря на то, что он меняется) = 1 — (1 — 3/100000)^2880 ~ 8.2%. 10 дней таких попыток уже дают 57.8% успеха. 28 дней — 91% успеха.
Так что надо чётко осознавать, что необходимо реализовать хотя бы одну (а лучше обе) меры, предложенные авторами стандарта.
Не стоит забывать и о стойкости ключа. Авторы в параграфе 4 обязывают длину ключа быть не менее 128 бит, а рекомендованную длину устанавливают в 160 бит (на данный момент неатакуемая длина ключа).
R6 — The algorithm MUST use a strong shared secret. The length of the shared secret MUST be at least 128 bits. This document RECOMMENDs a shared secret length of 160 bits.
Изменения в схеме БД
Итого, в модели (или в таблице БД, если угодно) надо хранить:
- Телефон: phone (советую использовать библиотеки для унификации телефонного номера, вроде этой для Rails),
- Ключ для TOTP: otp_secret_key (читаете подробное README для выбранной библиотеки TOTP),
- Токен: token (создаете при первом запросе к API чем-нибудь типа SecureRandom),
- Ссылку на пользователя: user_id (если у вас есть отдельная таблица/модель, где хранятся данные пользователя).
Особенности реализации мобильного приложения
В случае Android полученный токен можно хранить в SharedPreferences (почему не AccountManager), а для iOS в KeyChain. См. обсуждение на SoF.
Заключение
Вышеописанный подход позволит вам в рамках вашего стека технологий реализовать указанную задачу. Если вас есть соображения по этому подходу или альтернативные подходы, то прошу поделиться в комментариях. Аналогичная просьба, если у вас есть примеры документации к безопасным
Комментарии (20)
sergeylanz
14.07.2016 22:02+3Самое главное что много кто пропускает это защита от brute force. А то можно быстро числа угадать
z0rgoyok
15.07.2016 02:34+1Еще бы защиту от многократных запросов.
mpetrunin
15.07.2016 12:17Это вроде как и есть brute force или вы что-то ещё имели ввиду?
z0rgoyok
15.07.2016 15:05Другое — много запросов СМС = дорого.
mpetrunin
15.07.2016 16:41Это хороший вопрос. Можно ограничить число СМС в час на определённый номер. Впрочем, ничто не мешает указать произвольное число номеров. Тогда можно банить IP, но ничто не мешает менять IP. В этом случае, можно блокировать регистрацию на время, но, судя по всему, если вас захотят задидосить таким образом — задидосят.
У вас есть соображения на тему?
mpetrunin
15.07.2016 12:17Спасибо, что затронули эту тему. Обновил статью, и добавил раздел Безопасность, где дан ответ на тему противостояние прямому перебору. Перечислил два подхода, которые рекомендуются авторами стандарта.
IT_SECURITY
15.07.2016 08:39Поясните, как выглядит брут-форс атака на TOTP? Если код меняется в течение каждых 60 секунд, и секрет (в основе кода) не скомпрометирован, то достаточно ограничить число попыток ввода.
Алгоритм простой: человек ошибся с вводом кода, всё, надо ждать следующего. Натуральным образом (при ttl 30с) это даёт всего 2880 попыток в сутки. А дальше простой алгоритм, который увеличивает задержку после каждой следующей попытки. Очевидно, что человек не будет пробовать больше пары тысяч раз ни при каких обстоятельствах (тем паче, код надо не «вспоминать», а «набрать»), то есть увеличение задержки можно делать после пятой-шестой попытки.
IT_SECURITY
15.07.2016 08:43+2и еще добавлю
Ну вот, я уже обрадовался было, что наконец-то Яндекс сделает двухфакторку по RFC, и я смогу его занести в свой MS Authenticator в тёплую компанию к
Гуглу,
Майкрософту,
Гитхабу,
Дропбоксу,
Фэйсбуку,
Вордпрессу,
Вконтакту,
и даже моему любимому Тайнипэссу, для которой я сам буквально пару месяцев назад её и реализовал (обкатывается на QA, скоро глобально включим).
Но нет. Оказывается, Яндекс изобрёл свой собственный нестандартный велосипед, для которого вдобавок нету приложения под WP.
Спасибо, Яндекс.Leginnn
15.07.2016 16:47+1Что есть, то есть, видимо чтобы вот так вот не совали его в MS Authenticator.
mpetrunin
15.07.2016 12:19Да, вы правы насчёт ограничения или задержки. Просто важно об этом помнить. Я обновил статью и добавил раздел "Безопасность". Процитирую кусочек:
Посмотрим на примере. Пусть код TOTP состоит из 6 цифр — это 1000000 возможных вариантов. И пусть разрешено вводить 1 код в 1 секунду, а код живёт 30 секунд.
Шанс, что за 30 попыток в 30 секунд будет угадан код — 3/100000 ~ 0.003%. Казалось бы мало. Однако, таких 30-ти секундных окон в сутках — 2880 штук. Итого, у нас вероятность угадать код (даже несмотря на то, что он меняется) = 1 — (1 — 3/100000)^2880 ~ 8.2%. 10 дней таких попыток уже дают 57.8% успеха. 28 дней — 91% успеха.
Lucky_spirit
15.07.2016 12:20+1В Android'е есть AccountManager, в котором можно хранить токен для пользователя. Чем хранение токена в SharedPreferences лучше?
mpetrunin
15.07.2016 12:43Вот тут и тут дискуссии на эту тему. Краткий вывод такой:
The only reason I could think of that would encourage the use of AccountManager would be if you want to share your account across a number of different apps, as the data is stored in the central Android datastore which can be accessed by all apps. However, if this isn't required, I think its probably easier and simpler to just use SharedPreferences
Т.е. если вам не нужно ваши данные аутентификации синхронизировать между многими приложениями, то да — Account Manager, если нет (что значительно чаще. если вы не Яндекс, Google и т.п.), то проще обойтись SharedPreferences.
random1st
15.07.2016 13:02Считаю аутентификацию исключительно по SMS несекьюрной в принципе. Телефония — штука небезопасная, и любой субъект, имеющий возможность перехвата SMS, может с легкостью получить доступ к вашим данным. Двухфакторная — это другое дело.
mpetrunin
15.07.2016 13:16По этой же логике двухфакторка тоже несекьюрна в принципе, т.к. любой, кто у установил слежение за вашим устройством (например, перехват ввода с клавиатуры) и к вашим СМС может с легкостью получить доступ к данным.
Тут корректнее говорить, о вероятности и сложности получения доступа. И тут вы, безусловно, правы. Двухфакторка значительно надёжднее. Но она — некоторый компромисс с удобством пользователя, и упомянутые Telegram/Viber/WhatsApp на него не идут, оставляя только СМС. За что, кстати, Телеграм уже платился.
Но, если у вас не такое критичное и важное приложение как мессенджер или инернет-банк, то, в целом, можно и пойти на этот риск. Конечно, если ценность ваших конкретных данных (к примеру, в приложениях такси) не сопоставима с затратами на перехват СМС.
random1st
15.07.2016 13:28Однако если я позволил установить себе кейлоггер, то это мой промах. В случае же, когда мой оператор сливает данные спецслужбам и т.п. — тут никакие превентивные меры мне не помогут. Поэтому сравнение некорректно. Более того, классическая аутентификация по паролю в данном случае даже более безопасна, чем аутентификация исключительно по SMS.
mpetrunin
15.07.2016 13:40Строго говоря, пока у вас нет 100% контроля на устройством (как его нет ни в случае iOS, ни в случае Android), говорить о том, что это чисто ваш промах не до конца корректно. Теоретически, кейлогер могут установить на заводе, в магазине или где-то в недрах Apple, или Google или Huawei и ему подобные. Кроме того существуют уязвимости нулевого дня, и опять же ваша вина будет только в том, что вы оказались в сети.
Превентивные меры в вышеописанных ситуациях тоже неясно, какие можно предпринять.
random1st
15.07.2016 13:45Вы сравниваете технические возможности организации перехвата моих данных с организационными. Это некорректно. Одно дело случайный взлом по причине совпадения звезд и другое дело — целенаправленный перехват данных конкретного абонента.
mpetrunin
15.07.2016 14:20В целом, я комментировал фразу про то, чей промах, и помогут ли превентивные меры.
Кроме того, что касается Apple. Если им не западло удалять "пиратские" файлы с компьютеров пользователей, или Google не брезгует объявлять, что Android будет удалять пиратское ПО с телефонов пользователей, то я не вижу технических причин у этих компаний не перехватить ваши пароли. Только организационные.
У вас нет контроля над их ПО. Вы точно также доверяете свои данные их ПО, как вы доверяете свою переписку своему ОПСОСу.
Может показаться, что МТС себя скомпрометировало и попалось на этом, а Google и Apple — уважаемые западные компании и не позволяют себе такого. Так вот, согласно Guardian, это не так.
Ведущие американские ИТ-компании знали о том, что АНБ собирает данные их пользователей, и даже помогали агентству. Об этом сообщил главный юрист АНБ.
…
Он сообщил, что АНБ получала как содержимое сообщений пользователей, которые они пересылали друг-другу, так и сопутствующие метаданные.
jonic
Это точно 1в1 как в Telegram/Viber/WhatsApp? Просто если нет, то зачем?
mpetrunin
Конечно, не точно. Насколько мне известно Telegram/Viber/WhatsApp не раскрывают особенностей серверной архитектуры в этой части. Если это не так — прошу меня поправить.
У статьи, грубо говоря, следующий посыл: если вы хотите организовать нечто похожее, но не знаете как — то вам сюда.
P.S. Немножко дополнил введение и в разделе про OTP добавил описание HOTP — это больше похоже на то, что используется в Telegram.