Вопрос или проблема
Недавно я пытался реализовать собственную безопасность в скрипте входа, который наткнулся в интернете. После долгих попыток узнать, как сделать свой собственный скрипт для генерации соли для каждого пользователя, я наткнулся на password_hash
.
Насколько я понимаю (согласно чтению на этой странице), соль уже генерируется в строке, когда вы используете password_hash
. Это правда?
Другой вопрос, который у меня возник, не было бы разумно иметь 2 соли? Одна непосредственно в файле и одна в БД? Таким образом, если кто-то компрометирует вашу соль в БД, у вас все еще будет одна, прямо в файле? Я прочитал здесь, что хранить соли никогда не является разумной идеей, но мне всегда было непонятно, что люди имеют в виду под этим.
Использование password_hash
является рекомендуемым способом хранения паролей. Не разделяйте их на БД и файлы.
Предположим, у нас есть следующий ввод:
$password = $_POST['password'];
Сначала вы хешируете пароль, делая это:
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
Затем посмотрите на вывод:
var_dump($hashed_password);
Как вы можете видеть, он хеширован. (Я предполагаю, вы выполнили эти шаги).
Теперь вы храните этот хешированный пароль в вашей базе данных, убедившись, что колонка пароля достаточно велика, чтобы вместить хешированное значение (как минимум 60 символов или больше). Когда пользователь запрашивает вход, вы проверяете введенный пароль с этим хеш-значением в базе данных, делая это:
// Запрос к базе данных для имени пользователя и пароля
// ...
if(password_verify($password, $hashed_password)) {
// Если введенные пароли совпадают с хешированным паролем в базе данных
// Сделайте что-то, вы знаете... войдите в систему.
}
// В противном случае, перенаправьте их обратно на страницу входа.
Да, вы все поняли правильно, функция password_hash() будет генерировать соль сама и включает ее в результирующее значение хеша. Хранение соли в базе данных абсолютно правильно, она выполняет свою работу, даже если известна.
// Хешируйте новый пароль для хранения в базе данных.
// Функция автоматически генерирует криптографически безопасную соль.
$hashToStoreInDb = password_hash($_POST['password'], PASSWORD_DEFAULT);
// Проверьте, совпадает ли хеш введенного пароля с хранимым хешем.
// Соль и фактор стоимости будут извлечены из $existingHashFromDb.
$isPasswordCorrect = password_verify($_POST['password'], $existingHashFromDb);
Вторая соль, о которой вы упомянули (та, что хранится в файле), на самом деле является перцем или серверным ключом. Если вы добавите его перед хешированием (как соль), то добавите перец. Есть и лучший способ: вы можете сначала рассчитать хеш, а затем зашифровать (двунаправленно) хеш с помощью серверного ключа. Это дает вам возможность изменять ключ, когда это необходимо.
В отличие от соли, этот ключ должен храниться в секрете. Люди часто путают это и пытаются скрыть соль, но лучше позволить соли выполнять свою работу и добавить секрет с ключом.
Существует явная нехватка обсуждений о совместимости назад и вперед, которые встроены в функции паролей PHP. В частности:
- Совместимость назад: Функции пароля, по сути, являются хорошо написанной оберткой вокруг
crypt()
и по своей природе совместимы с хешами форматаcrypt()
, даже если они используют устаревшие и/или небезопасные алгоритмы хеширования. - Совместимость вперед: Вставка
password_needs_rehash()
и немного логики в ваш процесс аутентификации может поддерживать ваши хеши в актуальном состоянии с современными и будущими алгоритмами с потенциально нулевыми изменениями в будущем потоке работы. Примечание: Любая строка, которая не соответствует указанному алгоритму, будет отмечена как нуждающаяся в повторном хешировании, включая несовместимые с crypt хеши.
Например:
class FakeDB {
public function __call($name, $args) {
printf("%s::%s(%s)\n", __CLASS__, $name, json_encode($args));
return $this;
}
}
class MyAuth {
protected $dbh;
protected $fakeUsers = [
// старый формат crypt-md5
1 => ['password' => '$1$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
// старый формат salted md5
2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
// текущий формат bcrypt
3 => ['password' => '$2y$10$3eUn9Rnf04DR.aj8R3WbHuBO9EdoceH9uKf6vMiD7tz766rMNOyTO']
];
public function __construct($dbh) {
$this->dbh = $dbh;
}
protected function getuser($id) {
// просто сделайте вид, что эти данные приходят из БД
return $this->fakeUsers[$id];
}
public function authUser($id, $password) {
$userInfo = $this->getUser($id);
// У вас есть старые, супер-ударные, несовместимые с crypt хеши?
if( strpos( $userInfo['password'], '$' ) !== 0 ) {
printf("%s::legacy_hash\n", __METHOD__);
$res = $userInfo['password'] === md5($password . $userInfo['salt']);
} else {
printf("%s::password_verify\n", __METHOD__);
$res = password_verify($password, $userInfo['password']);
}
// После проверки мы можем проверить, нужно ли обновить хеш.
if( $res && password_needs_rehash($userInfo['password'], PASSWORD_DEFAULT) ) {
printf("%s::rehash\n", __METHOD__);
$stmt = $this->dbh->prepare('UPDATE users SET pass = ? WHERE user_id = ?');
$stmt->execute([password_hash($password, PASSWORD_DEFAULT), $id]);
}
return $res;
}
}
$auth = new MyAuth(new FakeDB());
for( $i=1; $i<=3; $i++) {
var_dump($auth->authuser($i, 'foo'));
echo PHP_EOL;
}
Вывод:
MyAuth::authUser::password_verify
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$zNjPwqQX\/RxjHiwkeUEzwOpkucNw49yN4jjiRY70viZpAx5x69kv.",1]])
bool(true)
MyAuth::authUser::legacy_hash
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$VRTu4pgIkGUvilTDRTXYeOQSEYqe2GjsPoWvDUeYdV2x\/\/StjZYHu",2]])
bool(true)
MyAuth::authUser::password_verify
bool(true)
В качестве последней заметки, учитывая, что вы можете повторно хешировать пароль пользователя только при входе, вам следует рассмотреть возможность «постепенного вывода» небезопасных устаревших хешей, чтобы защитить ваших пользователей. Под этим я подразумеваю, что после определенного срока вы удаляете все небезопасные [например: голые MD5/SHA/иначе слабые] хеши и заставляете своих пользователей полагаться на механизмы сброса паролей вашего приложения.
Да, это правда. Почему вы сомневаетесь в ответах на php faq по этой функции? 🙂
Результат выполнения password_hash()
состоит из четырех частей:
- используемый алгоритм
- параметры
- соль
- фактический хеш пароля
Как видите, хеш является частью этого.
Конечно, вы можете иметь дополнительную соль для добавочного уровня безопасности, но я честно думаю, что это излишне в обычном php приложении. Алгоритм bcrypt по умолчанию хороший, а необязательный алгоритм blowfish, возможно, даже лучше.
Чтобы сделать пароль в базе данных более безопасным в случае взлома базы данных, вы можете создать несколько колонок с электронной почтой и паролем (электронная почта также должна быть закодирована, на мой взгляд), например: колонки от email_1 до email_7 и колонки от password_1 до password_7. Пароль можно хранить в одной из колонок электронной почты, а электронную почту — в одной из колонок паролей. В оставшихся шести мы можем хранить зашифрованную случайную строку символов. Во-вторых, при хешировании мы можем использовать различные функции хеширования, например наш пароль $password,
$password = hash('sha384', $password);
$password = hash('sha256', $password);
$password = hash('sha512', $password);
$password = hash('guest', $password);
$h1 = hash('md5', $password);
$h2 = hash('md5', $h1);
$h3 = hash('md5', $h2);
$h4 = hash('md5', $h3);
$hash_db = $h1 . $h2 . $h3 . $h4;
Мы храним $hash_db
в базе данных. Хеш имеет длину 128 символов, похоже, что используется алгоритм sha512.
Ответ или решение
Использование функции password_hash
в PHP для хеширования и проверки паролей является наиболее рекомендуемым подходом для обеспечения безопасности паролей пользователей. В этом ответе мы подробно рассмотрим, как правильно хешировать пароли и проверять их, а также ответим на дополнительные вопросы о соли и дополнительных мерах безопасности.
Хеширование паролей с помощью password_hash
-
Хеширование пароля:
Вы начинаете с получения пароля от пользователя, например, из формы входа:$password = $_POST['password'];
Затем вы применяете
password_hash
, чтобы сгенерировать хеш:$hashed_password = password_hash($password, PASSWORD_DEFAULT);
Функция
password_hash
автоматически генерирует криптографически безопасную соль и включает ее в результирующий хеш. Хеш будет выглядеть примерно так:var_dump($hashed_password);
Важно: Убедитесь, что колонка в вашей базе данных, где вы храните хеши паролей, достаточно большая (минимум 60 символов).
-
Проверка пароля:
Когда пользователь пытается войти в систему, вам нужно сравнить введенный пароль с сохранённым хешом в базе данных:// Запрос к базе данных для получения хешированного пароля пользователя // ... if (password_verify($password, $hashed_password)) { // Если пароли совпадают, выполните вход пользователя } else { // В противном случае перенаправьте на страницу входа }
Ответы на ваши вопросы
-
Соль: Да, вы правильно поняли. Функция
password_hash
автоматически генерирует соль и включает ее в результирующий хеш. Это исключает необходимость хранения соли отдельно. - Дополнительная соль или перец: Идея использования двух солей (одной в файле, другой в базе данных) может показаться разумной, но это может быть излишним для большинства приложений. Обычно, основная соль в хеше выполняет свою работу хорошо, и добавление дополнительной соли может создать лишние сложности без значительного увеличения безопасности. Секрет, который вы обсуждаете, обычно называется "перец". Если решите использовать его, то лучше добавить его перед хешированием пароля. Нужно иметь в виду, что перец должен оставаться секретным.
Обеспечение совместимости
Для управления совместимостью хешей с разными алгоритмами вы можете использовать функции password_needs_rehash
. Это позволит вам обновлять хеши на основе текущих требований:
if ($isPasswordCorrect && password_needs_rehash($hashed_password, PASSWORD_DEFAULT)) {
$new_hash = password_hash($input_password, PASSWORD_DEFAULT);
// Сохраните новый хеш в базе данных
}
Вывод
Использование password_hash
и password_verify
— это наиболее безопасный способ обработки паролей в PHP. Они обеспечивают надежные и обновляемые хеши без необходимости управлять солями самостоятельно. Не стоит усложнять архитектуру хранения паролей, если это не необходимо. Храните пароли безопасно и следите за их интегритетом с использованием предложенных методов.