Skip to content

Instantly share code, notes, and snippets.

@AndrewLane
Created January 7, 2013 21:25
Show Gist options
  • Save AndrewLane/4478583 to your computer and use it in GitHub Desktop.
Save AndrewLane/4478583 to your computer and use it in GitHub Desktop.
BCrypt unit tests in PHP
<?php
if (version_compare(PHP_VERSION, '5.3.7', '<')) {
die("At least PHP 5.3.7 is required");
}
function bcryptHash($plainTextPassword, $cost = 10)
{
if ($cost < 4 || $cost > 31) {
die("Invalid cost: " . $cost);
}
return crypt($plainTextPassword, bcryptGenerateSalt($cost));
}
function bcryptVerify($plainTextPassword, $hash) {
return crypt($plainTextPassword, $hash) === $hash;
}
function bcryptGenerateSalt($cost) {
//stolen from https://github.com/ircmaxell/password_compat/blob/master/lib/password.php
$required_salt_len = 22;
$raw_length = (int) ($required_salt_len * 3 / 4 + 1);
$buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
if ($buffer === FALSE) {
die("mcrypt_create_iv failed");
}
return sprintf('$2a$%02d$', $cost) . substr(str_replace('+', '.', base64_encode($buffer)), 0, $required_salt_len);
}
?>
<?php
require_once("lib/bcrypt.php");
runSimpleTestCases();
runSpecialCharacterTests();
runLongPasswordTests();
runPrecalculatedHashTests();
runHashingTimeGrowsWithNumberOfRoundsTest();
runTestThatDefaultCostRequiresATenthOfASecond();
runTestFor72CharactersLimitation();
runTestFromAuthor();
print "\n<br />\n";
print "all tests pass";
function runSimpleTestCases() {
$inputs = Array();
$inputs[] = "";
$inputs[] = "Password1";
$inputs[] = "abc123";
$inputs[] = "a";
$inputs[] = "ukalltheway";
$inputs[] = "goBigBlu3!";
for($i=0; $i<count($inputs); $i++) {
runHashAndIsValidTestForVariousEncryptionRounds($inputs[$i]);
}
}
function runSpecialCharacterTests() {
$inputs = Array();
$inputs[] = "\t";
$inputs[] = "has a newline in it \n blah";
$inputs[] = "\0";
$inputs[] = "Śtiḷl tr̾ͪ̀́͘y̶̧̨̱̹̭ͧinǥ to ġęt ᵺê han͛ͪ̈g of twe͖͉̩̟͛͆̾ͫ̑͆̍ͫͥͨḙͯ̿̔͑̾̾ting wít̨̥̫͎h a ḟo̗uᶇẗaiṋ p҉̯͈͕en.";
$inputs[] = "Je ne suis pas fatigué";
$inputs[] = "Il ne sera pas là";
$inputs[] = "¡Hola!";
$inputs[] = "¿Y Tú?";
$inputs[] = "Bis später!";
$inputs[] = "ようこそいらっしゃいました";
for($i=0; $i<count($inputs); $i++) {
runHashAndIsValidTestForVariousEncryptionRounds($inputs[$i]);
}
}
function runLongPasswordTests() {
$inputs = Array();
$inputs[] = "Noooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!";
$inputs[] = "Yeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaahhh!";
$inputs[] = "You'll find the grail in the castle arrgggggggggggggggggggggggggggggggghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh!";
for($i=0; $i<count($inputs); $i++) {
runHashAndIsValidTestForVariousEncryptionRounds($inputs[$i]);
}
}
function runHashAndIsValidTestForVariousEncryptionRounds($plainTextInput) {
for ($cost=4; $cost<=8; $cost++) {
$hash = bcryptHash($plainTextInput, $cost);
bcryptVerify($plainTextInput, $hash) or die("Verify failed for plain text input $plainTextInput and cost $cost");
strlen($hash) === 60 or die("Hash is not exactly 60 characters");
showSuccess();
}
}
function runPrecalculatedHashTests() {
runPrecalculatedHashTest("Password1", 9, '$2a$09$CmaUTtldsNCjYXpTikhnRuNHIj5BK236TCnlohzFKtncF9xjBvUwu');
runPrecalculatedHashTest("it's_just_a_string", 8, '$2a$08$ZBNPFt4fk4/d7NqwofJiCuS7sAQc0KbXTY4jgYo6KFFWae7IX5huW');
runPrecalculatedHashTest("abc", 8, '$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm');
runPrecalculatedHashTest("Noooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!", 4, '$2a$04$vgIsSb2s7cKVc9sEpCuwi.KWHwie/TzR0NgaNUAyWTp4z23pPWbZe');
runPrecalculatedHashTest("has a newline in it \n blah", 5, '$2a$05$Pa3E9c/YzrKxBEqyluT3Z..HBqlPZs1hIuv4zIdAco.KUM1pK58R2');
runPrecalculatedHashTest("Śtiḷl tr̾ͪ̀́͘y̶̧̨̱̹̭ͧinǥ to ġęt ᵺê han͛ͪ̈g of twe͖͉̩̟͛͆̾ͫ̑͆̍ͫͥͨḙͯ̿̔͑̾̾ting wít̨̥̫͎h a ḟo̗uᶇẗaiṋ p҉̯͈͕en.", 6, '$2a$06$qGxvg6.Kwax2fCmnQ5UCmOoilgXfYpoTAMZbpopjs6QOtSsnxSwDO');
}
function runPrecalculatedHashTest($plainText, $cost, $hashThatShouldVerify) {
$newHash = bcryptHash($plainText, $cost);
$hashThatShouldVerify !== $newHash or die("New hash isn't supposed to be the same");
bcryptVerify($plainText, $hashThatShouldVerify) or die("Verify failed for plain text $plainText with hash $hashThatShouldVerify");
bcryptVerify($plainText, $newHash) or die("Verify failed with new hash for plain text $plainText with hash $newHash");
showSuccess();
}
function runHashingTimeGrowsWithNumberOfRoundsTest() {
$plainTextPassword = uniqid();
//run through once for each value of the cost parameter and generate a password just to eliminate startup time from the actual test
for ($i=6; $i<=10; $i++) {
bcryptHash($plainTextPassword, $i);
}
$lasttime = 0;
for ($i=6; $i<=10; $i++) {
$before = microtime(true);
bcryptHash($plainTextPassword, $i);
$after = microtime(true);
$timediff = $after - $before;
$timediff > $lasttime or die("Time should have increased");
$lasttime = $timediff;
}
showSuccess();
}
function runTestThatDefaultCostRequiresATenthOfASecond() {
$before = microtime(true);
bcryptHash(uniqid());
$after = microtime(true);
$timediff = $after - $before;
$timediff > .1 or die("Hashing didn't take at least a tenth of a second.");
showSuccess();
}
// There is a known limitation that the BCrypt algorithm only cares about the first 72 bytes of the password
// See: https://github.com/ncb000gt/node.bcrypt.js/blob/master/src/node_blf.h#L59
function runTestFor72CharactersLimitation() {
$stringOf72Bytes = "___hello_i_am_72_chars_of___________________________________________data";
strlen($stringOf72Bytes) == 72 or die ("String isn't really 72 characters");
$stringOf71Bytes = "___hello_i_am_71_chars_of__________________________________________data";
strlen($stringOf71Bytes) == 71 or die ("String isn't really 71 characters");
$validHashForStringWith72Bytes = '$2a$10$.z73An.eUqQFDOlKHjNMPO6rA0LqRJiFVzr753sKj9sEnvaz/Ix0K';
$validHashForStringWith71Bytes = '$2a$10$59w/ESAcGZ1m6wsR7yYR4O.IlFDj9AO3.I.oF22rvWE1pcQ4u1o.K';
bcryptVerify($stringOf72Bytes, $validHashForStringWith72Bytes) or die("Should verify");
bcryptVerify($stringOf71Bytes, $validHashForStringWith71Bytes) or die("Should verify");
//add something random to the 72 byte string and make sure it's still valid with the other hash
bcryptVerify($stringOf72Bytes . uniqid(), $validHashForStringWith72Bytes) or die("Should verify");
//add something random to the 71 byte string and make sure it's NOT still valid with the other hash
bcryptVerify($stringOf71Bytes . uniqid(), $validHashForStringWith71Bytes) === FALSE or die("Should NOT verify");
showSuccess(".");
}
function runTestFromAuthor() {
$testVectorsFromAuthor = array(
array('', '$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.'),
array('', '$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye'),
array('', '$2a$10$k1wbIrmNyFAPwPVPSVa/zecw2BCEnBwVS2GbrmgzxFUOqW9dk4TCW'),
array('', '$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO'),
array('a', '$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe'),
array('a', '$2a$08$cfcvVd2aQ8CMvoMpP2EBfeodLEkkFJ9umNEfPD18.hUF62qqlC/V.'),
array('a', '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'),
array('a', '$2a$12$8NJH3LsPrANStV6XtBakCez0cKHXVxmvxIlcz785vxAIZrihHZpeS'),
array('abc', '$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i'),
array('abc', '$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm'),
array('abc', '$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi'),
array('abc', '$2a$12$EXRkfkdmXn2gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q'),
array('abcdefghijklmnopqrstuvwxyz', '$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC'),
array('abcdefghijklmnopqrstuvwxyz', '$2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz.'),
array('abcdefghijklmnopqrstuvwxyz', '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'),
array('abcdefghijklmnopqrstuvwxyz', '$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG'),
array('~!@#$%^&*() ~!@#$%^&*()PNBFRD', '$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO'),
array('~!@#$%^&*() ~!@#$%^&*()PNBFRD', '$2a$08$Eq2r4G/76Wv39MzSX262huzPz612MZiYHVUJe/OcOql2jo4.9UxTW'),
array('~!@#$%^&*() ~!@#$%^&*()PNBFRD', '$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS'),
array('~!@#$%^&*() ~!@#$%^&*()PNBFRD', '$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC')
);
for ($i=0; $i<count($testVectorsFromAuthor); $i++) {
$plainText = $testVectorsFromAuthor[$i][0];
$hashThatShouldVerify = $testVectorsFromAuthor[$i][1];
$hashThatShouldNotVerify = $testVectorsFromAuthor[($i + 4) % count($testVectorsFromAuthor)][1];
bcryptVerify($plainText, $hashThatShouldVerify) or die("Verify failed");
bcryptVerify($plainText, $hashThatShouldNotVerify) === FALSE or die("Verify succeeded when it should have failed");
showSuccess();
}
}
// show some indication that tests are succeeding
function showSuccess($char = ".") {
print $char;
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment