[Web] Implement all supported dovecot password schemas ()

When migrating from other Dovecot based installations it can be very
convenient to just copy over existing hashed passwords.
However, mailcow currently only supports a limited number of password
schemes.

This commit implements all password schemes that do not require
challenge/response or OTP mechanisms.

A convenient way to generate the regex with all supported schemas is
`docker-compose exec dovecot-mailcow doveadm pw -l | awk -F' ' '{printf
"/^{("; for(i=1;i<=NF-1;i++){printf "%s%s", sep, $i; sep="|"}; printf
")}/i\n"}'`

Note that this will also include unsupported challenge/response and OTP
schemas.

Furthermore this increases the vsz_limit for the dovecot auth service to
2G for the use of ARGON2I and ARGON2ID schemas.

Signed-off-by: Felix Kaechele <felix@kaechele.ca>
This commit is contained in:
Felix Kaechele 2021-02-11 03:31:53 -05:00 committed by GitHub
parent 6286cda396
commit 31805f1656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 70 deletions

View File

@ -382,6 +382,7 @@ service auth {
mode = 0600
user = vmail
}
vsz_limit = 2G
}
service managesieve-login {
inet_listener sieve {

View File

@ -483,75 +483,94 @@ function alertbox_log_parser($_data){
}
return false;
}
function verify_hash($hash, $password) {
if (preg_match('/^{SSHA256}/i', $hash)) {
// Remove tag if any
$hash = preg_replace('/^{SSHA256}/i', '', $hash);
// Decode hash
$dhash = base64_decode($hash);
// Get first 32 bytes of binary which equals a SHA256 hash
$ohash = substr($dhash, 0, 32);
// Remove SHA256 hash from decoded hash to get original salt string
$osalt = str_replace($ohash, '', $dhash);
// Check single salted SHA256 hash against extracted hash
if (hash_equals(hash('sha256', $password . $osalt, true), $ohash)) {
return true;
}
}
elseif (preg_match('/^{SSHA}/i', $hash)) {
// Remove tag if any
$hash = preg_replace('/^{SSHA}/i', '', $hash);
function verify_salted_hash($hash, $password, $algo, $salt_length)
{
// Decode hash
$dhash = base64_decode($hash);
// Get first 20 bytes of binary which equals a SSHA hash
$ohash = substr($dhash, 0, 20);
$ohash = substr($dhash, 0, $salt_length);
// Remove SSHA hash from decoded hash to get original salt string
$osalt = str_replace($ohash, '', $dhash);
// Check single salted SSHA hash against extracted hash
if (hash_equals(hash('sha1', $password . $osalt, true), $ohash)) {
if (hash_equals(hash($algo, $password . $osalt, true), $ohash)) {
return true;
}
return false;
}
function verify_hash($hash, $password)
{
if (preg_match('/^{(.+)}(.+)/i', $hash, $hash_array)) {
$scheme = strtoupper($hash_array[1]);
$hash = $hash_array[2];
switch ($scheme) {
case "ARGON2I":
case "ARGON2ID":
case "BLF-CRYPT":
case "CRYPT":
case "DES-CRYPT":
case "MD5-CRYPT":
case "MD5":
case "SHA256-CRYPT":
case "SHA512-CRYPT":
return password_verify($password, $hash);
case "CLEAR":
case "CLEARTEXT":
case "PLAIN":
return $password == $hash;
case "LDAP-MD5":
$hash = base64_decode($hash);
return hash_equals(hash('md5', $password, true), $hash);
case "PBKDF2":
$components = explode('$', $hash);
$salt = $components[2];
$rounds = $components[3];
$hash = $components[4];
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
case "PLAIN-MD4":
return hash_equals(hash('md4', $password), $hash);
case "PLAIN-MD5":
return md5($password) == $hash;
case "PLAIN-TRUNC":
$components = explode('-', $hash);
if (count($components) > 1) {
$trunc_len = $components[0];
$trunc_password = $components[1];
return substr($password, 0, $trunc_len) == $trunc_password;
} else {
return $password == $hash;
}
elseif (preg_match('/^{PLAIN-MD5}/i', $hash)) {
$hash = preg_replace('/^{PLAIN-MD5}/i', '', $hash);
if (md5($password) == $hash) {
return true;
}
}
elseif (preg_match('/^{SHA512-CRYPT}/i', $hash)) {
// Remove tag if any
$hash = preg_replace('/^{SHA512-CRYPT}/i', '', $hash);
// Decode hash
preg_match('/\\$6\\$(.*)\\$(.*)/i', $hash, $hash_array);
$osalt = $hash_array[1];
$ohash = $hash_array[2];
if (hash_equals(crypt($password, '$6$' . $osalt . '$'), $hash)) {
return true;
}
}
elseif (preg_match('/^{SSHA512}/i', $hash)) {
$hash = preg_replace('/^{SSHA512}/i', '', $hash);
// Decode hash
$dhash = base64_decode($hash);
// Get first 64 bytes of binary which equals a SHA512 hash
$ohash = substr($dhash, 0, 64);
// Remove SHA512 hash from decoded hash to get original salt string
$osalt = str_replace($ohash, '', $dhash);
// Check single salted SHA512 hash against extracted hash
if (hash_equals(hash('sha512', $password . $osalt, true), $ohash)) {
return true;
}
}
elseif (preg_match('/^{MD5-CRYPT}/i', $hash)) {
$hash = preg_replace('/^{MD5-CRYPT}/i', '', $hash);
if (password_verify($password, $hash)) {
return true;
}
}
elseif (preg_match('/^{BLF-CRYPT}/i', $hash)) {
$hash = preg_replace('/^{BLF-CRYPT}/i', '', $hash);
if (password_verify($password, $hash)) {
return true;
case "SHA":
case "SHA1":
case "SHA256":
case "SHA512":
// SHA is an alias for SHA1
$scheme = $scheme == "SHA" ? "sha1" : strtolower($scheme);
$hash = base64_decode($hash);
return hash_equals(hash($scheme, $password, true), $hash);
case "SMD5":
return verify_salted_hash($hash, $password, 'md5', 16);
case "SSHA":
return verify_salted_hash($hash, $password, 'sha1', 20);
case "SSHA256":
return verify_salted_hash($hash, $password, 'sha256', 32);
case "SSHA512":
return verify_salted_hash($hash, $password, 'sha512', 64);
default:
return false;
}
}
return false;

View File

@ -1055,7 +1055,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false;
}
// support pre hashed passwords
if (preg_match('/^({SSHA256}|{SSHA}|{SHA512-CRYPT}|{SSHA512}|{MD5-CRYPT}|{PLAIN-MD5})/i', $password)) {
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password;
}
else {
@ -2557,7 +2557,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
continue;
}
// support pre hashed passwords
if (preg_match('/^({SSHA256}|{SSHA}|{SHA512-CRYPT}|{SSHA512}|{MD5-CRYPT}|{PLAIN-MD5})/i', $password)) {
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password;
}
else {