From 0876c368e428eb9f0bb961bfd53226fa365cb532 Mon Sep 17 00:00:00 2001 From: "Shao Yu-Lung (Allen)" Date: Fri, 12 Apr 2024 16:57:19 +0800 Subject: [PATCH] feat: support Dovecot DIGEST-MD5 (#816) Add support for dovecot DIGEST-MD5 auth (using : $CONF['pacrypt'] = 'dovecot:DIGEST-MD5') This also changes the pacrypt() function to take an optional 3rd argument (username). Thanks @bestlong --- .github/workflows/php.yml | 22 ++++++++++++++-------- functions.inc.php | 19 ++++++++++++------- model/Login.php | 6 +++--- model/PFAHandler.php | 2 +- scripts/snippets/dovecot_crypt.php | 2 +- tests/PacryptTest.php | 8 +++++++- tests/bootstrap.php | 2 +- 7 files changed, 39 insertions(+), 22 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 6d4345dc..b53fb47c 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -6,14 +6,14 @@ jobs: lint_etc: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 7.4 tools: composer - extensions: sqlite3 + extensions: sqlite3, gd - name: run install.sh run: /bin/bash install.sh @@ -42,14 +42,21 @@ jobs: php-versions: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Install Dovecot + run: | + set -eux + sudo apt-get update -q + sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq dovecot-core + sudo sh -c '/sbin/useradd -G dovecot runner || usermod -aG dovecot runner ' - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} tools: composer - extensions: sqlite3 + extensions: sqlite3, gd - name: run install.sh run: /bin/bash install.sh @@ -61,21 +68,21 @@ jobs: run: composer install --prefer-dist -n - name: Build/test - run: composer test + run: sudo -u runner composer test build_coverage_report: needs: [testsuite] continue-on-error: true runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '7.4' tools: composer - extensions: sqlite3 + extensions: sqlite3, gd - name: run install.sh run: /bin/bash install.sh @@ -93,4 +100,3 @@ jobs: run: vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v || true env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - diff --git a/functions.inc.php b/functions.inc.php index fbde2fac..2a5aa1c7 100644 --- a/functions.inc.php +++ b/functions.inc.php @@ -1060,9 +1060,10 @@ function _pacrypt_authlib($pw, $pw_db) * * @param string $pw - plain text password * @param string $pw_db - encrypted password, or '' for generation. + * @param string $username * @return string crypted password */ -function _pacrypt_dovecot($pw, $pw_db = '') +function _pacrypt_dovecot($pw, $pw_db = '', $username = '') { global $CONF; @@ -1076,11 +1077,14 @@ function _pacrypt_dovecot($pw, $pw_db = '') throw new Exception("invalid dovecot encryption method"); } - # digest-md5 hashes include the username - until someone implements it, let's declare it as unsupported + $doveadm_options = ''; + if (strtolower($method) == 'digest-md5') { - throw new Exception("Sorry, \$CONF['encrypt'] = 'dovecot:digest-md5' is not supported by PostfixAdmin."); + if (empty($username)) { + throw new Exception("\$CONF['encrypt'] = 'dovecot:digest-md5' require username."); + } + $doveadm_options = ' -u ' . escapeshellarg($username); } - # TODO: add -u option for those hashes, or for everything that is salted (-u was available before dovecot 2.1 -> no problem with backward compatibility ) $dovecotpw = "doveadm pw"; if (!empty($CONF['dovecotpw'])) { @@ -1105,7 +1109,7 @@ function _pacrypt_dovecot($pw, $pw_db = '') $pipes = []; - $pipe = proc_open("$dovecotpw '-s' $method$dovepasstest", $spec, $pipes); + $pipe = proc_open("$dovecotpw -s {$method}{$dovepasstest}{$doveadm_options}", $spec, $pipes); if (!$pipe) { throw new Exception("can't proc_open $dovecotpw"); @@ -1323,9 +1327,10 @@ function _php_crypt_random_string($characters, $length) * * @param string $pw * @param string $pw_db optional encrypted password + * @param string $username optional, but required when $CONF['encrypt'] = 'dovecot:digest-md5' * @return string encrypted password - if this matches $pw_db then the original password is $pw. */ -function pacrypt($pw, $pw_db = "") +function pacrypt($pw, $pw_db = "", $username = '') { global $CONF; @@ -1382,7 +1387,7 @@ function pacrypt($pw, $pw_db = "") } if (preg_match('/^DOVECOT:(.*)$/i', $mechanism, $matches)) { - return _pacrypt_dovecot($pw, $pw_db); + return _pacrypt_dovecot($pw, $pw_db, $username); } if (empty($pw_db)) { diff --git a/model/Login.php b/model/Login.php index 93086866..d14faacd 100644 --- a/model/Login.php +++ b/model/Login.php @@ -36,7 +36,7 @@ class Login $row = $result[0]; try { - $crypt_password = pacrypt($password, $row['password']); + $crypt_password = pacrypt($password, $row['password'], $username); } catch (\Exception $e) { error_log("Error while trying to call pacrypt()"); error_log("" . $e); @@ -136,7 +136,7 @@ class Login } $set = array( - 'password' => pacrypt($new_password), + 'password' => pacrypt($new_password, '', $username), ); if (Config::bool('password_expiration')) { @@ -226,7 +226,7 @@ class Login throw new \Exception(Config::Lang('pPassword_password_current_text_error')); } - $app_pass = pacrypt($app_pass); + $app_pass = pacrypt($app_pass, '', $username); $result = db_insert('mailbox_app_password', ['username' => $username, 'description' => $app_desc, 'password_hash' => $app_pass], []); diff --git a/model/PFAHandler.php b/model/PFAHandler.php index c9eefae0..9ef9b9e5 100644 --- a/model/PFAHandler.php +++ b/model/PFAHandler.php @@ -882,7 +882,7 @@ abstract class PFAHandler if (sizeof($result) == 1) { $row = $result[0]; - $crypt_token = pacrypt($token, $row['token']); + $crypt_token = pacrypt($token, $row['token'], $username); if ($row['token'] == $crypt_token) { db_update($this->db_table, $this->id_field, $username, array( diff --git a/scripts/snippets/dovecot_crypt.php b/scripts/snippets/dovecot_crypt.php index 6ee548ef..7d215dea 100644 --- a/scripts/snippets/dovecot_crypt.php +++ b/scripts/snippets/dovecot_crypt.php @@ -35,7 +35,7 @@ class DovecotCrypt extends Crypt 'CLEARTEXT' => array('NONE', 0, null, 'plain_generate'), 'CRAM-MD5' => array('HEX', CRAM_MD5_CONTEXTLEN, null, 'cram_md5_generate'), //'HMAC-MD5' => array('HEX', CRAM_MD5_CONTEXTLEN, NULL, 'cram_md5_generate'), - //'DIGEST-MD5' => array('HEX', MD5_RESULTLEN, NULL, 'digest_md5_generate'), + 'DIGEST-MD5' => array('HEX', MD5_RESULTLEN, null, 'digest_md5_generate'), //'PLAIN-MD4' => array('HEX', MD4_RESULTLEN, NULL, 'plain_md4_generate'), //'PLAIN-MD5' => array('HEX', MD5_RESULTLEN, NULL, 'plain_md5_generate'), //'LDAP-MD5' => array('BASE64', MD5_RESULTLEN, NULL, 'plain_md5_generate'), diff --git a/tests/PacryptTest.php b/tests/PacryptTest.php index d61ae63b..f63b4a24 100644 --- a/tests/PacryptTest.php +++ b/tests/PacryptTest.php @@ -73,7 +73,6 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase $this->markTestSkipped("No /usr/bin/doveadm"); } - $CONF['encrypt'] = 'dovecot:SHA1'; $expected_hash = '{SHA1}qUqP5cyxm6YcTAhz05Hph5gvu9M='; @@ -87,6 +86,13 @@ class PaCryptTest extends \PHPUnit\Framework\TestCase $sha512 = '{SHA512}ClAmHr0aOQ/tK/Mm8mc8FFWCpjQtUjIElz0CGTN/gWFqgGmwElh89WNfaSXxtWw2AjDBmyc1AO4BPgMGAb8kJQ=='; // foobar $this->assertNotEquals($sha512, _pacrypt_dovecot('foobarbaz', $sha512)); + + $CONF['encrypt'] = 'dovecot:DIGEST-MD5'; + + $expected_hash = '{DIGEST-MD5}dad736686b7d1f1db09f3dc9ff538e03'; + $username = 'test@mail.com'; + + $this->assertEquals($expected_hash, _pacrypt_dovecot('test', '', $username)); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index cce3da4a..e2b35417 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,7 +12,7 @@ $CONF['language_hook'] = ''; if (getenv('DATABASE') == 'sqlite' || getenv('DATABASE') == false) { $version = PHP_VERSION_ID; // try and stop different tests running at the same trying to use the same sqlite db at once - $db_file = dirname(__FILE__) . '/postfixadmin.sqlite.' . $version . '.test'; + $db_file = tempnam(sys_get_temp_dir(), 'postfixadmin-test'); $CONF['database_type'] = 'sqlite'; $CONF['database_name'] = $db_file; Config::write('database_type', 'sqlite');