blowfish

Hi everyone, happy new year :)

While Dovecot support a lot of different password schemes, making both Postfixadmin and Roundcube playing nice by using something else than the old MD5 scheme need a little work.

In this post I'll explain how to configure them to use a Blowfish scheme (BLF-CRYPT in the Dovecot terminology), but you can easily adapt the steps to use something else. You should already have Dovecot, Postfixadmin, and Roundcube (with the password plugin) up and running.

First of all

Let me state the obvious: backup your configuration files, database, etc. before any changes.

Checking BLF-CRYPT support

We need to check if the system support BLF-CRYPT. As stated in the documentation: BLF-CRYPT, SHA256-CRYPT, and SHA512-CRYPT are optional and their availability depends on the system's libc.

% doveadm pw -s BLF-CRYPT -p secret

If you get something like:

Fatal: Unknown scheme: BLF-CRYPT

Then BLF-CRYPT is not available on your system. You can run doveadm pw -l to see the list of supported scheme, and pick the best one available. If you don't know how to choose, the Dovecot documentation is a good start.

Computing the ideal number of round

The shorter the number of rounds, the faster a password hash is computed; the faster a password hash is computed, the faster is an attack trying many passwords (i.e. Brute-force attack). On the other hand, a high number rounds means it is harder to brute-force but also harder to compute for legitimate use (in this case users connecting to their emails). For a good read on the question check Thomas Pornin's answer on stackexchange.

In this example we'll pick the least number of rounds taking more than 250ms to compute a password hash (try different values for -r until happy):

% /usr/bin/time doveadm pw -s BLF-CRYPT -r 12 -p secret
{BLF-CRYPT}$2a$12$Y7ai9J06MhusOmJ0r/0nB.Aok4gts53SCDx/pZLN0FwjS09BBRWPa
        0.30 real         0.30 user         0.00 sys

Be sure to run theses commands on the same machine that will run the Dovecot server, the whole point is to find a minimum computation time when user actually login on the "production" server.

Postfixadmin configuration

Here is the relevant configuration part:

config.local.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// ...
// Encrypt
// In what way do you want the passwords to be crypted?
// md5crypt = internal postfix admin md5
// md5 = md5 sum of the password
// system = whatever you have set as your PHP system default
// cleartext = clear text passwords (ouch!)
// mysql_encrypt = useful for PAM integration
// authlib = support for courier-authlib style passwords
// dovecot:CRYPT-METHOD = use dovecotpw -s 'CRYPT-METHOD'. Example: dovecot:CRAM-MD5
//   (WARNING: don't use dovecot:* methods that include the username in the
//   hash - you won't be able to login to PostfixAdmin in this case)
$CONF['encrypt'] = 'dovecot:BLF-CRYPT';

// If you use the dovecot encryption method: where is the dovecotpw binary located?
// for dovecot 1.x
// $CONF['dovecotpw'] = "/usr/sbin/dovecotpw";
// for dovecot 2.x (dovecot 2.0.0 - 2.0.7 is not supported!)
// $CONF['dovecotpw'] = "/usr/sbin/doveadm pw";
$CONF['dovecotpw'] = "/usr/local/bin/doveadm pw -r 12";
  1. Don't forget to adapt the -r option value to the number of rounds found in the last step. Also check your doveadm executable path (/usr/local/bin/doveadm in this example).

Old password scheme backward compatibility

Now in order to allow admin account to still login using the "old" password scheme (which is still the one used in your database at this point!), you need to figure out which one it is. First look at one of the password from the postfix database:

SELECT password FROM admin LIMIT 1;

Then check the Key derivation Functions table to find the corresponding scheme. For example, if your password look like $1$etNnh7FA$OlM7eljE/B7F1J4XYNnk81 then it is using the MD5 scheme.

Once you know the old passwords scheme you need to find how Dovecot prefix it. The prefix should be easy to guess knowing the scheme name (for MD5 it is MD5-CRYPT), you can check by running doveadm pw -s CRYPT-NAME-TO-TEST -p secret and see if the result looks like the corresponding Wikipedia's example. Again, the full list of scheme supported by Dovecot doveadm pw -l may be helpful.

Alright! Now we can prefix all the old password so they can still be used to log in:

UPDATE admin   SET password = CONCAT('{MD5-CRYPT}', password);
UPDATE mailbox SET password = CONCAT('{MD5-CRYPT}', password);

Postfixadmin checkup

