РНР-безопасность: где и как хранить пароли. Часть 2 +9



Всем привет! На прошлой неделе мы опубликовали первую часть данной статьи, чем вызвали нешуточный холивар.

Одной из главных претензий было отсутствие в статье упоминания password_hash, как мы и обещали, вторую часть данного материала начнем как раз таки с хеширования пароля с помощью password_hash. Также напоминаем о том, что написание данной статьи было навеяно запуском новой группы по курсу «Backend-разработчик на PHP», но к программе обучения данный материал отношения не имеет.


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

Пожалуй на этом мы завершим и без того затянувшееся предисловие и перейдем непосредственно к статье.

Хеширование пароля с помощью password_hash


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

Мы можем выбрать, какой тип алгоритма использовать, задав одну из констант по своему выбору:

  • PASSWORD_DEFAULT из PHP 5.5 использует Bcrypt в качестве алгоритма по умолчанию. Однако с течением времени это изменяется по мере обнаружения новых, более безопасных алгоритмов либо иных факторов.
  • PASSWORD_BCRYPT создает хеш crypt(). Обычно он содержит 60 символов, опознать его можно по идентификатору в формате "$2y$".
  • PASSWORD-ARGON2I Argon2 на данный момент является одним из наиболее безопасных алгоритмов хеширования. Он доступен только в том случае, если РНР был скомпилирован с Argon2.
  • PASSWORD_ARGON2ID Этот алгоритм хеширования также принадлежит к семейству Argon2 и задействует версию Argon2ID, а не I. Чтобы он работал, также необходимо, чтобы РНР был скомпилирован посредством Argon2.

У этой функции также есть необязательный параметр, который состоит из ассоциативного массива, принимающего несколько ключей в соответствии с выбранным алгоритмом.
Если вы предпочитаете использовать Bcrypt, ключ этой последовательности будет значением cost.

Если вы выберете алгоритм, который использует Argon2, ключами для ассоциативного массива будут: memory_cost (целое число, указывающее максимальное количество памяти, необходимое для вычисления хэша), time_cost (целое число, указывающее максимальное время, необходимое для вычисления хэша) и thread (другое целое число, указывающее количество потоков, используемых при вычислении хэша).

Не указывайте параметр salt в РНР 7.0, иначе получите предупреждение об устаревшем подходе.

Теперь мы знаем, какие элементы необходимы для использования функции password_hash(). Давайте посмотрим, как ее прописывать.

echo password_hash("MySuperPass", PASSWORD_DEFAULT);
$2y$10$TLayAY8ZaAZ9FE50EylGYO9oEgrb7gsw1yzJemHdBu1gOQfyWrEUm
$options = ['cost' => 12,];

echo password_hash("MySuperPass", PASSWORD_BCRYPT, $options);
$2y$12$jhmTbxAuZXVtX2y.Jc8iy.dW/NENqVCeq2vuoFI9/oa4./YlzhpYO

echo password_hash('rasmuslerdorf', PASSWORD_ARGON2I);
$argon2i$v=19$m=1024,t=2,p=2$YzJBSzV4TUhkMzc3d3laeg$zqU/1IN0/AogfP4cmSJI1vc8lpXRW9/S0sYY2i2jHT0

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

Скрипт в вышеприведенном примере поможет вам задать оптимальное значение cost для вашего аппаратного оборудования.

Верификация пароля пользователя


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

Хешируя данные в соответствии с последними трендами безопасности, вы ничего не храните в зашифрованном виде, а ваш сервер спрятан в подвале 10-метровой глубины.

Что теперь?

Теперь вы должны позволить пользователям залогиниться в приложении. Для этого в РНР предусмотрена встроенная функция, которая проверяет соответствие пароля хешированной последовательности. Эта функция носит название password_verify(). Работает она примерно вот так:

$hash = '$2y$07$BCryptRequires22Chrcte/VlQH0piJtjXl.0t1XkA8pw9dMXTpOq';
if (password_verify('rasmuslerdorf', $hash)) {
    echo 'Пароль верен!';
} else {
    echo 'Пароль не правильный!';
}

У нее есть два параметра, и оба должны иметь формат последовательности. Первый параметр — это пароль, который пользователь ввел в форму входа в аккаунт. Второй параметр — это непосредственно хешированные данные, с которыми мы будем сверяться.

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

Эта функция работает благодаря тому, что на предыдущем шаге (когда мы хешировали пароль), значение, вернувшееся от password_hash, включало в себя используемый нами алгоритм, cost и salt.

Таким образом, нам доступна вся информация, необходимая для password_verify().

