Lời tựa
Bắt đầu với định nghĩa bảng của bạn:
- UserID
- Fname
- Lname
- Email
- Password
- IV
Dưới đây là những thay đổi:
- Các lĩnh vực
Fname
, Lname
và Email
sẽ được mã hóa bằng mật mã đối xứng, được cung cấp bởi OpenSSL ,
- Các
IV
trường sẽ lưu trữ các vector khởi tạo sử dụng để mã hóa. Các yêu cầu lưu trữ phụ thuộc vào mật mã và chế độ được sử dụng; thêm về điều này sau.
- Các
Password
lĩnh vực sẽ được băm bằng cách sử dụng một chiều mật khẩu băm,
Mã hóa
Mật mã và chế độ
Việc chọn mật mã và chế độ mã hóa tốt nhất nằm ngoài phạm vi của câu trả lời này, nhưng lựa chọn cuối cùng ảnh hưởng đến kích thước của cả khóa mã hóa và vectơ khởi tạo; cho bài đăng này, chúng tôi sẽ sử dụng AES-256-CBC có kích thước khối cố định là 16 byte và kích thước khóa là 16, 24 hoặc 32 byte.
Khóa mã hóa
Khóa mã hóa tốt là một khối nhị phân được tạo ra từ một trình tạo số ngẫu nhiên đáng tin cậy. Ví dụ sau sẽ được đề xuất (> = 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
Điều này có thể được thực hiện một lần hoặc nhiều lần (nếu bạn muốn tạo một chuỗi khóa mã hóa). Giữ những điều này càng riêng tư càng tốt.
IV
Vectơ khởi tạo thêm tính ngẫu nhiên vào mã hóa và cần thiết cho chế độ CBC. Các giá trị này lý tưởng chỉ nên được sử dụng một lần (về mặt kỹ thuật là một lần cho mỗi khóa mã hóa), do đó, bản cập nhật cho bất kỳ phần nào của hàng sẽ tạo lại nó.
Một chức năng được cung cấp để giúp bạn tạo IV:
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
Thí dụ
Hãy mã hóa trường tên bằng cách sử dụng trường tên cũ hơn $encryption_key
và $iv
; để làm điều này, chúng tôi phải đưa dữ liệu của mình vào kích thước khối:
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
);
Yêu cầu lưu trữ
Đầu ra được mã hóa, giống như IV, là nhị phân; Việc lưu trữ các giá trị này trong cơ sở dữ liệu có thể được thực hiện bằng cách sử dụng các kiểu cột được chỉ định như BINARY
hoặc VARBINARY
.
Giá trị đầu ra, giống như IV, là nhị phân; để lưu trữ các giá trị đó trong MySQL, hãy xem xét sử dụng BINARY
hoặcVARBINARY
các cột. Nếu đây không phải là một tùy chọn, bạn cũng có thể chuyển đổi dữ liệu nhị phân thành biểu diễn văn bản bằng cách sử dụng base64_encode()
hoặc bin2hex()
, làm như vậy yêu cầu thêm từ 33% đến 100% không gian lưu trữ.
Giải mã
Việc giải mã các giá trị được lưu trữ cũng tương tự:
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
));
Mã hóa xác thực
Bạn có thể cải thiện hơn nữa tính toàn vẹn của văn bản mật mã đã tạo bằng cách thêm chữ ký được tạo từ khóa bí mật (khác với khóa mã hóa) và văn bản mật mã. Trước khi văn bản mật mã được giải mã, chữ ký sẽ được xác minh đầu tiên (tốt nhất là bằng phương pháp so sánh thời gian không đổi).
Thí dụ
// 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
}
Xem thêm: hash_equals()
Băm
Lưu trữ mật khẩu có thể đảo ngược trong cơ sở dữ liệu của bạn phải được tránh càng nhiều càng tốt; bạn chỉ muốn xác minh mật khẩu hơn là biết nội dung của nó. Nếu người dùng mất mật khẩu, tốt hơn nên cho phép họ đặt lại mật khẩu thay vì gửi cho họ mật khẩu ban đầu (đảm bảo rằng việc đặt lại mật khẩu chỉ có thể được thực hiện trong một thời gian giới hạn).
Áp dụng một hàm băm là một hoạt động một chiều; sau đó nó có thể được sử dụng một cách an toàn để xác minh mà không tiết lộ dữ liệu gốc; đối với mật khẩu, phương pháp brute force là một cách tiếp cận khả thi để khám phá nó do độ dài tương đối ngắn và sự lựa chọn mật khẩu kém của nhiều người.
Các thuật toán băm như MD5 hoặc SHA1 được thực hiện để xác minh nội dung tệp dựa trên giá trị băm đã biết. Chúng được tối ưu hóa rất nhiều để giúp xác minh này nhanh nhất có thể mà vẫn chính xác. Với không gian đầu ra tương đối hạn chế, thật dễ dàng để xây dựng một cơ sở dữ liệu với các mật khẩu đã biết và đầu ra băm tương ứng của chúng, các bảng cầu vồng.
Thêm một muối vào mật khẩu trước khi băm nó sẽ làm cho một bảng cầu vồng trở nên vô dụng, nhưng những tiến bộ phần cứng gần đây đã khiến tra cứu brute force trở thành một cách tiếp cận khả thi. Đó là lý do tại sao bạn cần một thuật toán băm cố tình làm chậm và đơn giản là không thể tối ưu hóa. Nó cũng có thể tăng tải cho phần cứng nhanh hơn mà không ảnh hưởng đến khả năng xác minh các băm mật khẩu hiện có để làm bằng chứng trong tương lai.
Hiện tại, có hai sự lựa chọn phổ biến:
- PBKDF2 (Chức năng lấy lại khóa dựa trên mật khẩu v2)
- bcrypt (hay còn gọi là Blowfish)
Câu trả lời này sẽ sử dụng một ví dụ với bcrypt.
Thế hệ
Một hàm băm mật khẩu có thể được tạo như sau:
$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);
Muối được tạo ra openssl_random_pseudo_bytes()
để tạo thành một khối dữ liệu ngẫu nhiên sau đó được chạy qua base64_encode()
và strtr()
khớp với bảng chữ cái được yêu cầu của [A-Za-z0-9/.]
.
Các crypt()
thực hiện chức năng các băm dựa trên các thuật toán ( $2y$
cho Blowfish), yếu tố chi phí (một yếu tố của 13 mất khoảng 0.40s trên một máy 3GHz) và muối của 22 ký tự.
Thẩm định
Khi bạn đã tìm nạp hàng chứa thông tin người dùng, bạn xác thực mật khẩu theo cách sau:
$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;
}
Để xác minh mật khẩu, bạn gọi crypt()
lại nhưng bạn chuyển hàm băm đã tính trước đó làm giá trị muối. Giá trị trả về mang lại cùng một hàm băm nếu mật khẩu đã cho khớp với hàm băm. Để xác minh hàm băm, bạn thường nên sử dụng hàm so sánh thời gian không đổi để tránh các cuộc tấn công về thời gian.
Băm mật khẩu với PHP 5.5
PHP 5.5 đã giới thiệu các hàm băm mật khẩu mà bạn có thể sử dụng để đơn giản hóa phương pháp băm ở trên:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);
Và xác minh:
if (password_verify($given_password, $db_hash)) {
// password valid
}
Xem thêm: password_hash()
,password_verify()