Here is a (non-exhaustive) list of steps to check that the new configuration work as expected:

  1. Login using a Postfixadmin admin account, it should work and still use the "old" password scheme.

  2. Once logged in, change your password. Once updated, check that your new password has been hashed using the new password scheme:

    SELECT password FROM admin ORDER BY modified DESC LIMIT 1;
  3. Logout and login again, it should work now using the new password scheme.

  4. Create a new mailbox and check that its password has been hashed using the new password scheme:

    SELECT password FROM mailbox ORDER BY modified DESC LIMIT 1;

Roundcube password plugin configuration

Here is the relevant configuration part:

plugins/password/config.inc.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

// Password Plugin options
// -----------------------
// ...
// force saving when the new password is the same as the current password, so
// that it uses the latest configured password scheme.
$rcmail_config['password_force_save'] = true;
// ...

// SQL Driver options
// ------------------
// ...
// The SQL query used to change the password.
// The query can contain the following macros that will be expanded as follows:
//      %p is replaced with the plaintext new password
//      %c is replaced with the crypt version of the new password, MD5 if available
//         otherwise DES.
//      %D is replaced with the dovecotpw-crypted version of the new password
//      %o is replaced with the password before the change
//      %n is replaced with the hashed version of the new password
//      %q is replaced with the hashed password before the change
//      %h is replaced with the imap host (from the session info)
//      %u is replaced with the username (from the session info)
//      %l is replaced with the local part of the username
//         (in case the username is an email address)
//      %d is replaced with the domain part of the username
//         (in case the username is an email address)
// Escaping of macros is handled by this module.
// Default: "SELECT update_passwd(%c, %u)"
$rcmail_config['password_query'] = "UPDATE mailbox SET password = %D, modified = NOW() WHERE username = %u";

// By default domains in variables are using unicode.
// Enable this option to use punycoded names
$rcmail_config['password_idn_ascii'] = false;

// Path for dovecotpw (if not in $PATH)
$rcmail_config['password_dovecotpw'] = '/usr/local/bin/doveadm pw -r 12';

// Dovecot method (dovecotpw -s 'method')
$rcmail_config['password_dovecotpw_method'] = 'BLF-CRYPT';

// Enables use of password with crypt method prefix in %D, e.g. {MD5}$1$LUiMYWqx$fEkg/ggr/L6Mb2X7be4i1/
$rcmail_config['password_dovecotpw_with_method'] = true;
  1. YMMV regarding the password_query but the important part is to use %D as it is the dovecotpw-crypted version.
  2. Don't forget to adapt the -r option value to the number of rounds found in the last step. Also check your doveadm executable path (/usr/local/bin/doveadm in this example).
  3. We use crypt method prefix in %D to be consistent with Postfixadmin.

Roundcube checkup

Here is a (non-exhaustive) list of steps to check that the new configuration work as expected:

  1. Login from Roundcube with your usual access. It should work and still use the "old" password scheme.

  2. Once logged in, change your password. Once updated, check that your new password has been hashed using the new password scheme:

    SELECT password FROM mailbox ORDER BY modified DESC LIMIT 1;
  3. Logout and login again, it should work now using the new password scheme.

Automatic password scheme migration

Dovecot has a facility to run post-login script which can be used to migrate mailbox password to our freshly configured scheme.

Dovecot config

First, the password_query of dovecot-sql.conf.ext must be adapted to setup the plaintext password in the Dovecot environment. This is what we need to add to the SELECT query:

'%w' AS userdb_plain_pass

For example, this is what my password_query looked like before:

password_query = SELECT password, CONCAT('/home/vmail/', maildir) AS userdb_home, concat('*:bytes=', quota) AS userdb_quota_rule FROM mailbox WHERE username = '%u' AND domain = '%d' AND active

And now:

password_query = SELECT password, '%w' AS userdb_plain_pass, CONCAT('/home/vmail/', maildir) AS userdb_home, concat('*:bytes=', quota) AS userdb_quota_rule FROM mailbox WHERE username = '%u' AND domain = '%d' AND active

Then, make sure you have configured "userdb prefetch" correctly. In your Dovecot configuration, look for:

# "prefetch" user database means that the passdb already provided the
# needed information and there's no need to do a separate userdb lookup.
# <doc/wiki/UserDatabase.Prefetch.txt>
userdb {
  driver = prefetch
}