Алгоритм работы системы регистрации пользователей на РНР


Надеюсь, теперь вы понимаете, какие меры безопасности принимают РНР-разработчики при обращении с паролями.

Сначала необходимо проверить наличие post-запроса, а затем отобрать и подсчитать количество пользователей, чьи данные совпадают с введенными.

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

Заключение


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

Читать первую часть

Вы можете помочь и перевести немного средств на развитие сайта

Теги:



Комментарии (17):

  1. Mylistryx
    /#20887422

    Какой то кусок текста из официальной документации, если честно.
    Как насчет password_needs_rehash?
    В первый раз слышу про настройку cost. Судя по статье — поставлю я cost=1, менее 100мс? Менее! Зачем её вообще трогать то?
    IMHO, статья ради статьи.

  2. php7
    /#20888114 / -1

    Напишите, пожалуйста, о том, как работать с авторизационной кукой, что туда писать.

    • dady_KK
      /#20888460 / +1

      Не ради холивара, а что именно Вы хотите туда писать, например: есть сессионная кука с идентификатором сессии, по которому сервер сверяется с самими сессиями и уже на основании того, что есть в сессии делает выводы. Зачем ещё авторизационная кука, чтобы использовать вместо сессий? Так и сессионная кука насколько я понимаю для этого подойдёт, единственный выигрыш — устанавливать различное время жизни для разных печенек, или есть какие-то ещё плюшки?

      • VolCh
        /#20888850

        Отказаться от сессий в принципе можно, следуя REST архитектуре. Особенно полезно в случае если несколько реплик запущено и лоад балансер перед ними.

      • php7
        /#20889262

        Допустим есть галочка при авторизации «Запомнить меня».
        Это что, сессии должны жить год?

        • Mylistryx
          /#20889290

          А что сложного? пишем токен (рандомную строку) и храним её в БД на сервере, если пользователь не авторизован, то ищем эту строку в БД и авторизуем пользователя по токену.
          Или вы просите автора топика скопипастить вам еще одну статью из интернета?

          • php7
            /#20889426

            А чтобы не хранить на сервере?

            • Mylistryx
              /#20889590

              С трудом себе это представляю. Пользователь может менять на своей стороне COOKIE как угодно, значит хранить данные там не безопасно. Т.е. записать в куку ID пользователя или что-то подобное — так себе идея. При очень большом желании можно зашифровать всё это ключем хранимым на сервере и расшифровывать им же, но мне кажется проще вариант с token.
              Есть еще вариант с Fingerprint, но я никогда его не использовал, только читал про это, помоему даже здесь, на хабре.

              • oxidmod
                /#20889894

                HttpOnly cookie

                • Mylistryx
                  /#20889906

                  От чего это защитит? От правки пользователем? Это защита от XSS, не более.

            • BoShurik
              /#20889646

              Например, в Symfony данные в куке подписываются с помощью секретного ключа, чтоб их нельзя было подделать

              • Mylistryx
                /#20889976

                Посмотрел, да, интересная реализация. Позволяет обойтись без токенов и если пользователь сменит пароль, то по куке уже не войдет. От подделки кук защищает хэш.
                php7 — вот то, что вам надо!

            • iig
              /#20889700

              Можно в cookie сохранить сертификат SSL со сроком действия в 1 год, и проверять его валидность. А что не так с хранением токена на сервере? + 2 поля (токен и срок действия) в таблице, которая и так есть.

              • Mylistryx
                /#20889736

                В Yii2 даже проще, $token = TOKEN_STRING_TIME
                Проверяется через explode('_', $token), сначала TIME, что не протух, потом уже сам токен.
                Но я обычно переделываю и храню токены в отдельной таблице с внешним ключем и указанием типа токена. Дает возможность хранить все связанные токены в одной таблице, подчищать протухшие, ограничивать кол-во в целом и по времени.
                Но что то мы из комментариев тостер тут устраиваем.

            • dso
              /#20891270

              Тогда токен jwt, который подписывается своим приватным ключом. Токен в бд можно не хранить.

              • Mylistryx
                /#20892240

                Смысл от этого действия? Просто пустить в систему? Если мы не храним этот токен в БД, то к какому пользователю он относится? Выше ссылка на шикарный пример из Symfony!

                • dso
                  /#20892374

                  Информация о пользователе находится в payload части токена. Гарантией, что токен не подделан, служит сигнатура, которая подписана секретным ключом. Злоумышленник не сможет написать в payload части токена свой email, потому что у него нет секретного ключа, чтобы сгенерировать верную сигнатуру.