НИЛИТ П::01::Вводная и бонусом - как можно безопасно хранить и проверять пароли
Уф... В процессе суровой почти круглосуточной работы в последние 2 недели наметился просвет (в 23:30, что символично...). Так вот, подумал я, подумал, да и решил публиковать маленькие хитрости ИТ до которых дошел сам (что не исключает, что кто-то дошел до них раньше/позже).
Итак, прежде всего обо мне нужно знать 2 вещи:
1. Говнокодю пишу (или это надо зачеркнуть?) я в подавляющем случае для себя.
2. Я суровый и бородатый, поэтому библиотеки, а уж тем более фреймворки использую только когда пустив скупую слезу признаю, что в данном вопросе я тупой ламер и сам ничего сделать не могу.
Еще мой большой недостаток - работа в "режиме FullStack". Те кто читали/читают Хабр наверно думают, что FS - это миф (https://habrahabr.ru/company/Voximplant/blog/275229/), но я больше склоняюсь к такой концепции (с того же Хабра) https://habrahabr.ru/post/278467/
Когда мой знакомый (очень уважаемый человек, запредельно классный специалист в своей узкой области) узнал чем и как я занимаюсь он сказал такую поговорку: Лебедь умеет ходить, летать и плавать, но делает он это плохо (в значении, что рыбы лучше плавают, соколы лучше летают, гепарды быстрее бегают).
Можно разжечь цельный холивар о том что лучше FS или OO (Only One - разработчик только в одной технологии), но лучше сразу признать, что нужны обе категории специалистов, а с учетом современных реалий для 95-99% задач нужны именно FS разработчики. Почему? Потому что в 95-99% случаев заказчику требуется куча всего и он не настолько квалифицирован чтобы отличить говнокод от высококлассного. Поэтому (к моему глубочайшему сожалению) говнокодеры нынче в моде.
Итак, дабы потешить ЧСВ и взяв пример с коммунистов (нормальных, а не КПРФ) я решил немного заняться ликвидацией безграмотности (ликбезом). Для этого в перерывах основной работы создается специализированная платформа по популяризации, обучению и созданию действительно свободной биржи фриланса (управляемой сообществом). О чем по готовности будет пост.
С вводной частью покончили, теперь о набивших оскомину паролях и их хранении. Поскольку я теперь несколько "научно-исследовательский", то сперва немного занудства (ну и переписывания терминов в "ламер-стайл"):
логин - находящийся в свободном доступе набор символов который служит для опознания пользователя (идентификации, так сказать).
пароль - секретный набор символов, которые служат для подтверждения соответствия логина пользователю. Пароли хранят в файлах, чаще всего пароли хранятся в текстовых, специальных (например, базах данных) файлах и являются целью № 1 при взломе.
Пароли в открытом виде хранятся только в двух случаях:
1. У полных идиотов
2. Для обеспечения автоматического использования в сторонних программах
Во всех остальных случаях хранят не пароли, а их хеши. Хеш-функция - односторонняя функция преобразования с потерей точности. Самая простая иллюстрация хеш-функции это остаток от деления. Например простейший хеш можно изобразить как: хеш = остаток(число / 12345). Совершенно очевидно, что если число больше чем 12345 то одному хешу будет соответствовать несколько значений, а значит исходным паролем может быть любой из возможных вариантов. Это приводит к двум очевидным особенностям:
Положительная: даже если хеш подобран, это не гарантирует что именно это решение - верное. Если один и тот же пароль используется на нескольких сайтах, то при небольшом везении можно частично избежать взлома всего.
Отрицательная: как пароль подойдет любой из возможных вариантов хеша.
Атаки на хеши разнообразны и порой весьма оригинальны, но самой действенной атакой остается банальный перебор вариантов (особенно помогают "Радужные таблицы" https://ru.wikipedia.org/wiki/Радужная_таблица ). Хорошая статья по методам защиты от перебора написана здесь: https://habrahabr.ru/post/139974/
Так вот, посидел я, подумал и решил, что пора двигаться дальше, потому что предложенное в статье решение хоть и рабочее, но приводит к сильной нагрузке на сервер. В принципе это не проблема если ресурс очень слабо нагружен или есть много денег на кучу серверов. Но если мы бедны и убоги?
А вот если мы бедны и убоги, то нам поможет следующая абсурдная конструкция (алгоритм, думаю перенести его на любой язык программирования не составит труда):
Регистрация:
Запрашиваем: логин, пароль
Случайно генерируем "соль" т.е. последовательность символов.
Вычисляем длину пароля.
Генерируем n паролей той же длинны и заносим в массив n[x] (лучше чтобы 30-40 % вариантов были из радужных таблиц)
Вычисляем хеши логин+соль+n[x] и заносим его в отдельную таблицу (отдельная таблица для y пользователей)
Обход цикла n (циклично проходим по массиву подставляя значения n): Вычисляем хеши логин+соль+n[x] и заносим эти значения в ту же таблицу
Авторизация:
Запрашиваем: логин, пароль
Вычисляем хеши логин+соль+n[x] и сверяем его с таблицей
Если хеш в таблице есть, то авторизацию разрешаем
В чем плюсы:
- низкая нагрузка на сервер
- нужно пересчитывать пароли для каждого пользователя
- есть n вероятностей паролей, в т.ч. такие которые пользователь реально мог использовать, а значит нужно сделать n попыток авторизации на сторонних сайтах от имени данного пользователя.
В чем минусы:
- к аккаунту подходят y*n паролей
Этот минус можно нивелировать если детектировать перебор паролей как связку IP/логин и разрешать запросы на авторизацию по 1 в 5-10 секунд.
Разумная критика приветствуется.
Что-то не уловил, в чем выигрыш вашей схемы.
Классика - это когда каждый пароль хранится в хэшированном виде вместе со своей солью (см. /etc/passwd, /etc/shadow).
Тогда берем присланный пароль, берем из таблички уникальную соль, которая для данного юзера сгенерирована, и получаем хэш, который должен подойти.
У вас, получается, соль будет одна на всех, я правильно понимаю? По факту у вас получается модифицированная хэш-функция, которая от обычной отличается только тем, что начальные значения переменных не "канонические", а соответствуют состоянию, когда прохэширована заранее известная соль.
Ну а в качестве самой соли вы предлагаете использовать логин и/или IP.
То, что вместо прямого сравнения с образцом предлагается искать полученный хэш в таблице, мало что меняет. Разве что не дает использовать некоторые схемы авторизации.
Например, присылаем клиенту соль и уникальное число - nonce - и получаем от него хэш hash('nonce:' + hash('salt:password')). На стороне сервера хэшируем то же самое (hash('salt:password') нам уже известен) и сравниваем с присланным - если совпало, то клиент молодец, а если нет - то не молодец и не клиент. Большой плюс этой схемы - пароль не передается по сети в открытом виде и не хранится на сервере, а значит, его не получится перехватить прослушиванием трафика.
Конечно, плохо хранить пароли на сервере в открытом виде, но. Если вашу базу данных увели - поздно пить боржоми, там и без паролей есть чем поживиться (фио-адреса-телефоны-явки-карты-емейлы и т.п.).