Finally, in order to have our script run on pop3 and imap login, we need to adapt both service imap and service pop3. Here is the diff of my configuration but of course YMMV:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
--- usr/local/etc/dovecot/conf.d/10-master.conf 2014-10-27 22:09:44.000000000 +0100
+++ /usr/local/etc/dovecot/conf.d/10-master.conf        2017-05-24 19:40:19.000353000 +0200
@@ -67,6 +67,8 @@
 }

 service imap {
+  # see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes
+  executable = imap imap-postlogin
   # Most of the memory goes to mmap()ing files. You may need to increase this
   # limit if you have huge mailboxes.
   #vsz_limit = $default_vsz_limit
@@ -76,6 +78,8 @@
 }

 service pop3 {
+  # see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes
+  executable = pop3 pop3-postlogin
   # Max. number of POP3 processes (connections)
   #process_limit = 1024
 }
@@ -131,3 +135,19 @@
     #group =
   }
 }
+
+# see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes
+service imap-postlogin {
+  executable = script-login /usr/local/etc/dovecot/postlogin-updatepw.php
+  user = $default_internal_user
+  unix_listener imap-postlogin {
+  }
+}
+
+# see https://wiki2.dovecot.org/HowTo/ConvertPasswordSchemes
+service pop3-postlogin {
+  executable = script-login /usr/local/etc/dovecot/postlogin-updatepw.php
+  user = $default_internal_user
+  unix_listener pop3-postlogin {
+  }
+}

Remember the configured script-login path on line 28 and 36, we'll use it right after.

password scheme update script

Now we'll write the post-login script that will update the mailbox password to the new scheme. I've chosen to write my own for several reasons I will outline later.

Let's start by creating the file directly with the right permissions. It is important because the script will contain the credentials to connect to our mailbox database.

# /usr/bin/install -m 750 -g dovecot -o root /dev/null /usr/local/etc/dovecot/postlogin-updatepw.php

I've set the group to dovecot here because it match my configured $default_internal_user but adapt if needed. If the permissions mismatch, you'll get some "Permission denied" messages in your maillog looking like this:

blablabla dovecot: imap-postlogin: Error: script-login(blowfish@kaworu.ch): Fatal: execvp(/usr/local/etc/dovecot/postlogin-updatepw.php) failed: Permission denied
blablabla dovecot: imap(blowfish@kaworu.ch): Post-login script denied access to user blowfish@kaworu.ch
blablabla dovecot: imap-postlogin: Fatal: master: service(imap-postlogin): child 42 returned error 84 (exec() failed)

The real issue though is that the login will fail, so take extra care.

Here is the full script. It has been written in PHP because since you have Postfixadmin and Roundcube installed you probably have PHP too :)

The pcntl shared extension for PHP is required to run this script. It also need PDO (and your database PDO driver) but Roundcube uses PDO, so it should be safe to assume that it is installed (and working). If Dovecot runs out of memory while calling the postlogin script (fails to load libraries) try tweaking default_vsz_limit in the Dovecot configuration.

/usr/local/etc/dovecot/postlogin-updatepw.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#!/usr/bin/env php
<?php
// PDO dsn
$pdo_dsn = 'pgsql:host=localhost;dbname=postfix;user=postfixadmin;password=secret';
// $rcmail_config['password_dovecotpw']
$dovecotpw = '/usr/local/bin/doveadm pw -r 12';
// $rcmail_config['password_dovecotpw_method']
$dovecotpw_method = 'BLF-CRYPT';
// where we log
$syslog_facility = LOG_MAIL;

// grab what we care about from the env.
$username  = getenv("USER");
$plainpass = getenv("PLAIN_PASS");

// init syslog.
$progname = basename(__FILE__);
openlog($progname, LOG_PID, $syslog_facility);

// connect to the database.
try {
    $dbh = new PDO($pdo_dsn);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    syslog(LOG_CRIT, "database connection failed: {$e->getMessage()}");
    goto out;
}

