Prefazione
A partire dalla definizione della tabella:
- UserID
- Fname
- Lname
- Email
- Password
- IV
Ecco le modifiche:
- I campi
Fname
, Lname
e Email
verranno crittografati utilizzando una crittografia simmetrica, fornita da OpenSSL ,
- Il
IV
campo memorizzerà il vettore di inizializzazione utilizzato per la crittografia. I requisiti di archiviazione dipendono dalla crittografia e dalla modalità utilizzata; ne parleremo più avanti.
- Il
Password
campo verrà sottoposto ad hashing utilizzando un hash della password unidirezionale ,
crittografia
Cipher e modalità
La scelta della migliore crittografia e modalità di crittografia va oltre lo scopo di questa risposta, ma la scelta finale influisce sulla dimensione sia della chiave di crittografia che del vettore di inizializzazione; per questo post utilizzeremo AES-256-CBC che ha una dimensione del blocco fissa di 16 byte e una dimensione della chiave di 16, 24 o 32 byte.
Chiave crittografica
Una buona chiave di crittografia è un BLOB binario generato da un generatore di numeri casuali affidabile. Si consiglia il seguente esempio (> = 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
Questa operazione può essere eseguita una o più volte (se desideri creare una catena di chiavi di crittografia). Tienili il più privati possibile.
IV
Il vettore di inizializzazione aggiunge casualità alla crittografia ed è richiesto per la modalità CBC. Questi valori dovrebbero essere idealmente usati solo una volta (tecnicamente una volta per chiave di crittografia), quindi un aggiornamento a qualsiasi parte di una riga dovrebbe rigenerarla.
Viene fornita una funzione per aiutarti a generare l'IV:
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
Esempio
Crittografiamo il campo del nome, utilizzando i precedenti $encryption_key
e $iv
; per fare questo, dobbiamo riempire i nostri dati alla dimensione del blocco:
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
);
Requisiti di archiviazione
L'output crittografato, come l'IV, è binario; la memorizzazione di questi valori in un database può essere eseguita utilizzando tipi di colonna designati come BINARY
o VARBINARY
.
Il valore di output, come IV, è binario; per memorizzare questi valori in MySQL, considera l'utilizzo di colonne BINARY
oVARBINARY
. Se questa non è un'opzione, è anche possibile convertire i dati binari in una rappresentazione testuale utilizzando base64_encode()
o bin2hex()
, ciò richiede dal 33% al 100% in più di spazio di archiviazione.
decrittazione
La decrittografia dei valori memorizzati è simile:
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
));
Crittografia autenticata
È possibile migliorare ulteriormente l'integrità del testo cifrato generato aggiungendo una firma generata da una chiave segreta (diversa dalla chiave di crittografia) e il testo cifrato. Prima che il testo cifrato venga decrittografato, la firma viene prima verificata (preferibilmente con un metodo di confronto a tempo costante).
Esempio
// 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
}
Guarda anche: hash_equals()
hashing
La memorizzazione di una password reversibile nel database deve essere evitata il più possibile; desideri solo verificare la password invece di conoscerne il contenuto. Se un utente perde la password, è meglio consentirgli di reimpostarla piuttosto che inviargli quella originale (assicurati che la reimpostazione della password possa essere eseguita solo per un tempo limitato).
L'applicazione di una funzione hash è un'operazione unidirezionale; successivamente può essere tranquillamente utilizzato per la verifica senza rivelare i dati originali; per le password, un metodo di forza bruta è un approccio fattibile per scoprirlo a causa della sua lunghezza relativamente breve e delle scarse scelte di password di molte persone.
Algoritmi di hash come MD5 o SHA1 sono stati creati per verificare il contenuto del file rispetto a un valore hash noto. Sono notevolmente ottimizzati per rendere questa verifica il più veloce possibile pur rimanendo accurati. Dato il loro spazio di output relativamente limitato, è stato facile creare un database con password note e i rispettivi output hash, le tabelle arcobaleno.
Aggiungere un salt alla password prima dell'hashing renderebbe inutile una tabella arcobaleno, ma i recenti progressi dell'hardware hanno reso le ricerche di forza bruta un approccio praticabile. Ecco perché hai bisogno di un algoritmo di hashing che sia deliberatamente lento e semplicemente impossibile da ottimizzare. Dovrebbe anche essere in grado di aumentare il carico per hardware più veloce senza influire sulla capacità di verificare gli hash delle password esistenti per renderlo a prova di futuro.
Attualmente sono disponibili due scelte popolari:
- PBKDF2 (funzione di derivazione chiave basata su password v2)
- bcrypt (aka Blowfish)
Questa risposta utilizzerà un esempio con bcrypt.
Generazione
Un hash della password può essere generato in questo modo:
$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);
Il sale viene generato con openssl_random_pseudo_bytes()
per formare un blob casuale di dati che viene quindi eseguito base64_encode()
e strtr()
per abbinare l'alfabeto richiesto [A-Za-z0-9/.]
.
La crypt()
funzione esegue l'hashing in base all'algoritmo ( $2y$
per Blowfish), al fattore di costo (un fattore di 13 impiega circa 0.40s su una macchina a 3GHz) e il sale di 22 caratteri.
Validazione
Dopo aver recuperato la riga contenente le informazioni sull'utente, convalidi la password in questo modo:
$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;
}
Per verificare una password, si chiama di crypt()
nuovo ma si passa l'hash calcolato in precedenza come valore salt. Il valore restituito restituisce lo stesso hash se la password fornita corrisponde all'hash. Per verificare l'hash, è spesso consigliabile utilizzare una funzione di confronto a tempo costante per evitare attacchi temporali.
Hashing delle password con PHP 5.5
PHP 5.5 ha introdotto le funzioni di hashing della password che puoi usare per semplificare il metodo di hashing sopra:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);
E verificando:
if (password_verify($given_password, $db_hash)) {
// password valid
}
Vedi anche: password_hash()
,password_verify()