Предисловие
Начиная с определения вашей таблицы:
- UserID
- Fname
- Lname
- Email
- Password
- IV
Вот изменения:
- Поля
Fname
, Lname
и Email
будут зашифрованы с помощью симметричного шифра, предоставляемую OpenSSL ,
- В
IV
поле будет храниться вектор инициализации, используемый для шифрования. Требования к хранилищу зависят от используемого шифра и режима; подробнее об этом позже.
Password
Поле будет хэшируются с использованием одностороннюю хэш пароля,
Шифрование
Шифр и режим
Выбор лучшего шифра и режима шифрования выходит за рамки этого ответа, но окончательный выбор влияет на размер как ключа шифрования, так и вектора инициализации; для этого поста мы будем использовать AES-256-CBC, который имеет фиксированный размер блока 16 байтов и размер ключа 16, 24 или 32 байта.
Ключ шифрования
Хороший ключ шифрования - это двоичный большой двоичный объект, который генерируется надежным генератором случайных чисел. Рекомендуется следующий пример (> = 5,3):
$key_size = 32; // 256 bits
$encryption_key = openssl_random_pseudo_bytes($key_size, $strong);
// $strong will be true if the key is crypto safe
Это можно сделать один или несколько раз (если вы хотите создать цепочку ключей шифрования). Держите их как можно конфиденциальнее.
IV
Вектор инициализации добавляет случайности к шифрованию и требуется для режима CBC. Эти значения в идеале должны использоваться только один раз (технически один раз для каждого ключа шифрования), поэтому обновление любой части строки должно восстанавливать ее.
Предоставляется функция, которая поможет вам сгенерировать IV:
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
пример
Давайте зашифруем поле имени, используя предыдущие $encryption_key
и $iv
; для этого мы должны дополнить наши данные размером блока:
function pkcs7_pad($data, $size)
{
$length = $size - strlen($data) % $size;
return $data . str_repeat(chr($length), $length);
}
$name = 'Jack';
$enc_name = openssl_encrypt(
pkcs7_pad($name, 16), // padded data
'AES-256-CBC', // cipher and mode
$encryption_key, // secret key
0, // options (not used)
$iv // initialisation vector
);
Требования к хранилищу
Зашифрованный вывод, как и IV, является двоичным; сохранение этих значений в базе данных может быть выполнено с использованием определенных типов столбцов, таких как BINARY
или VARBINARY
.
Выходное значение, как и IV, является двоичным; чтобы сохранить эти значения в MySQL, рассмотрите возможность использования столбцов BINARY
илиVARBINARY
. Если это не вариант, вы также можете преобразовать двоичные данные в текстовое представление, используя base64_encode()
или bin2hex()
, для этого потребуется от 33% до 100% больше места для хранения.
Дешифрирование
Расшифровка сохраненных значений аналогична:
function pkcs7_unpad($data)
{
return substr($data, 0, -ord($data[strlen($data) - 1]));
}
$row = $result->fetch(PDO::FETCH_ASSOC); // read from database result
// $enc_name = base64_decode($row['Name']);
// $enc_name = hex2bin($row['Name']);
$enc_name = $row['Name'];
// $iv = base64_decode($row['IV']);
// $iv = hex2bin($row['IV']);
$iv = $row['IV'];
$name = pkcs7_unpad(openssl_decrypt(
$enc_name,
'AES-256-CBC',
$encryption_key,
0,
$iv
));
Аутентифицированное шифрование
Вы можете дополнительно улучшить целостность сгенерированного зашифрованного текста, добавив подпись, созданную на основе секретного ключа (отличного от ключа шифрования) и зашифрованного текста. Перед расшифровкой зашифрованного текста сначала проверяется подпись (предпочтительно с помощью метода сравнения с постоянным временем).
пример
// generate once, keep safe
$auth_key = openssl_random_pseudo_bytes(32, $strong);
// authentication
$auth = hash_hmac('sha256', $enc_name, $auth_key, true);
$auth_enc_name = $auth . $enc_name;
// verification
$auth = substr($auth_enc_name, 0, 32);
$enc_name = substr($auth_enc_name, 32);
$actual_auth = hash_hmac('sha256', $enc_name, $auth_key, true);
if (hash_equals($auth, $actual_auth)) {
// perform decryption
}
Смотрите также: hash_equals()
Хэш
По возможности следует избегать хранения обратимого пароля в вашей базе данных; вы хотите только проверить пароль, а не знать его содержимое. Если пользователь теряет свой пароль, лучше позволить ему сбросить его, а не отправлять им исходный (убедитесь, что сброс пароля может быть выполнен только в течение ограниченного времени).
Применение хеш-функции - это односторонняя операция; впоследствии его можно безопасно использовать для проверки без раскрытия исходных данных; Что касается паролей, то для их обнаружения можно использовать метод грубой силы из-за его относительно короткой длины и плохого выбора пароля многими людьми.
Алгоритмы хеширования, такие как MD5 или SHA1, были созданы для проверки содержимого файла по известному значению хеш-функции. Они значительно оптимизированы, чтобы сделать эту проверку максимально быстрой, но при этом оставаясь точной. Учитывая относительно ограниченное пространство для вывода, было легко создать базу данных с известными паролями и соответствующими выходными хэш-значениями - радужными таблицами.
Добавление соли к паролю перед хешированием сделало бы радужную таблицу бесполезной, но недавние аппаратные достижения сделали поиск методом грубой силы жизнеспособным подходом. Вот почему вам нужен алгоритм хеширования, который заведомо медленный и просто невозможно оптимизировать. Он также должен иметь возможность увеличивать нагрузку на более быстрое оборудование, не влияя на возможность проверки существующих хэшей паролей, чтобы сделать их надежными в будущем.
В настоящее время доступны два популярных варианта:
- PBKDF2 (функция вывода ключей на основе пароля v2)
- bcrypt (он же Blowfish)
В этом ответе будет использоваться пример с bcrypt.
поколение
Хеш пароля можно сгенерировать так:
$password = 'my password';
$random = openssl_random_pseudo_bytes(18);
$salt = sprintf('$2y$%02d$%s',
13, // 2^n cost factor
substr(strtr(base64_encode($random), '+', '.'), 0, 22)
);
$hash = crypt($password, $salt);
Соль создается с помощью, openssl_random_pseudo_bytes()
чтобы сформировать случайный блок данных, который затем обрабатывается base64_encode()
и strtr()
соответствует требуемому алфавиту [A-Za-z0-9/.]
.
В crypt()
функции выполняет хэширование на основе алгоритма ( $2y$
для Blowfish), фактор стоимости (коэффициент 13 занимает примерно 0.40s на машине 3GHz) и соли 22 символов.
Проверка
После того, как вы выбрали строку, содержащую информацию о пользователе, вы проверяете пароль следующим образом:
$given_password = $_POST['password']; // the submitted password
$db_hash = $row['Password']; // field with the password hash
$given_hash = crypt($given_password, $db_hash);
if (isEqual($given_hash, $db_hash)) {
// user password verified
}
// constant time string compare
function isEqual($str1, $str2)
{
$n1 = strlen($str1);
if (strlen($str2) != $n1) {
return false;
}
for ($i = 0, $diff = 0; $i != $n1; ++$i) {
$diff |= ord($str1[$i]) ^ ord($str2[$i]);
}
return !$diff;
}
Чтобы проверить пароль, вы звоните еще crypt()
раз, но передаете ранее вычисленный хэш в качестве значения соли. Возвращаемое значение дает тот же хеш, если данный пароль совпадает с хешем. Для проверки хэша часто рекомендуется использовать функцию сравнения с постоянным временем, чтобы избежать временных атак.
Хеширование паролей с PHP 5.5
PHP 5.5 представил функции хеширования паролей, которые вы можете использовать для упрощения вышеуказанного метода хеширования:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);
И проверяя:
if (password_verify($given_password, $db_hash)) {
// password valid
}
Смотрите также: password_hash()
,password_verify()