0
0
mirror of https://github.com/postfixadmin/postfixadmin.git synced 2024-09-19 19:22:14 +02:00
This commit is contained in:
David Goodwin 2021-05-08 21:47:45 +01:00
parent ce79972653
commit 71ee810891
2 changed files with 416 additions and 14 deletions

346
model/PFACrypt.php Normal file
View File

@ -0,0 +1,346 @@
<?php
class PFACrypt
{
private $algorithm;
public function __construct(string $algorithm)
{
$this->algorithm = $algorithm;
}
public function hash(string $pw, $pw_db)
{
$algorithm = $this->algorithm;
// try and 'upgrade' some dovecot commands to use local algorithms (rather tnan a dependency on the dovecot binary).
if (preg_match('/^dovecot:/', $algorithm)) {
$tmp = preg_replace('/^dovecot:/', '', $algorithm);
$supported = [
'SHA1', 'SHA1,HEX', 'SHA1.B64',
'BLF-CRYPT', 'BLF-CRYPT.B64',
'SHA512-CRYPT', 'SHA512-CRYPT.B64',
'ARGON2I',
'ARGON2I.B64',
'ARGON2ID',
'ARGON2ID.B64',
'SHA256', 'SHA256-CRYPT', 'SHA256-CRYPT.B64',
'SHA512', 'SHA512.B64',
'MD5',
'PLAIN-MD5',
'CRYPT',
];
if (in_array($tmp, $supported)) {
$algorithm = $tmp;
} else {
error_log("Warning: using alogrithm that requires proc_open: $algorithm, consider using one of : " . implode(', ', $supported));
}
}
switch ($algorithm) {
case 'SHA1':
case 'SHA1.B64':
case 'SHA1.HEX':
return $this->hashSha1($pw, $pw_db, $algorithm);
case 'BLF-CRYPT':
case 'BLF-CRYPT.B64':
return $this->blowfishCrypt($pw, $pw_db, $algorithm);
case 'SHA512-CRYPT':
case 'SHA512-CRYPT.B64':
return $this->sha512Crypt($pw, $pw_db, $algorithm);
case 'ARGON2I':
case 'ARGON2I.B64':
return $this->argon2iCrypt($pw, $pw_db, $algorithm);
case 'ARGON2ID':
case 'ARGON2ID.B64':
return $this->argon2idCrypt($pw, $pw_db, $algorithm);
case 'SHA256':
return '{SHA256}' . base64_encode(hash('sha256', $pw, true));
case 'SHA256-CRYPT':
case 'SHA256-CRYPT.B64':
return $this->sha256Crypt($pw, $pw_db, $algorithm);
case 'SHA512':
case 'sha512':
case 'sha512b.b64':
case 'SHA512.B64':
return $this->hashSha512($pw, $algorithm);
case 'md5':
case 'PLAIN-MD5':
return $this->hashMd5($pw, $algorithm);
case 'MD5':
case 'MD5-CRYPT':
return $this->cryptMd5($pw, $pw_db, $algorithm);
case 'CRYPT':
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{CRYPT}/', '', $pw_db);
}
if (empty($pw_db)) {
$pw_db = '$2y$10$' . substr(sha1(random_bytes(8)), 0, 22);
}
return '{CRYPT}' . crypt($pw, $pw_db);
case 'system':
return _pacrypt_crypt($pw, $pw_db);
case 'cleartext':
return $pw;
case 'CLEAR':
case 'PLAIN':
case 'CLEARTEXT':
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{.*}}/', '', $pw_db);
if (password_verify($pw, $pw_db)) {
return '{' . $algorithm . '}' . $pw;
}
}
return '{' . $algorithm . '}' . $pw;
case 'mysql_encrypt':
return _pacrypt_mysql_encrypt($pw, $pw_db);
// these are all supported by the above (SHA,
case 'authlib':
return _pacrypt_authlib($pw, $pw_db);
case 'sha512crypt.b64':
return $this->pacrypt_sha512crypt_b64($pw, $pw_db);
}
if (preg_match("/^dovecot:/", $algorithm)) {
return _pacrypt_dovecot($pw, $pw_db);
}
if (substr($algorithm, 0, 9) === 'php_crypt') {
return _pacrypt_php_crypt($pw, $pw_db);
}
throw new Exception('unknown/invalid $CONF["encrypt"] setting: ' . $algorithm);
}
public function hashSha1(string $pw, string $pw_db = '', string $algorithm = 'SHA1'): string
{
$hash = hash('sha1', $pw, true);
if (preg_match('/\.HEX$/', $algorithm)) {
$hash = bin2hex($hash);
} else {
$hash = base64_encode($hash);
}
return "{{$algorithm}}{$hash}";
}
public function hashSha512(string $pw, string $algorithm = 'SHA512')
{
$prefix = '{SHA512}';
if ($algorithm == 'SHA512.B64' || $algorithm == 'sha512b.b64') {
$prefix = '{SHA512.B64}';
}
return $prefix . base64_encode(hash('sha512', $pw, true));
}
public function hashMd5(string $pw, string $algorithm = 'PLAIN-MD5'): string
{
if ($algorithm == 'PLAIN-MD5') {
return '{PLAIN-MD5}' . md5($pw);
}
return md5($pw);
}
public function cryptMd5(string $pw, string $pw_db = '', $algorithm = 'MD5-CRYPT')
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{MD5.*}/', '', $pw_db);
}
if (empty($pw_db)) {
$pw_db = '$1$' . substr(sha1(random_bytes(8)), 0, 16);
}
return "{{$algorithm}}" . crypt($pw, $pw_db);
}
public function blowfishCrypt(string $pw, string $pw_db = '', string $algorithm = 'BLF-CRYPT'): string
{
if (!empty($pw_db)) {
if ($algorithm == 'BLF-CRYPT') {
$pw_db = preg_replace('/^{BLF-CRYPT}/', '', $pw_db);
}
if ($algorithm == 'BLF-CRYPT.B64') {
$pw_db = base64_decode(preg_replace('/^{BLF-CRYPT.B64}/', '', $pw_db));
}
$hash = crypt($pw, $pw_db);
if ($algorithm == 'BLF-CRYPT.B64') {
$hash = base64_encode($hash);
}
return "{{$algorithm}}{$hash}";
}
$r = password_hash($pw, PASSWORD_BCRYPT);
if (!is_string($r)) {
throw new \RuntimeException("Failed to generate password");
}
if ($algorithm == 'BLF-CRYPT.B64') {
return '{BLF-CRYPT.B64}' . base64_encode($r);
}
return '{BLF-CRYPT}' . $r;
}
public function sha256Crypt(string $pw, string $pw_db = '', string $algorithm = 'SHA256-CRYPT'): string
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{SHA256-CRYPT(\.B64)?}/', '', $pw_db);
if ($algorithm == 'SHA256-CRYPT.B64') {
$pw_db = base64_decode($pw_db);
}
}
if (empty($pw_db)) {
$pw_db = '$5$' . substr(sha1(random_bytes(8)), 0, 16);
}
$hash = crypt($pw, $pw_db);
if ($algorithm == 'SHA256-CRYPT.B64') {
return '{SHA256-CRYPT.B64}' . base64_encode($hash);
}
return "{SHA256-CRYPT}" . $hash;
}
public function sha512Crypt(string $pw, string $pw_db = '', $algorithm = 'SHA512-CRYPT'): string
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{SHA512-CRYPT(\.B64)?}/', '', $pw_db);
if ($algorithm == 'SHA512-CRYPT.B64') {
$pw_db = base64_decode($pw_db);
}
}
if (empty($pw_db)) {
$pw_db = '$6$' . substr(sha1(random_bytes(8)), 0, 16);
}
$hash = crypt($pw, $pw_db);
if ($algorithm == 'SHA512-CRYPT.B64') {
$hash = base64_encode($hash);
return "{SHA512-CRYPT.B64}{$hash}";
}
return "{SHA512-CRYPT}$hash";
}
public function argon2ICrypt(string $pw, string $pw_db = '', $algorithm = 'ARGON2I'): string
{
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{ARGON2I(\.B64)?}/', '', $pw_db);
$orig_pwdb = $pw_db;
if ($algorithm == 'ARGON2I.B64') {
$pw_db = base64_decode($pw_db);
}
if (password_verify($pw, $pw_db)) {
return "{{$algorithm}}" . $orig_pwdb;
}
$hash = password_hash($pw, PASSWORD_ARGON2I);
if ($algorithm == 'ARGON2I') {
return '{ARGON2I}' . $hash;
}
return '{ARGON2I.B64}' . base64_encode($hash);
;
}
$hash = password_hash($pw, PASSWORD_ARGON2I);
if ($algorithm == 'ARGON2I') {
return '{ARGON2I}' . $hash;
}
return "{ARGON2I.B64}" . base64_encode($hash);
}
public function argon2idCrypt(string $pw, string $pw_db = '', string $algorithm = 'ARGON2ID'): string
{
if (!defined('PASSWORD_ARGON2ID')) {
throw new \Exception("Requires PHP 7.3+");
}
if (!empty($pw_db)) {
$pw_db = preg_replace('/^{ARGON2ID(\.B64)?}/', '', $pw_db);
$orig_pwdb = $pw_db;
if ($algorithm == 'ARGON2ID.B64') {
$pw_db = base64_decode($pw_db);
}
if (password_verify($pw, $pw_db)) {
return "{{$algorithm}}" . $orig_pwdb;
}
$hash = password_hash($pw, PASSWORD_ARGON2ID);
if ($algorithm == 'ARGON2ID') {
return '{ARGON2ID}' . $hash;
}
// if($algorithm == 'ARGON2ID.B64') {
return '{ARGON2ID.B64}' . base64_encode($hash);
}
$hash = password_hash($pw, PASSWORD_ARGON2ID);
if ($algorithm == 'ARGON2ID') {
return '{ARGON2ID}' . $hash;
}
return '{ARGON2ID.B64}' . base64_encode($hash);
}
/**
* @see https://github.com/postfixadmin/postfixadmin/issues/58
*
* Note, this is really a base64 encoded CRYPT formatted hash; this isn't the same as a
* sha512 hash that's been base64 encoded.
*/
public function pacrypt_sha512crypt_b64($pw, $pw_db = "")
{
if (!function_exists('random_bytes') || !function_exists('crypt') || !defined('CRYPT_SHA512') || !function_exists('mb_substr')) {
throw new Exception("sha512.b64 not supported!");
}
if (!$pw_db) {
$salt = mb_substr(rtrim(base64_encode(random_bytes(16)), '='), 0, 16, '8bit');
return '{SHA512-CRYPT.B64}' . base64_encode(crypt($pw, '$6$' . $salt));
}
$password = "#Thepasswordcannotbeverified";
if (strncmp($pw_db, '{SHA512-CRYPT.B64}', 18) == 0) {
$dcpwd = base64_decode(mb_substr($pw_db, 18, null, '8bit'), true);
if ($dcpwd !== false && !empty($dcpwd) && strncmp($dcpwd, '$6$', 3) == 0) {
$password = '{SHA512-CRYPT.B64}' . base64_encode(crypt($pw, $dcpwd));
}
} elseif (strncmp($pw_db, '{MD5-CRYPT}', 11) == 0) {
$dcpwd = mb_substr($pw_db, 11, null, '8bit');
if (!empty($dcpwd) && strncmp($dcpwd, '$1$', 3) == 0) {
$password = '{MD5-CRYPT}' . crypt($pw, $dcpwd);
}
}
return $password;
}
}

