Desenvolvimento - PHP

Criptografando senhas usando bcrypt (Blowfish) no PHP

Neste artigo vamos nos aprofundar mais sobre criptografia de dados com php, vamos falar sobre o bcrypt(Blowfish).

por Thiago Belem



Suporte ao bcrypt

O PHP suporta hashing via bcrypt através da função crypt() que está presente desde o PHP 4, e serve pra trabalhar com hashings de mão única (como o MD5 e SHA1).

Usando o bcrypt

O bcrypt precisa – obrigatóriamente – receber dois “parâmetros” pra funcionar: o salt e o custo de processamento. O salt nada mais é do que a sua garantia de que, dado um salt aleatório, a mesma senha nunca será igualmente hasheada duas vezes… não importa que você criptografe a mesma senha 100 vezes, se o salt for diferente nas 100 vezes, o resultado final será sempre diferente. Para o bcrypt funcionar:

  1. O salt precisa ser uma string de 22 caracteres que respeite a expressão regular ./0-9A-Za-z.
  2. O custo deve ser um número inteiro entre 4 e 31, outro detalhe é que o custo precisa ter dois dígitos, então números menores que 10 precisam ter zero à esquerda

Fiz alguns testes e meu computador quase parou quado usei um custo de 15 — O custo é a potência de 2, então 2^15 equivale a 32.768 ciclos, já 2^31 equivaleria a 2.147.483.648 ciclos

O custo de processamento influencia diretamente nas tentativas de ataque de força bruta, quanto maior, mais lento, quanto mais lento, melhor.

Criptografando senhas usando bcrypt

Basicamente, pra criptografar a senha “olá mundo“, com o salt “Cf1f11ePArKlBJomM0F6aJ” à um custo de processamento de 8, você faria algo assim:

<?php

$senha = 'ola mundo';
$custo = '08';
$salt = 'Cf1f11ePArKlBJomM0F6aJ';

// Gera um hash baseado em bcrypt
$hash = crypt($senha, '$2a$' . $custo . '$' . $salt . '$');

O que fizemos foi passar dois valores para a função crypt(): o valor a ser criptografado (a senha em si), e uma string ‘$2a$08$Cf1f11ePArKlBJomM0F6aJ$‘, que é composta por três partes (separadas por cifrão):

  1. O método de hashing - 2a - que fará com que o bcrypt/blowfish seja usado
  2. O custo - 08
  3. O salt - Cf1f11ePArKlBJomM0F6aJ

Isso no vai gerar o seguinte hash:

$2a$08$Cf1f11ePArKlBJomM0F6a.EyvTNh6W2huyQi5UZst5qsHVyi3w5x.

Que é o valor que você deve salvar no banco de dados.

Caso essa mesma senha seja criptografada com o mesmo salt e o mesmo custo, o resultado será idêntico… caso você mude o salt (que deve ser gerado de forma aleatória) o resultado seria diferente.

Vale lembrar que o hash gerado terá sempre 60 caracteres, então você pode modelar a sua coluna que armazena a senha como CHAR(60) ou VARCHAR(60) se você estiver usando o MySQL.

Verificando e validando senhas usando bcrypt

Agora suponhamos que o seu usuário está fazendo o login, e o único valor que você tem é o nome de usuário (ou email, tanto faz) e a senha que ele digitou no formulário (ola mundo), como comparar isso com o valor que está no banco para verificar se os dados estão válidos?

Já que você está trabalhando com um salt gerado aleatoriamente, é impossível gerar um novo hash que seja igual ao hash que está no banco… correto? ERRADO!

Para comparar uma senha texto-plano com um já hasheado, é só usar esse próprio valor hasheado como hash da senha text-plano, vejam como é simples:

<?php

// Senha digitada pelo usuário (veio do formulário)
$senha = 'ola mundo';

// Senha já criptografada (salva no banco)
$hash = '$2a$08$Cf1f11ePArKlBJomM0F6a.EyvTNh6W2huyQi5UZst5qsHVyi3w5x.';