try {
    // retrieve the user's current password.
    $sth = $dbh->prepare('SELECT password FROM mailbox WHERE username = ?');
    $sth->execute([$username]);
    $oldpasswd = $sth->fetchColumn();
    if (!$oldpasswd) {
        syslog(LOG_WARNING, "unable to find the mailbox for $username");
        goto out;
    }

    // bail out if the new password is empty.
    if (strlen($plainpass) === 0) {
        syslog(LOG_ERR, "empty password for $username!");
        goto out;
    }

    // if we find the dovecot password method in the current password, it does
    // not need to be updated.
    if (strpos($oldpasswd, $dovecotpw_method) !== false) {
        syslog(LOG_INFO, "$username already use $dovecotpw_method, skipping.");
        goto out;
    }

    // generate a new password using `doveadm pw' without passing plainpass
    // on the cmdline for security concerns.
    $process = proc_open("$dovecotpw -s $dovecotpw_method", [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w'],
    ], $pipes);
    if (!is_resource($process)) {
        syslog(LOG_CRIT, "proc_open() failed.");
        goto out;
    }
    // $pipes now looks like this:
    // 0 => writeable handle connected to child stdin
    // 1 => readable handle connected to child stdout
    // 2 => readable handle connected to child stderr
    fclose($pipes[2]); // immediately close stderr as we don't need it.
    fwrite($pipes[0], "$plainpass\n");
    fwrite($pipes[0], "$plainpass\n");
    fclose($pipes[0]);
    $newpasswd = trim(stream_get_contents($pipes[1]));
    fclose($pipes[1]);
    $retval = proc_close($process);
    if ($retval !== 0) {
        syslog(LOG_ERR, "$dovecotpw exited with status $retval, expected 0.");
        goto out;
    }

    // sanity check to ensure that the new password has been computed with the
    // requested method.
    if (strpos($newpasswd, $dovecotpw_method) === false) {
        syslog(LOG_ERR, "unexpected $dovecotpw output.");
        goto out;
    }

    // update the password in the database with the newly computed one.
    $sth = $dbh->prepare('UPDATE mailbox SET password = :newpasswd WHERE username = :username AND password = :oldpasswd');
    $success = $sth->execute([
        ':newpasswd' => $newpasswd,
        ':username'  => $username,
        ':oldpasswd' => $oldpasswd,
    ]);

    // "close" the database connection,
    // see https://secure.php.net/manual/en/pdo.connections.php
    $sth = null;
    $dbh = null;
} catch (PDOException $e) {
    syslog(LOG_CRIT, "database query failed: {$e->getMessage()}");
    goto out;
}

if ($success) {
    syslog(LOG_INFO, "$username password successfully migrated to $dovecotpw_method.");
} else {
    syslog(LOG_CRIT, "$username password migration to $dovecotpw_method failed.");
}

// FALLTHROUGH
out: // cleanup.

// close syslog.
closelog();

/*
 * We have to continue execution from what we get on the command line argument,
 * i.e. $argv.
 *
 * see https://wiki.dovecot.org/PostLoginScripting
 */

// $argv[0] is our script (i.e. __FILE__), $argv[1] the next program to
// execute, and $argv[2..] the next program's arguments.
$next_exe  = $argv[1];
$next_argv = array_slice($argv, 2);
pcntl_exec($next_exe, $next_argv);

Lines 3 through 10 are meant to be adapted, most of them from the Roundcube password plugin configuration. Also, depending on your database schema, the SQL queries on lines 31 and 81 may need to be changed too.

This script has several advantages over the example(s) you can find from the Dovecot wiki:

  • It uses exclusively syslog's LOG_MAIL for messages, so its output is handily around the Dovecot logs. You might care or not.
  • It is not limited to MySQL as I am using PostgreSQL. Again, not a big deal.
  • It only compute the new password scheme when the password actually need to be updated. This is relevant as we specifically picked a number of round that would be costly.
  • It uses doveadm(1) in a way that doesn't leak the password on the command line. This is critical, when you call doveadm pw -p secret secret (the plaintext password) is part of the cmdline that can be seen from other processes (e.g. when running ps a).

Testing the password migration

Reload Dovecot's configuration:

# service dovecot reload

Now when a user with a mailbox using the old password scheme login (through imap for example), you should see something like this in your maillog:

blablabla dovecot: imap-login: Login: user=<blowfish@kaworu.ch>, method=PLAIN, rip=2a01:4f8:120:5388::1, lip=2a01:4f8:120:5388::1, ...
blablabla postlogin-updatepw.php[666]: blowfish@kaworu.ch password successfully migrated to BLF-CRYPT.

Then if you check in the database, its password scheme should have been updated to BLF-CRYPT. The next logins from this user should generate messages like this:

blablabla dovecot: imap-login: Login: user=<blowfish@kaworu.ch>, method=PLAIN, rip=2a01:4f8:120:5388::1, lip=2a01:4f8:120:5388::1, ...
blablabla postlogin-updatepw.php[667]: blowfish@kaworu.ch already use BLF-CRYPT, skipping.

The End.

And voilà! Now only the Postfixadmin account passwords needs to be updated, but it should be not too much trouble to force them to update their password :)

Many thanks to Aki Tuomi for pointing out the Dovecot wiki documentation about the password scheme migration. It prompted me to test it and complete this article.