Last active
August 20, 2017 05:55
-
-
Save koseki/c21b3cb505660c9ede0b459a3a0459fa to your computer and use it in GitHub Desktop.
PHP - MD5 based htpasswd entry generator class
This file contains hidden or 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 | |
/** | |
* MD5 based htpasswd entry generator | |
* | |
* Original: https://stackoverflow.com/questions/2994637/how-to-edit-htpasswd-using-php/8786956#8786956 | |
* Spec: https://httpd.apache.org/docs/2.4/misc/password_encryptions.html | |
* | |
* | |
* random_compat is required if you are using PHP < 7.0. | |
* | |
* $ composer require paragonie/random_compat | |
* | |
* See: https://github.com/paragonie/random_compat | |
*/ | |
class HTPasswd | |
{ | |
/** | |
* Generate MD5 htpasswd entry. | |
*/ | |
public function md5($plainpasswd, $salt = null) | |
{ | |
if (empty($salt)) { | |
$salt = $this->salt(); | |
} | |
$len = strlen($plainpasswd); | |
$text = $plainpasswd . '$apr1$' . $salt; | |
$bin = pack('H32', md5($plainpasswd . $salt . $plainpasswd)); | |
for($i = $len; $i > 0; $i -= 16) { | |
$text .= substr($bin, 0, min(16, $i)); | |
} | |
for($i = $len; $i > 0; $i >>= 1) { | |
$text .= ($i & 1) ? chr(0) : $plainpasswd{0}; | |
} | |
$bin = pack('H32', md5($text)); | |
for($i = 0; $i < 1000; $i++) { | |
$new = ($i & 1) ? $plainpasswd : $bin; | |
if ($i % 3) $new .= $salt; | |
if ($i % 7) $new .= $plainpasswd; | |
$new .= ($i & 1) ? $bin : $plainpasswd; | |
$bin = pack('H32', md5($new)); | |
} | |
$alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; | |
$nums = '0123456789'; | |
$tmp = ''; | |
for ($i = 0; $i < 5; $i++) { | |
$k = $i + 6; | |
$j = $i + 12; | |
if ($j == 16) $j = 5; | |
$tmp = $bin[$i] . $bin[$k] . $bin[$j] . $tmp; | |
} | |
$tmp = chr(0) . chr(0) . $bin[11] . $tmp; | |
$tmp = strtr( | |
strrev(substr(base64_encode($tmp), 2)), | |
$alpha . $nums . '+/', | |
'./' . $nums . $alpha | |
); | |
return "\$apr1\$$salt\$$tmp"; | |
} | |
/** | |
* See: https://paragonie.com/blog/2015/07/how-safely-generate-random-strings-and-integers-in-php | |
*/ | |
public function salt() | |
{ | |
try { | |
$salt = strtr(base64_encode(random_bytes(6)), '+', '.'); | |
} catch (TypeError $e) { | |
die('An unexpected error has occurred'); | |
} catch (Error $e) { | |
die('An unexpected error has occurred'); | |
} catch (Exception $e) { | |
die('Could not generate a random int. Is our OS secure?'); | |
} | |
return $salt; | |
} | |
} |
This file contains hidden or 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 | |
// composer require paragonie/random_compat | |
// require __DIR__ . '/../vendor/autoload.php'; | |
require __DIR__ . '/HTPasswd.php'; | |
class HTPasswdTest | |
{ | |
/** | |
* Test md5() method using htpasswd and openssl commands. | |
*/ | |
public function testMD5($repeat) | |
{ | |
$htpasswd = new Htpasswd(); | |
// PHP -(salt)-> openssl | |
for ($i = 0; $i < $repeat; $i++) { | |
echo '+'; | |
$password = $this->passwordForTest(); | |
$result = $htpasswd->md5($password); | |
list($apr1, $salt, $hash) = preg_split('{\$}', substr($result, 1)); | |
$escaped = escapeshellarg($password); | |
$escapedSalt = escapeshellarg($salt); | |
$expected = exec("openssl passwd -apr1 -salt $escapedSalt $escaped"); | |
// echo "$expected\n"; | |
if ($apr1 != 'apr1' || | |
!preg_match('{\A[a-zA-Z0-9/\.]{8}\z}', $salt) || | |
strlen($hash) != 22 || | |
$expected != $result) { | |
die("ERROR: \n $expected\n $result\n"); | |
} | |
} | |
// htpasswd -(salt)-> PHP | |
for ($i = 0; $i < $repeat; $i++) { | |
echo '.'; | |
$out = null; | |
$password = $this->passwordForTest(); | |
$escaped = escapeshellarg($password); | |
exec("htpasswd -nbm $i $escaped", $out); | |
// echo $out[0] . "\n"; | |
$expected = preg_split('{:}', $out[0], 2)[1]; | |
list($apr1, $salt, $hash) = preg_split('{\$}', substr($expected, 1)); | |
$result = $htpasswd->md5($password, $salt); | |
$tokens = preg_split('{\$}', $result); | |
if ($tokens[1] != 'apr1' || | |
!preg_match('{\A[a-zA-Z0-9/\.]{8}\z}', $tokens[2]) || | |
strlen($tokens[3]) != 22 || | |
$result != $expected) { | |
die("ERROR: \n $expected\n $result"); | |
} | |
} | |
// PHP -> htpasswd -v (verify) | |
$tmpfile = __DIR__ . '/tmp-htpasswd'; | |
for ($i = 0; $i < $repeat; $i++) { | |
echo '*'; | |
$password = $this->passwordForTest(); | |
$escaped = escapeshellarg($password); | |
$result = $htpasswd->md5($password); | |
// echo $result . "\n"; | |
$out = fopen($tmpfile, 'w'); | |
fputs($out, "$i:$result\n"); | |
fclose($out); | |
exec("htpasswd -vbm $tmpfile $i $escaped 2>&1", $out); | |
if ($out[0] != "Password for user $i correct.") { | |
die("ERROR: \n $result\n {$out[0]}\n"); | |
} | |
} | |
unlink($tmpfile); | |
echo "\nOK\n"; | |
} | |
private function passwordForTest() | |
{ | |
$alphanum = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | |
$symbol = '"\'!#$%&()*+/:;<=>?@[\\]^_`{|}~-,.'; | |
$passwordLength = rand() % 20 + 1; | |
return $this->randomString($passwordLength, $alphanum . $symbol); | |
} | |
public function randomString($keyspace, $length) | |
{ | |
$keysize = strlen($keyspace); | |
$str = ''; | |
try { | |
for ($i = 0; $i < $length; ++$i) { | |
$str .= $keyspace[random_int(0, $keysize - 1)]; | |
} | |
} catch (TypeError $e) { | |
die('An unexpected error has occurred'); | |
} catch (Error $e) { | |
die('An unexpected error has occurred'); | |
} catch (Exception $e) { | |
die('Could not generate a random int. Is our OS secure?'); | |
} | |
return $str; | |
} | |
} | |
$test = new HtpasswdTest(); | |
$test->testMD5(300); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment