changes to pacrypt to support a prefix like {SHA265-CRYPT} on a hash - @see https://github.com/postfixadmin/postfixadmin/issues/344

feature-php-crypt-prefix
David Goodwin 5 years ago
parent dec4b38f35
commit d9f38e8ee7

@ -184,7 +184,7 @@ $CONF['smtp_sendmail_tls'] = 'NO';
// mysql_encrypt = useful for PAM integration // mysql_encrypt = useful for PAM integration
// authlib = support for courier-authlib style passwords - also set $CONF['authlib_default_flavor'] // 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 // 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 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: 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. // - 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 // - 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) // - 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 // - 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) // sha512.b64 - {SHA512-CRYPT.B64} (base64 encoded sha512) (no dovecot dependency; should support migration from md5crypt)
$CONF['encrypt'] = 'md5crypt'; $CONF['encrypt'] = 'md5crypt';

@ -1061,12 +1061,15 @@ function _pacrypt_dovecot($pw, $pw_db = '') {
/** /**
* Supports DES, MD5, BLOWFISH, SHA256, SHA512 methods. * 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
* @param string $pw_db (can be empty if setting a new password) * @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. * @return string crypt'ed password; if it matches $pw_db then $pw is the original password.
*/ */
function _pacrypt_php_crypt($pw, $pw_db) { function _pacrypt_php_crypt($pw, $pw_db)
global $CONF; {
$configEncrypt = Config::read_string('encrypt');
// use PHPs crypt(), which uses the system's crypt() // use PHPs crypt(), which uses the system's crypt()
// same algorithms as used in /etc/shadow // 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() // 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' 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' 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 // tested on linux
$prefix = '';
if (strlen($pw_db) > 0) { if (strlen($pw_db) > 0) {
// existing pw provided. send entire password hash as salt for crypt() to figure out // existing pw provided. send entire password hash as salt for crypt() to figure out
$salt = $pw_db; $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 { } else {
$salt_method = 'SHA512'; // hopefully a reasonable default (better than MD5) $salt_method = 'SHA512'; // hopefully a reasonable default (better than MD5)
$hash_difficulty = ''; $hash_difficulty = '';
// no pw provided. create new password hash // no pw provided. create new password hash
if (strpos($CONF['encrypt'], ':') !== false) { if (strpos($configEncrypt, ':') !== false) {
// use specified hash method // use specified hash method
$split_method = explode(':', $CONF['encrypt']); $spec = explode(':', $configEncrypt);
$salt_method = $split_method[1]; $salt_method = $spec[1];
if (count($split_method) >= 3) { if (isset($spec[2])) {
$hash_difficulty = $split_method[2]; $hash_difficulty = $spec[2];
}
if (isset($spec[3])) {
$prefix = $spec[3]; // hopefully something like {SHA256-CRYPT}
} }
} }
// create appropriate salt for selected hash method // create appropriate salt for selected hash method
$salt = _php_crypt_generate_crypt_salt($salt_method, $hash_difficulty); $salt = _php_crypt_generate_crypt_salt($salt_method, $hash_difficulty);
} }
// send it to PHPs crypt()
$password = crypt($pw, $salt); $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 string $hash_type must be one of: MD5, DES, BLOWFISH, SHA256 or SHA512 (default)
* @param int hash difficulty * @param int hash difficulty

@ -1,7 +1,9 @@
<?php <?php
class PaCryptTest extends \PHPUnit\Framework\TestCase { class PaCryptTest extends \PHPUnit\Framework\TestCase
public function testMd5Crypt() { {
public function testMd5Crypt()
{
$hash = _pacrypt_md5crypt('test', ''); $hash = _pacrypt_md5crypt('test', '');
$this->assertNotEmpty($hash); $this->assertNotEmpty($hash);
@ -10,7 +12,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase {
$this->assertEquals($hash, _pacrypt_md5crypt('test', $hash)); $this->assertEquals($hash, _pacrypt_md5crypt('test', $hash));
} }
public function testCrypt() { public function testCrypt()
{
// E_NOTICE if we pass in '' for the salt // E_NOTICE if we pass in '' for the salt
$hash = _pacrypt_crypt('test', 'sa'); $hash = _pacrypt_crypt('test', 'sa');
@ -21,7 +24,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase {
$this->assertEquals($hash, _pacrypt_crypt('test', $hash)); $this->assertEquals($hash, _pacrypt_crypt('test', $hash));
} }
public function testMySQLEncrypt() { public function testMySQLEncrypt()
{
if (!db_mysql()) { if (!db_mysql()) {
$this->markTestSkipped('Not using MySQL'); $this->markTestSkipped('Not using MySQL');
} }
@ -45,7 +49,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase {
); );
} }
public function testAuthlib() { public function testAuthlib()
{
global $CONF; global $CONF;
// too many options! // too many options!
@ -66,7 +71,8 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase {
} }
} }
public function testPacryptDovecot() { public function testPacryptDovecot()
{
global $CONF; global $CONF;
if (!file_exists('/usr/bin/doveadm')) { if (!file_exists('/usr/bin/doveadm')) {
$this->markTestSkipped("No /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)); $this->assertEquals($expected_hash, _pacrypt_dovecot('test', $expected_hash));
} }
public function testPhpCrypt() { public function testPhpCrypt()
global $CONF; {
$config = Config::getInstance(); $config = Config::getInstance();
Config::write('encrypt', 'php_crypt:MD5'); Config::write('encrypt', 'php_crypt:MD5');
@ -99,11 +104,44 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase {
$fail = _pacrypt_php_crypt('bar', $expected); $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); $str1 = _php_crypt_random_string('abcdefg123456789', 2);
$str2 = _php_crypt_random_string('abcdefg123456789', 2); $str2 = _php_crypt_random_string('abcdefg123456789', 2);
$str3 = _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... // it should be difficult for us to get three salts of the same value back...
// not impossible though. // 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', ''); $str1 = _pacrypt_sha512_b64('test', '');
$str2 = _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->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));
} }
} }

Loading…
Cancel
Save