Prefácio
Começando com a definição da sua mesa:
- UserID
- Fname
- Lname
- Email
- Password
- IV
Aqui estão as mudanças:
- Os campos
Fname
, Lname
e Email
serão criptografados usando uma cifra simétrica, fornecida pelo OpenSSL ,
- O
IV
campo armazenará o vetor de inicialização usado para criptografia. Os requisitos de armazenamento dependem da cifra e do modo usado; mais sobre isso mais tarde.
- O
Password
campo será hash usando um one-way hash de senha,
Encriptação
Cifra e modo
A escolha da melhor cifra e modo de criptografia está além do escopo desta resposta, mas a escolha final afeta o tamanho da chave de criptografia e do vetor de inicialização; para esta postagem, usaremos AES-256-CBC, que tem um tamanho de bloco fixo de 16 bytes e um tamanho de chave de 16, 24 ou 32 bytes.
Chave de encriptação
Uma boa chave de criptografia é um blob binário gerado a partir de um gerador de números aleatórios confiável. O seguinte exemplo seria recomendado (> = 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
Isso pode ser feito uma ou várias vezes (se desejar criar uma cadeia de chaves de criptografia). Mantenha-os o mais privados possível.
IV
O vetor de inicialização adiciona aleatoriedade à criptografia e é necessário para o modo CBC. Idealmente, esses valores devem ser usados apenas uma vez (tecnicamente, uma vez por chave de criptografia), portanto, uma atualização de qualquer parte de uma linha deve gerá-la novamente.
Uma função é fornecida para ajudá-lo a gerar o IV:
$iv_size = 16; // 128 bits
$iv = openssl_random_pseudo_bytes($iv_size, $strong);
Exemplo
Vamos criptografar o campo de nome, usando o anterior $encryption_key
e $iv
; para fazer isso, temos que preencher nossos dados com o tamanho do bloco:
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
);
Requisitos de armazenamento
A saída criptografada, como o IV, é binária; o armazenamento desses valores em um banco de dados pode ser realizado usando tipos de coluna designados como BINARY
ou VARBINARY
.
O valor de saída, como o IV, é binário; para armazenar esses valores no MySQL, considere usar as colunas BINARY
ouVARBINARY
. Se isso não for uma opção, você também pode converter os dados binários em uma representação textual usando base64_encode()
ou bin2hex()
, para fazer isso requer entre 33% a 100% mais espaço de armazenamento.
Decifrar
A descriptografia dos valores armazenados é semelhante:
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
));
Criptografia autenticada
Você pode melhorar ainda mais a integridade do texto cifrado gerado anexando uma assinatura gerada a partir de uma chave secreta (diferente da chave criptografada) e o texto cifrado. Antes de o texto cifrado ser descriptografado, a assinatura é primeiro verificada (de preferência com um método de comparação de tempo constante).
Exemplo
// 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
}
Veja também: hash_equals()
Hashing
O armazenamento de uma senha reversível em seu banco de dados deve ser evitado tanto quanto possível; você deseja apenas verificar a senha em vez de saber seu conteúdo. Se um usuário perder a senha, é melhor permitir que ele a redefina em vez de enviar a senha original (certifique-se de que a redefinição da senha só possa ser feita por um período limitado).
Aplicar uma função hash é uma operação unilateral; depois, pode ser usado com segurança para verificação sem revelar os dados originais; para senhas, um método de força bruta é uma abordagem viável para descobri-lo devido ao seu comprimento relativamente curto e escolhas de senhas pobres de muitas pessoas.
Algoritmos de hash, como MD5 ou SHA1, foram feitos para verificar o conteúdo do arquivo em relação a um valor de hash conhecido. Eles são bastante otimizados para tornar essa verificação o mais rápido possível e, ao mesmo tempo, ser precisa. Dado seu espaço de saída relativamente limitado, foi fácil construir um banco de dados com senhas conhecidas e suas respectivas saídas hash, as rainbow tables.
Adicionar um salt à senha antes de fazer o hash tornaria uma tabela de arco-íris inútil, mas os recentes avanços de hardware tornaram as pesquisas de força bruta uma abordagem viável. É por isso que você precisa de um algoritmo de hash deliberadamente lento e simplesmente impossível de otimizar. Ele também deve ser capaz de aumentar a carga para hardware mais rápido sem afetar a capacidade de verificar os hashes de senha existentes para torná-lo à prova de futuro.
Atualmente, existem duas opções populares disponíveis:
- PBKDF2 (função de derivação de chave baseada em senha v2)
- bcrypt (também conhecido como Blowfish)
Esta resposta usará um exemplo com bcrypt.
Geração
Um hash de senha pode ser gerado assim:
$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);
O sal é gerado openssl_random_pseudo_bytes()
para formar um blob aleatório de dados que é então percorrido base64_encode()
e strtr()
para corresponder ao alfabeto exigido de [A-Za-z0-9/.]
.
A crypt()
função executa o hashing com base no algoritmo ( $2y$
para Blowfish), o fator de custo (um fator de 13 leva aproximadamente 0,40s em uma máquina de 3GHz) e o sal de 22 caracteres.
Validação
Depois de buscar a linha que contém as informações do usuário, você valida a senha desta maneira:
$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;
}
Para verificar uma senha, você chama crypt()
novamente, mas passa o hash calculado anteriormente como o valor de sal. O valor de retorno produz o mesmo hash se a senha fornecida corresponder ao hash. Para verificar o hash, geralmente é recomendado usar uma função de comparação de tempo constante para evitar ataques de temporização.
Hash de senha com PHP 5.5
PHP 5.5 introduziu as funções de hash de senha que você pode usar para simplificar o método de hash acima:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 13]);
E verificando:
if (password_verify($given_password, $db_hash)) {
// password valid
}
Veja também: password_hash()
,password_verify()