1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-07-31 13:20:11 +02:00

Implement pre-hashing for passwords when using bcrypt

This enables support for passwords with more than 72 bytes (or more
than 18-72 characters) and for passwords containing null bytes
This commit is contained in:
Marco
2025-05-19 12:55:28 +02:00
parent 1cac1a5188
commit e087c9af2f

View File

@@ -11,6 +11,9 @@ namespace Delight\Auth;
final class PasswordHash {
const HASH_ALGORITHM_IDENTIFIER = \PASSWORD_DEFAULT;
const PEPPER_HMAC_SHA_512_PREHASH = 'bec95beffb3afd078df7cbfd4c4617ba214ac4641a157c1ca64106e7544c9fb4cef6e99b0a8f0b63e96328c09943ce96b9b8899ff54fa7ea57b622675442dbbf';
const PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH = '$pa01';
const PREFIX_LENGTH = 5;
/**
* Creates a computationally expensive hash from a password
@@ -19,7 +22,18 @@ final class PasswordHash {
* @return string|bool
*/
public static function from($passwordText) {
return \password_hash($passwordText, self::HASH_ALGORITHM_IDENTIFIER);
// if the bcrypt algorithm will be used for computationally expensive hashing
if (self::HASH_ALGORITHM_IDENTIFIER === \PASSWORD_BCRYPT || self::HASH_ALGORITHM_IDENTIFIER === null) {
// pre-hash the password to support passwords with more than 72 bytes (i.e. more than 18-72 characters) and passwords containing null bytes
$passwordText = self::prehash($passwordText);
// use 72 out of the ~88 bytes from the prehash in bcrypt later and denote this in a custom hash prefix
$outputPrefix = self::PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH;
}
else {
$outputPrefix = '';
}
return $outputPrefix . \password_hash($passwordText, self::HASH_ALGORITHM_IDENTIFIER);
}
/**
@@ -30,6 +44,14 @@ final class PasswordHash {
* @return bool
*/
public static function verify($passwordText, $expectedHash) {
// if the expected hash has a custom prefix that indicates a prehash has been used
if (\substr($expectedHash, 0, self::PREFIX_LENGTH) === self::PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH) {
// pre-hash the password here as well to allow for a possible match
$passwordText = self::prehash($passwordText);
// and drop the custom prefix from the expected hash
$expectedHash = \substr($expectedHash, self::PREFIX_LENGTH);
}
return \password_verify($passwordText, $expectedHash);
}
@@ -40,7 +62,35 @@ final class PasswordHash {
* @return bool
*/
public static function needsRehash($existingHash) {
// if the existing hash has a custom prefix indicating that a prehash has been used
if (\substr($existingHash, 0, self::PREFIX_LENGTH) === self::PREFIX_BCRYPT_WITH_HMAC_SHA_512_PREHASH) {
// drop that custom prefix from the existing hash
$existingHash = \substr($existingHash, self::PREFIX_LENGTH);
}
/*// if the existing hash has no custom prefix denoting a prehash
else {
// if the existing hash used the bcrypt algorithm
if (\preg_match('/^\$2[abxy]?\$/', $existingHash) === 1) {
// the prehash needs to be applied
return true;
}
}*/
return \password_needs_rehash($existingHash, self::HASH_ALGORITHM_IDENTIFIER);
}
private static function prehash($passwordText) {
$pepperBinary = \hex2bin(self::PEPPER_HMAC_SHA_512_PREHASH);
// do not just use SHA-512 but apply an HMAC with a (semi-public) pepper to avoid breach correlation or "password shucking"
$hmacBinary = \hash_hmac('sha512', $passwordText, $pepperBinary, true);
if (empty($hmacBinary)) {
throw new AuthError('Could not generate HMAC');
}
// encode the prehash using Base64 to avoid passing null bytes to the main hash function later (which could truncate the input)
return \base64_encode($hmacBinary);
}
}