Представим, что перед вами стоит задача организовать аутентификацию пользователя (в мобильном приложении, в первую очередь) так, как это сделано в 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.


Если же нет, то варианты исключений:


  1. Некорректный токен: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: TOKEN_INVALID.
  2. Некорректный код: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: SMS_CODE_INVALID.

3. Форсированная отправка кода повторно.


    PUT /api/sms_authentications/<token>/resend

Аналогично. Если всё ок — код 200.


Если же нет, то варианты исключений:


  1. Некорректный токен: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: TOKEN_INVALID.
  2. Слишком частая отправка (скажем, прошлая отправка была не позднее чем 60 секунд назад): HTTP код состояния: 400 (BAD_REQUEST), в теле ответа: TOO_OFTEN.

Помимо этого, каждый метод API, который требует аутентифицированного пользователя должен получать на вход дополнительный параметр token, который связан с пользователем.


Литература:


  1. Образец для подражания — API Telegram: https://core.telegram.org/methods
  2. Дискуссия на 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.

Изменения в схеме БД


Итого, в модели (или в таблице БД, если угодно) надо хранить:


  1. Телефон: phone (советую использовать библиотеки для унификации телефонного номера, вроде этой для Rails),
  2. Ключ для TOTP: otp_secret_key (читаете подробное README для выбранной библиотеки TOTP),
  3. Токен: token (создаете при первом запросе к API чем-нибудь типа SecureRandom),
  4. Ссылку на пользователя: user_id (если у вас есть отдельная таблица/модель, где хранятся данные пользователя).

Особенности реализации мобильного приложения


В случае Android полученный токен можно хранить в SharedPreferences (почему не AccountManager), а для iOS в KeyChain. См. обсуждение на SoF.


Заключение


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

Поделиться с друзьями
-->

