Как правильно организовать работу с паролями
Что проверяют
Отличаете ли вы хеширование от шифрования и понимаете ли, почему пароль нельзя хранить как обычные данные. Следом обычно просят показать, как это делается в Spring Security и как перевести базу со старого алгоритма на новый.
Как отвечать
Пароль хранится только как результат медленной адаптивной хеш-функции, восстановить его нельзя даже вам.
- алгоритм: Argon2id, bcrypt, scrypt или PBKDF2 с большим числом итераций. MD5 и SHA-256 не подходят даже с солью: они быстрые, и видеокарта перебирает миллиарды вариантов в секунду;
- соль: уникальная для каждого пароля, её генерирует энкодер и хранит внутри строки хеша. Отдельная колонка не нужна;
- Spring Security: интерфейс
PasswordEncoder, реализацииBCryptPasswordEncoderиArgon2PasswordEncoder(второму нужна BouncyCastle). Проверка только черезmatches(), а не сравнением строк; - миграция алгоритма:
PasswordEncoderFactories.createDelegatingPasswordEncoder()пишет префикс вида{bcrypt}и умеет проверять старые хеши, аupgradeEncoding()подсказывает, когда перехешировать пароль при успешном входе; - политика паролей: актуальная редакция NIST SP 800-63B делает упор на длину, запрещает обязательные спецсимволы и смену по расписанию, советует проверять пароль по базам утечек;
- вход и сброс: ограничение частоты попыток, одинаковый ответ для существующего и несуществующего логина, одноразовый токен сброса с коротким сроком жизни, который в базе тоже лежит хешем.
Для Java есть ещё деталь: пароль лучше держать в char[] и затирать после использования, потому что String неизменяем и живёт в памяти до сборки мусора. В веб-приложении он всё равно приходит строкой, так что это скорее про десктоп и CLI.
Где спотыкаются
Говорят «пароли шифруем». Шифрование обратимо: утёк ключ, утекли все пароли разом.
Выбирают bcrypt, не зная про лимит: учитываются только первые 72 байта. Без ограничения длины на форме два длинных пароля с общим началом становятся одним, а кириллица в UTF-8 укладывается всего в 36 символов.
Забывают про логи. DTO с полем password попадает в лог запросов или в toString(), и хеширование в базе уже не спасает.