Przedmowa
Zaczynając od definicji tabeli:
- UserID
- Fname
- Lname
- Email
- Password
- IV
Oto zmiany:
- Pola
Fname
, Lname
i Email
zostaną zaszyfrowane przy użyciu symetrycznego szyfru, dostarczone przez OpenSSL ,
IV
Pole będzie przechowywać wektor inicjujący używany do szyfrowania. Wymagania dotyczące przechowywania zależą od używanego szyfrowania i trybu; więcej o tym później.
Password
Pole zostanie zakodowane przy użyciu jednokierunkowej hash hasła,
Szyfrowanie
Szyfr i tryb
Wybór najlepszego szyfru i trybu szyfrowania wykracza poza zakres tej odpowiedzi, ale ostateczny wybór wpływa na rozmiar zarówno klucza szyfrowania, jak i wektora inicjalizacji; w tym poście będziemy używać AES-256-CBC, który ma stały rozmiar bloku 16 bajtów i rozmiar klucza 16, 24 lub 32 bajty.
Klucz szyfrowania
Dobry klucz szyfrowania to binarny obiekt blob wygenerowany z niezawodnego generatora liczb losowych. Zalecany byłby następujący przykład (> = 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
Można to zrobić raz lub wiele razy (jeśli chcesz utworzyć łańcuch kluczy szyfrujących). Zachowaj je jak najbardziej poufne.
IV
Wektor inicjalizacyjny dodaje losowość do szyfrowania i jest wymagany w trybie CBC. Idealnie byłoby, gdyby te wartości były używane tylko raz (technicznie raz na klucz szyfrowania), więc aktualizacja dowolnej części wiersza powinna ją ponownie wygenerować.
Dostępna jest funkcja pomagająca w generowaniu IV:
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
Przykład
Zaszyfrujmy pole nazwy, używając wcześniejszego $encryption_key
i $iv
; aby to zrobić, musimy dopełnić nasze dane do rozmiaru bloku:
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
);
Wymagania dotyczące przechowywania
Zaszyfrowane dane wyjściowe, podobnie jak IV, są binarne; przechowywanie tych wartości w bazie danych można osiągnąć za pomocą wyznaczonych typów kolumn, takich jak BINARY
lub VARBINARY
.
Wartość wyjściowa, podobnie jak IV, jest binarna; aby przechowywać te wartości w MySQL, rozważ użycie BINARY
lubVARBINARY
kolumn. Jeśli nie ma takiej opcji, możesz również przekonwertować dane binarne na reprezentację tekstową za pomocą base64_encode()
lub bin2hex()
, wymaga to od 33% do 100% więcej miejsca na dysku.
Deszyfrowanie
Odszyfrowanie przechowywanych wartości jest podobne:
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
));
Uwierzytelnione szyfrowanie
Możesz dodatkowo poprawić integralność wygenerowanego tekstu zaszyfrowanego, dołączając podpis, który jest generowany z tajnego klucza (innego niż klucz szyfrowania) i zaszyfrowanego tekstu. Przed odszyfrowaniem tekstu zaszyfrowanego podpis jest najpierw weryfikowany (najlepiej metodą porównywania w czasie stałym).
Przykład
// 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
}
Zobacz też: hash_equals()
Haszowanie
W miarę możliwości należy unikać przechowywania odwracalnego hasła w bazie danych; chcesz tylko zweryfikować hasło, a nie znać jego zawartość. Jeśli użytkownik zgubi swoje hasło, lepiej pozwolić mu je zresetować, zamiast wysyłać mu swoje oryginalne (upewnij się, że resetowanie hasła można wykonać tylko przez ograniczony czas).
Stosowanie funkcji skrótu jest operacją jednokierunkową; następnie można go bezpiecznie używać do weryfikacji bez ujawniania oryginalnych danych; w przypadku haseł metoda brutalnej siły jest wykonalnym podejściem do ich ujawnienia ze względu na jej stosunkowo krótką długość i zły dobór haseł przez wiele osób.
Algorytmy haszujące, takie jak MD5 lub SHA1, zostały stworzone w celu sprawdzenia zawartości plików względem znanej wartości skrótu. Są one znacznie zoptymalizowane, aby weryfikacja była jak najszybsza, a jednocześnie była dokładna. Biorąc pod uwagę ich stosunkowo ograniczoną przestrzeń wyjściową, łatwo było zbudować bazę danych ze znanymi hasłami i odpowiadającymi im wyjściami mieszania, tęczowymi tablicami.
Dodanie soli do hasła przed haszowaniem sprawiłoby, że tęczowa tablica byłaby bezużyteczna, ale ostatnie postępy w sprzęcie sprawiły, że wyszukiwanie brutalne stało się opłacalnym podejściem. Dlatego potrzebujesz algorytmu haszującego, który jest celowo wolny i po prostu niemożliwy do optymalizacji. Powinien również być w stanie zwiększyć obciążenie szybszego sprzętu bez wpływu na możliwość weryfikacji istniejących skrótów haseł, aby zapewnić przyszłą ochronę.
Obecnie dostępne są dwie popularne opcje:
- PBKDF2 (funkcja wyprowadzania klucza w oparciu o hasło v2)
- bcrypt (aka Blowfish)
W tej odpowiedzi wykorzystany zostanie przykład z bcrypt.
Pokolenie
Skrót hasła można wygenerować w następujący sposób:
$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);
Sól jest generowana w openssl_random_pseudo_bytes()
celu utworzenia losowej porcji danych, która jest następnie przepuszczana base64_encode()
i strtr()
dopasowywana do wymaganego alfabetu [A-Za-z0-9/.]
.
W crypt()
pełni funkcję wycieniowanie podstawie algorytmu ( $2y$
na Blowfish), czynnik kosztowy (współczynnik 13 zajmuje z grubsza 0.40s na maszynie do 3 GHz) i sól 22 znaków.
Uprawomocnienie
Po pobraniu wiersza zawierającego informacje o użytkowniku weryfikujesz hasło w następujący sposób:
$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;
}
Aby zweryfikować hasło, dzwonisz crypt()
ponownie, ale przekazujesz poprzednio obliczony skrót jako wartość soli. Wartość zwracana daje ten sam skrót, jeśli podane hasło jest zgodne z hashem. Aby zweryfikować hash, często zaleca się użycie funkcji porównania w stałym czasie, aby uniknąć ataków czasowych.
Haszowanie haseł w PHP 5.5
PHP 5.5 wprowadziło funkcje haszowania haseł , których możesz użyć do uproszczenia powyższej metody haszowania:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);
I weryfikacja:
if (password_verify($given_password, $db_hash)) {
// password valid
}
Zobacz również: password_hash()
,password_verify()