Комментарии (20)


  1. jonic
    14.07.2016 21:23
    +2

    Это точно 1в1 как в Telegram/Viber/WhatsApp? Просто если нет, то зачем?


    1. mpetrunin
      15.07.2016 10:14

      Конечно, не точно. Насколько мне известно Telegram/Viber/WhatsApp не раскрывают особенностей серверной архитектуры в этой части. Если это не так — прошу меня поправить.


      У статьи, грубо говоря, следующий посыл: если вы хотите организовать нечто похожее, но не знаете как — то вам сюда.


      P.S. Немножко дополнил введение и в разделе про OTP добавил описание HOTP — это больше похоже на то, что используется в Telegram.


  1. sergeylanz
    14.07.2016 22:02
    +3

    Самое главное что много кто пропускает это защита от brute force. А то можно быстро числа угадать


    1. z0rgoyok
      15.07.2016 02:34
      +1

      Еще бы защиту от многократных запросов.


      1. mpetrunin
        15.07.2016 12:17

        Это вроде как и есть brute force или вы что-то ещё имели ввиду?


        1. z0rgoyok
          15.07.2016 15:05

          Другое — много запросов СМС = дорого.


          1. mpetrunin
            15.07.2016 16:41

            Это хороший вопрос. Можно ограничить число СМС в час на определённый номер. Впрочем, ничто не мешает указать произвольное число номеров. Тогда можно банить IP, но ничто не мешает менять IP. В этом случае, можно блокировать регистрацию на время, но, судя по всему, если вас захотят задидосить таким образом — задидосят.


            У вас есть соображения на тему?


    1. mpetrunin
      15.07.2016 12:17

      Спасибо, что затронули эту тему. Обновил статью, и добавил раздел Безопасность, где дан ответ на тему противостояние прямому перебору. Перечислил два подхода, которые рекомендуются авторами стандарта.


  1. IT_SECURITY
    15.07.2016 08:39

    Поясните, как выглядит брут-форс атака на TOTP? Если код меняется в течение каждых 60 секунд, и секрет (в основе кода) не скомпрометирован, то достаточно ограничить число попыток ввода.

    Алгоритм простой: человек ошибся с вводом кода, всё, надо ждать следующего. Натуральным образом (при ttl 30с) это даёт всего 2880 попыток в сутки. А дальше простой алгоритм, который увеличивает задержку после каждой следующей попытки. Очевидно, что человек не будет пробовать больше пары тысяч раз ни при каких обстоятельствах (тем паче, код надо не «вспоминать», а «набрать»), то есть увеличение задержки можно делать после пятой-шестой попытки.


    1. IT_SECURITY
      15.07.2016 08:43
      +2

      и еще добавлю

      Ну вот, я уже обрадовался было, что наконец-то Яндекс сделает двухфакторку по RFC, и я смогу его занести в свой MS Authenticator в тёплую компанию к
      Гуглу,
      Майкрософту,
      Гитхабу,
      Дропбоксу,
      Фэйсбуку,
      Вордпрессу,
      Вконтакту,
      и даже моему любимому Тайнипэссу, для которой я сам буквально пару месяцев назад её и реализовал (обкатывается на QA, скоро глобально включим).

      Но нет. Оказывается, Яндекс изобрёл свой собственный нестандартный велосипед, для которого вдобавок нету приложения под WP.

      Спасибо, Яндекс.


      1. Leginnn
        15.07.2016 16:47
        +1

        Что есть, то есть, видимо чтобы вот так вот не совали его в MS Authenticator.


    1. 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% успеха.


  1. Lucky_spirit
    15.07.2016 12:20
    +1

    В Android'е есть AccountManager, в котором можно хранить токен для пользователя. Чем хранение токена в SharedPreferences лучше?


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


  1. random1st
    15.07.2016 13:02

    Считаю аутентификацию исключительно по SMS несекьюрной в принципе. Телефония — штука небезопасная, и любой субъект, имеющий возможность перехвата SMS, может с легкостью получить доступ к вашим данным. Двухфакторная — это другое дело.


    1. mpetrunin
      15.07.2016 13:16

      По этой же логике двухфакторка тоже несекьюрна в принципе, т.к. любой, кто у установил слежение за вашим устройством (например, перехват ввода с клавиатуры) и к вашим СМС может с легкостью получить доступ к данным.


      Тут корректнее говорить, о вероятности и сложности получения доступа. И тут вы, безусловно, правы. Двухфакторка значительно надёжднее. Но она — некоторый компромисс с удобством пользователя, и упомянутые Telegram/Viber/WhatsApp на него не идут, оставляя только СМС. За что, кстати, Телеграм уже платился.


      Но, если у вас не такое критичное и важное приложение как мессенджер или инернет-банк, то, в целом, можно и пойти на этот риск. Конечно, если ценность ваших конкретных данных (к примеру, в приложениях такси) не сопоставима с затратами на перехват СМС.


      1. random1st
        15.07.2016 13:28

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


        1. mpetrunin
          15.07.2016 13:40

          Строго говоря, пока у вас нет 100% контроля на устройством (как его нет ни в случае iOS, ни в случае Android), говорить о том, что это чисто ваш промах не до конца корректно. Теоретически, кейлогер могут установить на заводе, в магазине или где-то в недрах Apple, или Google или Huawei и ему подобные. Кроме того существуют уязвимости нулевого дня, и опять же ваша вина будет только в том, что вы оказались в сети.


          Превентивные меры в вышеописанных ситуациях тоже неясно, какие можно предпринять.


          1. random1st
            15.07.2016 13:45

            Вы сравниваете технические возможности организации перехвата моих данных с организационными. Это некорректно. Одно дело случайный взлом по причине совпадения звезд и другое дело — целенаправленный перехват данных конкретного абонента.


            1. mpetrunin
              15.07.2016 14:20

              В целом, я комментировал фразу про то, чей промах, и помогут ли превентивные меры.


              Кроме того, что касается Apple. Если им не западло удалять "пиратские" файлы с компьютеров пользователей, или Google не брезгует объявлять, что Android будет удалять пиратское ПО с телефонов пользователей, то я не вижу технических причин у этих компаний не перехватить ваши пароли. Только организационные.


              У вас нет контроля над их ПО. Вы точно также доверяете свои данные их ПО, как вы доверяете свою переписку своему ОПСОСу.


              Может показаться, что МТС себя скомпрометировало и попалось на этом, а Google и Apple — уважаемые западные компании и не позволяют себе такого. Так вот, согласно Guardian, это не так.


              Ведущие американские ИТ-компании знали о том, что АНБ собирает данные их пользователей, и даже помогали агентству. Об этом сообщил главный юрист АНБ.



              Он сообщил, что АНБ получала как содержимое сообщений пользователей, которые они пересылали друг-другу, так и сопутствующие метаданные.