From d9f38e8ee7b2f78e3c0e3e5238e609643721a71b Mon Sep 17 00:00:00 2001 From: David Goodwin Date: Wed, 1 Apr 2020 21:31:16 +0100 Subject: [PATCH] changes to pacrypt to support a prefix like {SHA265-CRYPT} on a hash - @see https://github.com/postfixadmin/postfixadmin/issues/344 --- config.inc.php | 3 +- functions.inc.php | 38 ++++++++++++++++++------ tests/PacryptTest.php | 67 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/config.inc.php b/config.inc.php index 949fe96e..6999cab9 100644 --- a/config.inc.php +++ b/config.inc.php @@ -184,7 +184,7 @@ $CONF['smtp_sendmail_tls'] = 'NO'; // mysql_encrypt = useful for PAM integration // authlib = support for courier-authlib style passwords - also set $CONF['authlib_default_flavor'] // dovecot:CRYPT-METHOD = use dovecotpw -s 'CRYPT-METHOD'. Example: dovecot:CRAM-MD5 -// php_crypt:CRYPT-METHOD:DIFFICULTY = use PHP built in crypt()-function. Example: php_crypt:SHA512:50000 +// php_crypt:CRYPT-METHOD:DIFFICULTY:PREFIX = use PHP built in crypt()-function. Example: php_crypt:SHA512:50000 // - php_crypt CRYPT-METHOD: Supported values are DES, MD5, BLOWFISH, SHA256, SHA512 // - php_crypt DIFFICULTY: Larger value is more secure, but uses more CPU and time for each login. // - php_crypt DIFFICULTY: Set this according to your CPU processing power. @@ -194,6 +194,7 @@ $CONF['smtp_sendmail_tls'] = 'NO'; // - don't use dovecot:* methods that include the username in the hash - you won't be able to login to PostfixAdmin in this case // - you'll need at least dovecot 2.1 for salted passwords ('doveadm pw' 2.0.x doesn't support the '-t' option) // - dovecot 2.0.0 - 2.0.7 is not supported +// - php_crypt PREFIX: hash has specified prefix - example: php_crypt:SHA512::{SHA256-CRYPT} // sha512.b64 - {SHA512-CRYPT.B64} (base64 encoded sha512) (no dovecot dependency; should support migration from md5crypt) $CONF['encrypt'] = 'md5crypt'; diff --git a/functions.inc.php b/functions.inc.php index 6e74f9c3..a957cad1 100644 --- a/functions.inc.php +++ b/functions.inc.php @@ -1061,12 +1061,15 @@ function _pacrypt_dovecot($pw, $pw_db = '') { /** * Supports DES, MD5, BLOWFISH, SHA256, SHA512 methods. * + * Via config we support an optional prefix (e.g. if you need hashes to start with {SHA256-CRYPT} and optional rounds (hardness) setting. + * * @param string $pw * @param string $pw_db (can be empty if setting a new password) * @return string crypt'ed password; if it matches $pw_db then $pw is the original password. */ -function _pacrypt_php_crypt($pw, $pw_db) { - global $CONF; +function _pacrypt_php_crypt($pw, $pw_db) +{ + $configEncrypt = Config::read_string('encrypt'); // use PHPs crypt(), which uses the system's crypt() // same algorithms as used in /etc/shadow @@ -1074,31 +1077,48 @@ function _pacrypt_php_crypt($pw, $pw_db) { // the algorithm for a new hash is chosen by feeding a salt with correct magic to crypt() // set $CONF['encrypt'] to 'php_crypt' to use the default SHA512 crypt method // set $CONF['encrypt'] to 'php_crypt:METHOD' to use another method; methods supported: DES, MD5, BLOWFISH, SHA256, SHA512 + // set $CONF['encrypt'] to 'php_crypt:METHOD:difficulty' where difficulty is between 1000-999999999 + // set $CONF['encrypt'] to 'php_crypt:METHOD:difficulty:PREFIX' to prefix the hash with the {PREFIX} etc. // tested on linux + $prefix = ''; + if (strlen($pw_db) > 0) { // existing pw provided. send entire password hash as salt for crypt() to figure out $salt = $pw_db; + + // if there was a prefix in the password, use this (override anything given in the config). + + if (preg_match('/^\{([-A-Z0-9]+)\}(.+)$/', $pw_db, $method_matches)) { + $salt = $method_matches[2]; + $prefix = "{" . $method_matches[1] . "}"; + } + } else { $salt_method = 'SHA512'; // hopefully a reasonable default (better than MD5) $hash_difficulty = ''; // no pw provided. create new password hash - if (strpos($CONF['encrypt'], ':') !== false) { + if (strpos($configEncrypt, ':') !== false) { // use specified hash method - $split_method = explode(':', $CONF['encrypt']); - $salt_method = $split_method[1]; - if (count($split_method) >= 3) { - $hash_difficulty = $split_method[2]; + $spec = explode(':', $configEncrypt); + $salt_method = $spec[1]; + if (isset($spec[2])) { + $hash_difficulty = $spec[2]; + } + if (isset($spec[3])) { + $prefix = $spec[3]; // hopefully something like {SHA256-CRYPT} } } // create appropriate salt for selected hash method $salt = _php_crypt_generate_crypt_salt($salt_method, $hash_difficulty); } - // send it to PHPs crypt() + $password = crypt($pw, $salt); - return $password; + + return "{$prefix}{$password}"; } + /** * @param string $hash_type must be one of: MD5, DES, BLOWFISH, SHA256 or SHA512 (default) * @param int hash difficulty diff --git a/tests/PacryptTest.php b/tests/PacryptTest.php index 75be199d..8cbeb946 100644 --- a/tests/PacryptTest.php +++ b/tests/PacryptTest.php @@ -1,7 +1,9 @@ assertNotEmpty($hash); @@ -10,7 +12,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { $this->assertEquals($hash, _pacrypt_md5crypt('test', $hash)); } - public function testCrypt() { + public function testCrypt() + { // E_NOTICE if we pass in '' for the salt $hash = _pacrypt_crypt('test', 'sa'); @@ -21,7 +24,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { $this->assertEquals($hash, _pacrypt_crypt('test', $hash)); } - public function testMySQLEncrypt() { + public function testMySQLEncrypt() + { if (!db_mysql()) { $this->markTestSkipped('Not using MySQL'); } @@ -45,7 +49,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { ); } - public function testAuthlib() { + public function testAuthlib() + { global $CONF; // too many options! @@ -66,7 +71,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { } } - public function testPacryptDovecot() { + public function testPacryptDovecot() + { global $CONF; if (!file_exists('/usr/bin/doveadm')) { $this->markTestSkipped("No /usr/bin/doveadm"); @@ -82,9 +88,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { $this->assertEquals($expected_hash, _pacrypt_dovecot('test', $expected_hash)); } - public function testPhpCrypt() { - global $CONF; - + public function testPhpCrypt() + { $config = Config::getInstance(); Config::write('encrypt', 'php_crypt:MD5'); @@ -99,11 +104,44 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { $fail = _pacrypt_php_crypt('bar', $expected); + } + + public function testPhpCryptHandlesPrefixAndOrRounds() + { + // try with 1000 rounds + Config::write('encrypt', 'php_crypt:SHA256:1000'); + $password = 'hello'; + + $randomHash = '$5$VhqhhsXJtPFeBX9e$kz3/CMIEu80bKdtDAcISIrDfdwtc.ilR68Vb3hNhu/7'; + $randomHashWithPrefix = '{SHA256-CRYPT}' . $randomHash; + + $new = _pacrypt_php_crypt($password, ''); + + $this->assertNotEquals($randomHash, $new); // salts should be different. + + $enc = _pacrypt_php_crypt($password, $randomHash); + $this->assertEquals($enc, $randomHash); + + $this->assertEquals($randomHash, _pacrypt_php_crypt("hello", $randomHash)); + $this->assertEquals($randomHash, _pacrypt_crypt("hello", $randomHash)); + + Config::write('encrypt', 'php_crypt:SHA256::{SHA256-CRYPT}'); + + $enc = _pacrypt_php_crypt("hello", $randomHash); + $this->assertEquals($randomHash, $enc); // we passed in something lacking the prefix, so we shouldn't have added it in. + $this->assertTrue(hash_equals($randomHash, $enc)); + + // should cope with this : + $enc = _pacrypt_php_crypt($password, ''); + + $this->assertEquals($enc, _pacrypt_php_crypt($password, $enc)); - $this->assertNotEquals($fail, $expected); + $this->assertRegExp('/^\{SHA256-CRYPT\}/', $enc); + $this->assertGreaterThan(20, strlen($enc)); } - public function testPhpCryptRandomString() { + public function testPhpCryptRandomString() + { $str1 = _php_crypt_random_string('abcdefg123456789', 2); $str2 = _php_crypt_random_string('abcdefg123456789', 2); $str3 = _php_crypt_random_string('abcdefg123456789', 2); @@ -114,10 +152,11 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { // it should be difficult for us to get three salts of the same value back... // not impossible though. - $this->assertFalse( strcmp($str1, $str2) == 0 && strcmp($str1, $str3) == 0 ); + $this->assertFalse(strcmp($str1, $str2) == 0 && strcmp($str1, $str3) == 0); } - public function testSha512B64() { + public function testSha512B64() + { $str1 = _pacrypt_sha512_b64('test', ''); $str2 = _pacrypt_sha512_b64('test', ''); @@ -138,6 +177,6 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase { $this->assertFalse(hash_equals('test', $str3)); - $this->assertTrue(hash_equals(_pacrypt_sha512_b64('foo',$str3), $str3)); + $this->assertTrue(hash_equals(_pacrypt_sha512_b64('foo', $str3), $str3)); } }