if (crypt($senha, $hash) === $hash) {
	echo 'Senha OK!';
} else {
	echo 'Senha incorreta!';
}

É ou não é sensacional? Você pode gerar o mesmo hash, tendo a senha original e o hash resultado, sem precisar do salt original! Isso garante que você pode gerar um salt aleatório sempre que for criptografar a senha de alguém.

E é aí que o custo entra em jogo… mesmo durante um ataque de força bruta, o atacante pode tentar diferentes combinações de “senha original” mas o custo vai tornar a operação toda tão lenta que não vai valer o esforço.

Uma pequena classe para facilitar a sua vida

Criei uma pequena classe Bcrypt que ajuda a fazer esse trabalho todo através de dois métodos bem simples de usar..

Primeiro, o código completo da classe:

<?php

/**
 * Bcrypt hashing class
 * 
 * @author Thiago Belem <contato@thiagobelem.net>
 * @link   https://gist.github.com/3438461
 */
class Bcrypt {

/**
 * Default salt prefix
 * 
 * @see http://www.php.net/security/crypt_blowfish.php
 * 
 * @var string
 */
	protected static $_saltPrefix = '2a';

/**
 * Default hashing cost (4-31)
 * 
 * @var integer
 */
	protected static $_defaultCost = 8;

/**
 * Salt limit length
 * 
 * @var integer
 */
	protected static $_saltLength = 22;

/**
 * Hash a string
 * 
 * @param  string  $string The string
 * @param  integer $cost   The hashing cost
 * 
 * @see    http://www.php.net/manual/en/function.crypt.php
 * 
 * @return string
 */
	public static function hash($string, $cost = null) {
		if (empty($cost)) {
			$cost = self::$_defaultCost;
		}

		// Salt
		$salt = self::generateRandomSalt();

		// Hash string
		$hashString = self::__generateHashString((int)$cost, $salt);

		return crypt($string, $hashString);
	}

/**
 * Check a hashed string
 * 
 * @param  string $string The string
 * @param  string $hash   The hash
 * 
 * @return boolean
 */
	public static function check($string, $hash) {
		return (crypt($string, $hash) === $hash);
	}

/**
 * Generate a random base64 encoded salt
 * 
 * @return string
 */
	public static function generateRandomSalt() {
		// Salt seed
		$seed = uniqid(mt_rand(), true);

		// Generate salt
		$salt = base64_encode($seed);
		$salt = str_replace('+', '.', $salt);

		return substr($salt, 0, self::$_saltLength);
	}

/**
 * Build a hash string for crypt()
 * 
 * @param  integer $cost The hashing cost
 * @param  string $salt  The salt
 * 
 * @return string
 */
	private static function __generateHashString($cost, $salt) {
		return sprintf('$%s$%02d$%s$', self::$_saltPrefix, $cost, $salt);
	}

}

Agora como você pode usar os métodos:

<?php

// Encriptando a senha
$senha = 'ola mundo';
$hash = Bcrypt::hash($senha);
// $hash = $2a$08$MTgxNjQxOTEzMTUwMzY2OOc15r9yENLiaQqel/8A82XLdj.OwIHQm
// Salve $hash no banco de dados

// Verificando a senha
$senha = 'ola mundo';
$hash = '$2a$08$MTgxNjQxOTEzMTUwMzY2OOc15r9yENLiaQqel/8A82XLdj.OwIHQm'; // Valor retirado do banco

if (Bcrypt::check($senha, $hash)) {
	echo 'Senha OK!';
} else {
	echo 'Senha incorreta!';
}

Artigo originalmente publicado por Thiago Belem: Criptografando senhas no PHP usando bcrypt (Blowfish)

Thiago Belem

Thiago Belem - Tenho 23 anos e trabalho com Desenvolvimento WEB há mais de 10 anos. Atualmente moro no Rio de Janeiro e, além de trabalhar como Freelancer, sou Professor no Assando Sites, meu curso online de CakePHP.