Skip to content

Commit

Permalink
feat: support Dovecot DIGEST-MD5 (#816)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bestlong committed Apr 12, 2024
1 parent 53426ac commit 0876c36
Show file tree
Hide file tree
Showing 7 changed files with 39 additions and 22 deletions.
22 changes: 14 additions & 8 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 }}

19 changes: 12 additions & 7 deletions functions.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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'])) {
Expand All @@ -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");
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down
6 changes: 3 additions & 3 deletions model/Login.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function login($username, $password): bool
$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);
Expand Down Expand Up @@ -136,7 +136,7 @@ public function changePassword($username, $new_password, $old_password): bool
}

$set = array(
'password' => pacrypt($new_password),
'password' => pacrypt($new_password, '', $username),
);

if (Config::bool('password_expiration')) {
Expand Down Expand Up @@ -226,7 +226,7 @@ public function addAppPassword(string $username, string $password, string $app_d
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], []);
Expand Down
2 changes: 1 addition & 1 deletion model/PFAHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ public function checkPasswordRecoveryCode($username, $token)
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(
Expand Down
2 changes: 1 addition & 1 deletion scripts/snippets/dovecot_crypt.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
8 changes: 7 additions & 1 deletion tests/PacryptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ public function testPacryptDovecot()
$this->markTestSkipped("No /usr/bin/doveadm");
}


$CONF['encrypt'] = 'dovecot:SHA1';

$expected_hash = '{SHA1}qUqP5cyxm6YcTAhz05Hph5gvu9M=';
Expand All @@ -87,6 +86,13 @@ public function testPacryptDovecot()

$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));
}


Expand Down
2 changes: 1 addition & 1 deletion tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

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');
Expand Down

0 comments on commit 0876c36

Please sign in to comment.