View File

@ -39,7 +39,7 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
$this->assertNotEquals('test', $hash);
$this->assertNotEquals('test', $hash2);
$this->assertTrue( hash_equals($hash, _pacrypt_mysql_encrypt('test1', $hash) ), "hashes should equal....");
$this->assertTrue(hash_equals($hash, _pacrypt_mysql_encrypt('test1', $hash)), "hashes should equal....");
}
public function testAuthlib()
@ -68,6 +68,7 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
public function testPacryptDovecot()
{
global $CONF;
if (!file_exists('/usr/bin/doveadm')) {
$this->markTestSkipped("No /usr/bin/doveadm");
}
@ -77,6 +78,7 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
$expected_hash = '{SHA1}qUqP5cyxm6YcTAhz05Hph5gvu9M=';
$this->assertEquals($expected_hash, _pacrypt_dovecot('test', ''));
$this->assertEquals($expected_hash, _pacrypt_dovecot('test', $expected_hash));
@ -155,28 +157,82 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase
$this->assertFalse(strcmp($str1, $str2) == 0 && strcmp($str1, $str3) == 0);
}
public function testSha512B64()
public function testNewDovecotStuff()
{
$str1 = _pacrypt_sha512_b64('test', '');
$str2 = _pacrypt_sha512_b64('test', '');
global $CONF;
$this->assertNotEmpty($str1);
$this->assertNotEmpty($str2);
$this->assertNotEquals($str1, $str2); // should have different salts
$algo_list = [
'SHA1',
'SHA1.B64',
'BLF-CRYPT',
'BLF-CRYPT.B64',
$actualHash = '{SHA512-CRYPT.B64}JDYkM2NWcFM1WFNlUHl5MzdwSiRZWW80d0FmeWg5MXpxcS4uY3dtYUR1Y1RodTJGTDY1NHpXNUNvRU0wT3hXVFFzZkxIZ1JJSTZmT281OVpDUWJOTTF2L0JXajloME0vVjJNbENNMUdwLg==';
'SHA512-CRYPT',
'SHA512-CRYPT.B64',
$check = _pacrypt_sha512_b64('test', $actualHash);
'SHA512',
'SHA512.B64',
$this->assertTrue(hash_equals($check, $actualHash));
// 'DES-CRYPT',
'CRYPT',
'MD5-CRYPT',
'PLAIN-MD5',
'PLAIN',
'CLEAR',
'CLEARTEXT',
'ARGON2I',
"ARGON2ID",
'MD5',
'SHA256',
'SHA256-CRYPT',
'SHA256-CRYPT.B64',
];
$str3 = _pacrypt_sha512_b64('foo', '');
// should all be from 'test123', generated via dovecot.
$example_json = <<<'EOF'
{
"SHA1": "{SHA1}cojt0Pw\/\/L6ToM8G41aOKFIWh7w=",
"SHA1.B64": "{SHA1.B64}cojt0Pw\/\/L6ToM8G41aOKFIWh7w=",
"BLF-CRYPT": "{BLF-CRYPT}$2y$05$cEEZv2h\/NtLXII.emi2TP.rMZyB7VRSkyToXWBqqz6cXDoyay166q",
"BLF-CRYPT.B64": "{BLF-CRYPT.B64}JDJ5JDA1JEhlR0lBeGFHR2tNUGxjRWpyeFc0eU9oRjZZZ1NuTWVOTXFxNWp4bmFwVjUwdGU3c2x2L1VT",
"SHA512-CRYPT": "{SHA512-CRYPT}$6$MViNQUSbWyXWL9wZ$63VsBU2a\/ZFb9f\/dK4EmaXABE9jAcNltR7y6a2tXLKoV5F5jMezno.2KpmtD3U0FDjfa7A.pkCluVMlZJ.F64.",
"SHA512-CRYPT.B64": "{SHA512-CRYPT.B64}JDYkR2JwY3NiZXNMWk9DdERXbiRYdXlhdEZTdy9oa3lyUFE0d24wenpGQTZrSlpTUE9QVWdPcjVRUC40bTRMTjEzdy81aWMvWTdDZllRMWVqSWlhNkd3Q2Z0ZnNjZEFpam9OWjl3OU5tLw==",
"SHA512": "{SHA512}2u9JU7l4M2XK1mFSI3IFBsxGxRZ80Wq1APpZeqCP+WTrJPsZaH8012Zfd4\/LbFNY\/ApbgeFmLPkPc6JnHFP5kQ==",
"SHA512.B64": "{SHA512.B64}2u9JU7l4M2XK1mFSI3IFBsxGxRZ80Wq1APpZeqCP+WTrJPsZaH8012Zfd4\/LbFNY\/ApbgeFmLPkPc6JnHFP5kQ==",
"CRYPT": "{CRYPT}$2y$05$ORqzr0AagWr25v3ixHD5QuMXympIoNTbipEFZz6aAmovGNoij2vDO",
"MD5-CRYPT": "{MD5-CRYPT}$1$AIjpWveQ$2s3eEAbZiqkJhMYUIVR240",
"PLAIN-MD5": "{PLAIN-MD5}cc03e747a6afbbcbf8be7668acfebee5",
"PLAIN": "{PLAIN}test123",
"CLEAR": "{CLEAR}test123",
"CLEARTEXT": "{CLEARTEXT}test123",
"ARGON2I": "{ARGON2I}$argon2i$v=19$m=32768,t=4,p=1$xoOcAGa27k0Sr6ZPbA9ODw$wl\/KAZVmJooD\/35IFG5oGwyQiAREXrLss5BPS1PDKfA",
"ARGON2ID": "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$eaXP376O9/VxleLw9OQIxg$jOoDyECeRRV4eta3eSN/j0RdBgqaA1VBGAA/pbviI20",
"ARGON2ID.B64" : "{ARGON2ID.B64}JGFyZ29uMmlkJHY9MTkkbT02NTUzNix0PTMscD0xJEljdG9DWko1T04zWlYzM3I0TVMrNEEkMUVtNTJRWkdsRlJzNnBsRXpwVmtMeVd4dVNPRUZ2dUZnaVNhTmNlb08rOA==",
"MD5": "{MD5}$1$q7P.FNm9$\/W8EtauGkrxlhQep80T8..",
"SHA256": "{SHA256}7NcYcNGWMxapfjrDQIyYNa2M8PPBvHA1J8MCZVNPda4=",
"SHA256-CRYPT": "{SHA256-CRYPT}$5$CFly6wzfn2az3U8j$EhfQPTdjpMGAisfCjCKektLke5GGEmtdLVaCZSmsKw2",
"SHA256-CRYPT.B64": "{SHA256-CRYPT.B64}JDUkUTZZS1ZzZS5sSVJoLndodCR6TWNOUVFVVkhtTmM1ME1SQk9TR3BEeGpRY2M1TzJTQ1lkbWhPN1YxeHlD"
}
EOF;
$this->assertNotEmpty($str3);
$algo_example = json_decode($example_json, true);
$this->assertFalse(hash_equals('test', $str3));
foreach ($algo_example as $algorithm => $example_hash) {
$CONF['encrypt'] = $algorithm;
$pfa_new_hash = pacrypt('test123');
$this->assertTrue(hash_equals(_pacrypt_sha512_b64('foo', $str3), $str3));
$pacrypt_check = pacrypt('test123', $example_hash);
$pacrypt_sanity = pacrypt('zzzzzzz', $example_hash);
$this->assertNotEquals($pacrypt_sanity, $example_hash, "Should not match, zzzz password. $algorithm / $pacrypt_sanity");
$this->assertEquals($pacrypt_check, $example_hash, "Should match, algorithm: $algorithm generated:{$pacrypt_check} vs example:{$example_hash}");
$new_new = pacrypt('test123', $pfa_new_hash);
$this->assertEquals($pfa_new_hash, $new_new, "Trying: $algorithm => gave: $new_new with $pfa_new_hash ... ");
}
}
}