Skip to content

Instantly share code, notes, and snippets.

@koseki
Last active August 20, 2017 05:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save koseki/c21b3cb505660c9ede0b459a3a0459fa to your computer and use it in GitHub Desktop.
Save koseki/c21b3cb505660c9ede0b459a3a0459fa to your computer and use it in GitHub Desktop.
PHP - MD5 based htpasswd entry generator class
<?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;
}
}
<?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