Всегда хотелось, чтобы хакер не мог взломать перебором чужой пароль на сайте.
Например, если пользователь похвастается хакеру, что его пароль состоит только из цифр, то скоро пользователь потеряет свой аккаунт.
А как быть, если пользователь сообщил по телефону свой пароль жене, а хакер его услышал?
Что?! Хакер знает пароль? Все, это фиаско. Можно ли помочь такому пользователю усложнить угон его аккаунта? Меня всегда волновал этот вопрос и, кажется, я нашел способ как это сделать. Или переоткрыл его, как это часто бывает. Ведь все уже давно придумано до нас.
Вводная
- Пользователь хочет на сайте иметь пароль «12345».
- Хакер может легко подобрать этот пароль.
- Но пользователь должен войти, а хакер нет. Даже если логин и пароль хакеру известны.
- и никаких SMS с секретными кодами и посредниками в виде дополнительных сервисов. Только пользователь и ваш сайт со страницей логина.
- а еще можно будет сравнительно безопасно в троллейбусе сказать своей жене: «Галя, я на сайте site для нашего логина alice поменял пароль на 123456 — говорят, он более популярный, чем наш 12345». И не бояться, что аккаунт взломают за секунду.
Как работает метод? Вся конкретика — под катом.
Что потребуется?
- концепт объясняет только метод аутентификации
- реализация требует хранить только "имя пользователя", "пароль", "соль1" и "соль2". Да, две соли.
- обойдемся без таблиц логирования и счетчиков в redis
- не будем вести таблицы с IP-адресами
- не будем использовать SMS
- не будем блокировать попытки входа в систему. Как известно из моей прошлой неуспешной попытки, бесполезно блокировать вход — даже если хакер упрется в ограничение по времени, он просто начнет подбирать пароли сразу у нескольких пользователей. Кроме того, от ограничений пострадает и сам пользователь. Не звонить же ему в поддержку, чтобы авторизоваться на вашем сайте с прикольными картинками?
- пользователь может поменять пароль в любое время и сделать его недействительным на остальных устройствах. Это обычное правило, но мне кажется его стоит упомянуть.
- можно сделать процесс подбора пароля по словарю более тяжелым для хакера (опционально, будет упомянуто ниже).
Суть метода
Позволить пользователю иметь пароль «12345», а взлом этого пароля должен быть усложнен. Например, как подбирать пароль, который выглядит, как хэш.
Как?
Представьте, если бы в браузере всегда была уникальная соль, которой можно было солить пароль. Каждому пользователю по соли. Зачем она нужна? Чтобы шифровать. Например, если зашифровать строку «12345» с солью «saltsalt» в argon2id, то получится "$argon2id$v=19$m=16,t=2,p=1$c2FsdHNhbHQ$jX94laSi6vo9AhS+bHwbkg". Поменяй соль — и хэш будет другим. Один алгоритм зашифрует одинаковые пароли по-разному, если использовать разную соль для каждого. Годится.
Но где взять эту соль изначально? Да вот же она сидит перед монитором. Пусть выдавит из себя два-три лишних символа и авторизуется уже наконец по-человечески. Рядом кот бегает? Ну, пусть будет cat. Что такое cat? Это наше секретное слово. Мы его сообщим на сервер при регистрации, а он сгенерирует по этому слову соль. А потом эту соль пришлет нам. Все — соль в браузере есть. Теперь пароль. А пароль тоже шифруем и солим той солью, что прислал сервер.
Теперь мы не шлем «12345». Мы шлем хэш, и так-как у каждого пользователя своя соль, хэш получается разный.
Кажется, брутфорсу сейчас поплохеет: мало того, что придется делать дополнительные вычисления и перебирать длинные строки хэшей аргона вместо простых цифр, так еще и у каждого пользователя будет свой хэш — теперь бесполезно пробовать одну и ту же строку в виде пароля для проверки ее у всех пользователей. Допустим, три пользователя выбрали один и тот же пароль: 12345. Но хэш у них получится разный. Потому что у каждого разная соль.
- Браузер должен вычислять хэш пароля используя соль, которую ему ранее прислал сервер. Отправлять он должен хэш, а не сам пароль.
- Сервер присылает соль по секретному слову, которое известно только пользователю. Оно может быть простым. Например — «cat».
- У каждого пользователя должна быть своя соль.
- Два пользователя, выбравшие одинаковое секретное слово, должны иметь разную соль.
- Сервер не должен сообщать было ли использовано правильное секретное слово и верна ли соль для этого пользователя — иначе это будет подбор двух простых паролей вместо одного.
- Если пользователь меняет секретное слово, меняется и соль.
То есть, для защиты своего простого пароля, пользователь должен придумать еще одно очень простое слово. Он вводит это слово везде, где хочет пройти аутентификацию, а потом потребуется вводить только пароль. Пока он не почистит куки.
- зашел на сайт
- ввел логин и секретное слово
- ввел пароль
- готово
Пароль и секретное слово могут быть очень простыми. Один или два символа. Например, пароль 12345 и секретное слово 42. И если кто-то еще придумает секретное слово 42, то это будет не страшно.
Как это работает. Пошаговый концепт
У нас есть следующие элементы:
- веб-сервер
- база данных и таблица users:
- login
- password_hash
- salt_unique_for_each_user
- salt_for_password
- браузер пользователя
- браузер хакера
- страницы логина и регистрации на сайте
- скрипт, который перехватывает событие submit для формы логина
Далее нам понадобятся два разных алгоритма, которые могут быть реализованы даже на одной шифровальной системе просто с разными параметрами:
- ALG1 — асимметричный алгоритм шифрования, который генерирует хэш из строки и соли. ALG1(str, salt) = hash1. Этот алгоритм используется только на сервере.
- ALG2 — асимметричный алгоритм шифрования, который генерирует хэш из строки и соли. ALG2(str, salt) = hash2. Этот алгоритм используется публично и должна быть возможность его реализации на клиенте (в нашем примере на javascript).
Кроме того, нам понадобится еще два алгоритма попроще:
- ALG_SALT — алгоритм, который вычисляет случайную соль в виде строки символов. ALG_SALT() = salt. Этот алгоритм используется только на сервере.
- ALG_PASS — алгоритм, который генерирует случайный простой пароль. ALG_PASS() = pass. Этот алгоритм используется только на сервере.
События пошагово
- Пользователь переходит на страницу регистрации, так-как у него пока нет логина.
- Сервер показывает форму с двумя полями: логин + простое секретное слово.
- Пользователь выбирает логин — alice
- Пользователь выбирает секретное слово — cat
- Пользователь нажимает кнопку “Отправить”.
Cервер проверяет и удостоверяется, что пользователь alice отсутствует в БД.
Сервер вычисляет следующие значения:
$salt_unique_for_each_user = ALG_SALT(); // строка "saltsalt"
$salt_for_password = ALG1("cat", $salt_unique_for_each_user); // строка "$argon2id$v=19$m=16,t=2,p=1$c2FsdHNhbHQ$jX94laSi6vo9AhS+bHwbkg"
$user_simple_password = ALG_PASS(); // строка "12345"
$user_simple_password_hashed = ALG2($user_simple_password , $salt_for_password); // строка "$argon2id$v=19$m=16,t=2,p=1$JGFyZ29uMmlkJHY9MTkkbT0xNix0PTIscD0xJGMyRnNkSE5oYkhRJGpYOTRsYVNpNnZvOUFoUytiSHdia2c$b+6ROJVsZ62UXA7hEAg0AQ"
Сервер создает в таблице пользователей запись и сохраняет данные:
INSERT INTO `users`
(
login,
password_hashed,
salt_unique_for_each_user,
salt_for_password
)
VALUES
(
"alice",
"$argon2id$v=19$m=16,t=2,p=1$JGFyZ29uMmlkJHY9MTkkbT0xNix0PTIscD0xJGMyRnNkSE5oYkhRJGpYOTRsYVNpNnZvOUFoUytiSHdia2c$b+6ROJVsZ62UXA7hEAg0AQ",
"saltsalt",
"$argon2id$v=19$m=16,t=2,p=1$c2FsdHNhbHQ$jX94laSi6vo9AhS+bHwbkg"
).
Сервер показывает пользователю страницу успеха регистрации с сообщением: «Пользователь alice успешно создан. Используйте временный пароль 12345 для входа.»
Пользователь радостно кричит: “Ура, я зарегистрировался на сайте site под ником alice и мне дали пароль 12345. Какой смешной и простой пароль!“. Но у квартиры пользователя очень плохая звукоизоляция, и его хакер-сосед все услышал.
- Хакер вбивает адрес сайта в своем браузере.
- Браузер хакера отсылает пустые куки.
- Сервер проверяет запрос хакера — есть ли кука “salt”. Не находит ее.
- Прежде, чем хакер пришлет украденные логин и пароль, браузер должен знать соль, чтобы ей зашифровать пароль.
- Браузер хакера пока не хранит соль в куке «salt».
- Сервер присылает форму логина с двумя полями: логин + секретное слово, чтобы дать пользователю возможность получить соль.
Хакер озадачен. Пока оставим его.
- Пользователь возвращается на страницу логина.
- Браузер пользователя отсылает пустые куки.
- Сервер проверяет запрос пользователя — есть ли кука «salt». Не находит ее.
- Прежде, чем пользователь пришлет логин и пароль, браузер должен знать соль, чтобы ей зашифровать пароль.
- Браузер пользователя пока не хранит соль в куке «salt».
- Сервер присылает форму логина с двумя полями: логин + секретное слово, чтобы дать пользователю возможность получить соль.
- Пользователь вводит login — alice, secret — cat и нажимает кнопку "Отправить".
Сервер получает запрос и видит, что вместо пароля прислали секретное слово.
- Сервер выбирает запись из базы данных с логином — alice и берет значения `salt_unique_for_each_user` -> $db_salt_unique_for_each_user и `salt_for_password -> $db_salt_for_password`.
- Сервер делает вычисления схожие с теми, что он делал при регистрации. Вычисляет значение: $salt_for_password = ALG1(«cat», $db_salt_unique_for_each_user).
- Сервер отсылает значение соли $salt_for_password в ответе пользователю. Эта соль правильная. Если с ее помощью зашифровать пароль 12345, получится хэш, который сейчас хранится в БД. В заголовках ответа от сервера указано — `установить куку salt = $db_salt_for_password`. Также давайте сохраним и логин: `установить куку login = «alice»`.
Пояснение: Сервер никак не уведомляет какая соль была отправлена — правильная или нет. Результат ее использования будет ясен, когда с ней попытаются авторизоваться с правильными логином и паролем.
- Пользователь получает ответ сервера. Его страница либо перегружается, либо сразу динамически меняется.
- Браузер пользователя отсылает куки: login = alice, salt = "$argon2id$v=19$m=16,t=2,p=1$c2FsdHNhbHQ$jX94laSi6vo9AhS+bHwbkg".
- Сервер проверяет запрос пользователя — есть ли кука “salt”. Находит ее.
- Браузер уже имеет соль, чтобы ей зашифровать пароль.
- Сервер присылает форму логина с двумя полями: логин (уже имеет значение alice) + пароль.
- Пользователь вводит свой простой пароль 12345 и нажимает кнопку "Отправить".
- Браузер перехватывает событие onSubmit.
- Вычисляет $password_hashed = ALG2(«12345», "$argon2id$v=19$m=16,t=2,p=1$c2FsdHNhbHQ$jX94laSi6vo9AhS+bHwbkg").
- Отправляет данные «alice»/$argon2id$v=19$m=16,t=2,p=1$JGFyZ29uMmlkJHY9MTkkbT0xNix0PTIscD0xJGMyRnNkSE5oYkhRJGpYOTRsYVNpNnZvOUFoUytiSHdia2c$b+6ROJVsZ62UXA7hEAg0AQ, а сам пароль 12345 никуда не шлет.
Сервер получает запрос на аутентификацию:
- Данные логин+пароль: «alice»/$password_hashed
- Идет в БД, достает значение `password_hashed` -> $db_password_hashed.
- Сравнивает $db_password_hashed === $password_hashed?
- Хэши совпадают, авторизация успешна.
Примечание: В моем примере сервер сличает хэши напрямую. Но хранить в БД строки, которые по факту уже являются паролями, нельзя. Их можно украсть и потом использовать в форме логина-пароля. Поэтому нужно хэшировать хэши — как бы странно это не звучало. Это значит, что понадобится третья соль. Но хранить ее надо не в БД, а в переменной окружения. Впрочем, это уже детали реализации, которые я пропустил для упрощения.
Тем временем наш хакер решает проверить эту странную форму входа:
- Хакер вводит login — alice, secret — dog и нажимает кнопку "Отправить".
- Сервер получает запрос хакера и видит, что вместо пароля прислали секретное слово.
- Сервер выбирает запись из базы данных с логином — alice и берет значения `salt_unique_for_each_user` -> $db_salt_unique_for_each_user и `salt_for_password` -> $salt_for_password.
- Сервер вычисляет значение соли и выдает ее, но она неправильная, потому что кодовое слово чужое: $result_fake_salt = ALG1(«dog», $db_salt_unique_for_each_user). Впрочем, сервер об этом тактично умалчивает.
Сервер отсылает вычисленное значение соли обратно в браузер пользователя. В заголовках указано — `установить куку salt = $result_fake_salt`. Также сохраняется и логин: `установить куку login = «alice»`.
Пояснение: Чтобы помочь хакеру в деле нелегкого труда, сервер отправляет ему соль. Но определить со стороны: правильное ли было секретное слово или нет — невозможно.
- Хакер получает ответ сервера. Его страница либо перегружается, либо сразу динамически меняется.
- Браузер хакера отсылает куки: login = alice, salt = $result_fake_salt.
- Сервер проверяет запрос пользователя — есть ли кука «salt». Находит ее.
- Браузер хакера уже имеет соль, чтобы ей зашифровать пароль.
- Сервер присылает форму логина с двумя полями: логин (уже имеет значение alice) + пароль.
- Хакер вводит украденный простой пароль 12345 и нажимает кнопку "Отправить".
- Браузер перехватывает событие onSubmit.
- Вычисляет $password_hashed = ALG2(«12345», $result_fake_salt).
- Отправляет данные «alice»/$password_hashed.
Сервер получает запрос на аутентификацию — «alice»/$password_hashed.
Идет в БД, достает значение `password_hashed` -> $db_password_hashed.
Сравнивает: $password_hashed === $db_password_hashed? Nope.
Хэши этих изначально одинаковых паролей не совпадают. Потому что их солили по-разному.
Хакер не сдается и идет регистрировать другого пользователя на сайте.
Совершенно случайно он вводит то же самое секретное слово, что и пользователь за стеной — cat.
Хакер получает валидную соль для пароля к новому аккаунту, и пробует ее подставить в скрипт для хэширования.
К счастью, генерация соли для паролей использовала вторую соль (`salt_unique_for_each_user`), которая для каждого пользователя генерируется по-новому. Так что разные пользователи даже с одинаковыми паролями и — что самое главное — секретными словами, будут иметь разные соли. И соль пользователя с тем же секретным словом, не совпадет с солью другого. И совпадение паролей тоже не будет являться проблемой.
Теперь, что касается усложнения перебора паролей по словарю. Если мы модифицируем ALG2, который является общим и для сервера и для клиента, и сделаем его трудозатратным, это серьезно осложнит перебор для хакера. Напомню, ALG2 это процесс получения хэша пароля, который отправляется на сервер. На сервере этот хэш уже вычислен и хранится в БД:
- сервер будет выполнять операцию ALG2 только один раз при записи пароля в БД или смене пароля на новый
- клиент будет выполнять операцию ALG2 только во время аутентификации (которую нужно не путать с авторизацией). Допустим, клиент ошибся пару раз при вводе пароля — это не страшно.
- Хакер будет делать это постоянно для каждого пароля, с чем его и можно будет поздравить. Особенно цинично, что будут затрачиваться титанические усилия на пароли типа 123/1234/12345.
На слабых машинах операция может выполняться значительно дольше, чем на быстрых. Это может стать проблемой. Так что можно не делать усложнение алгоритма.
Завершу описание концепта бочкой дегтя:
- Если пользователь случайно неправильно введет секретное слово, он попадет в ситуацию, когда он не сможет войти по своему паролю. Придется сбросить секретное слово (в нашем случае удалить куки) и послать запрос заново. Это можно реализовать прозрачно по нажатию одной кнопки, но до этого пользователь должен еще догадаться. Можно сбрасывать принудительно при 5 неправильных попытках входа.
- Два пользователя на одном компьютере вынуждены будут постоянно сбрасывать соль друг друга.
- Два разных компьютера будут получать одну и ту же соль для пароля
- Если соль сменится на сервере через один компьютер, другой компьютер со старой солью не будет знать, что ее нужно поменять
- Можно украсть соль с компьютера и с ее помощью осуществить очень быструю атаку на аккаунт, зная что пароль очень простой.
… и ложкой меда:
- пользователь может иметь несколько секретных слов для выполнения различных задач. Например, "cat" это зайти на почту, а "termorectal" — это показать фейковую страницу с ничем не примечательными письмами. Конечно, два пароля можно организовать в любой системе. Но второй пароль должен быть таким же сложным, как и первый. Здесь же можно помнить два простых пароля любого вида, используя удобочитаемые слова.
- Возможна интеграция секретного слова в уже существующие системы аутентификации. Если у пользователя значение в БД `salt_for_password` не пустое, значит, что пользователь придумал секретное слово, и можно применять новый метод аутентификации. В противном случае использовать старый аутентификатор.
Gigatrop
Поясните чем это отличается от двух паролей сразу или от префикса пароля? Вы в начале говорите, что человек не боится рассказать пароль. Но ведь если он расскажет два пароля, то его взломают. А если жена не знает второго пароля (соль/префикс) или его сменить тоже, то она не сможет зайти по одному паролю.
dso Автор
1) Два простых пароля — это пароль чуть сложнее. В моем примере простое секретное слово превращается в хэш, который используется, чтобы солить сам пароль. Это не два пароля вместе. Хэш секретного слова солится на сервере солью, которая нигде не публикуется.
2) Жена знает секретное слово — его можно оставлять прежним. А пароль можно менять и сообщать публично. Но только в троллейбусе, и только если Вашу жену зовут Галя :) Шучу, конечно. Я просто пытался обыграть ситуацию, что простой пароль как будто бы уже известен.
Gigatrop
На мой взгляд это просто реализация пароля. То же самое, что брать у каждого пароля первых три символа и солить ими остальную часть пароля. Но это технически. А практически думаю это может быть полезно. Для тех, кто любит поля ввода и боится сделать что-то не так. Про соль объяснить им сложнее. Так что пример с женой весьма рабочий :)
dso Автор
Если Вы берете первые три символа у пароля, тоже самое сделает и скрипт хакера. В моем случае хакер не знает чем солить. Он не может сгенерировать соль, не зная секретного слова. А подставлять просто секретное слово тоже не получится. Его должен захэшировать сервер своей солью, которую хакер не знает.
Предположим самое худшее — хакер решил брутфорсить не пароль, а секретное слово. Логин он знает, пароль тоже. Ему осталось зашифровать пароль и отправить его на сервер. Алгоритм хэширования у него тоже есть. Остался сущий пустяк — соль. Но какая соль правильная? Придется ее перебирать и проверять — но это возможно только если вы точно знаете, что пароль правильный.
Как этому можно противостоять? Усилить алгоритм ALG2 сложными расчетами, чтобы максимально усложнить перебор секретного слова с заведомо известным паролем.
Но даже без этого, скомпрометированным паролем воспользоваться сразу не удастся.
Gigatrop
Представьте, что у меня пароль SaltMypas. Я говорю жене, что любой мой пароль на любых сайтах начинается всегда с Salt, потом идёт Mypas. Потом не боюсь ей в маршрутке сказать, что было Mypas, а стало Mysuperpas. Вот такой префикс я имел в виду. Его хакер по вашей логике тоже не знает. И брутфорс будет такой же по смыслу. Но не придётся делать лишнее поле, и этот способ уже работает везде. А сложность соления и алгоритмы можно делать на любой реализации.
dso Автор
Да, согласен.
Жаль, выглядело неплохо, но действительно моя идея ничем не отличается от пароля, который слепили из двух частей.
С точки зрения хакера, он делает запросы «слово + пароль» для каждого аккаунта и получает все комбинации. Единственное, что его стесняет это дополнительные запросы к серверу для получения соли для каждого из пользователей вместо того, чтобы сразу получить ответ на нужную склеенную пару. А это и наши расходы в том числе. Их можно создать и без дополнительных солей.
Жаль, что не увидел это сразу.
sheknitrtch
Современные алгоритмы хеширования достаточно быстрые. Можно генерировать 70 000 SHA-512 хэшей в секунду. Сервер будет дольше обрабатывать запросы, чем хакер отправлять варианты. Можно взять медленную функцию ALG2, но всё равно если пароль известен, то слабое место — это секретное слово.
kipar
Современные алгоритмы хеширования паролей (KDF) медленные by design, как раз чтобы усложнить взлом (а еще используют много памяти чтобы мешать брутфорсу на GPU). SHA-512 использовать для этой цели некорректно, а скажем, 10000 проходов SHA-512 — уже лучше.