Created
January 7, 2013 21:25
-
-
Save AndrewLane/4478583 to your computer and use it in GitHub Desktop.
BCrypt unit tests in PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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