From d96bf91a0dfc45109e0396546f57ea9297071c03 Mon Sep 17 00:00:00 2001
From: Lukas Schreiner <github@lschreiner.de>
Date: Sun, 15 Nov 2020 20:22:35 +0100
Subject: [PATCH] Support of different default pass schemes + support of
 BLF-CRYPT (#3832)

* Introduce MAILCOW_PASS_SCHEME in order to support blowfish (cf. mailcow/mailcow-dockerized#1019)

* Furthermore added dovecot to support new environment varible for MAILCOW_PASS_SCHEME defaulted to SSHA256

* Revert changes regarding gitignore.

* Added fallback to SSHA256 if environment is not proper prepared.

* No fallback within management frontend, as it must match to other components.

* Unified and corrected alignment; implemented support of SSHA512

* Currently, password_hash of PHP is using by default bcrypt (BLF). As this might change later, we must ensure, that BLF is still used after PHP changes its default.

* Switched to BLF-CRYPT by default (even on update)

* Switched to BLF-CRYPT by default (even on update)

* Adding information in config generation / update with link to supported hash algorithm

* Bump sogo version to 1.92

* Fallback to BLF-CRYPT in case password scheme is not proper defined for Mailcow administration.
---
 data/Dockerfiles/dovecot/docker-entrypoint.sh |  2 +-
 data/Dockerfiles/sogo/bootstrap-sogo.sh       |  2 +-
 data/web/inc/functions.inc.php                | 27 +++++++++++++++++--
 data/web/inc/vars.inc.php                     |  1 +
 docker-compose.yml                            |  5 +++-
 generate_config.sh                            |  5 ++++
 update.sh                                     |  9 +++++++
 7 files changed, 46 insertions(+), 5 deletions(-)

diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh
index 63c63220..2bd160fe 100755
--- a/data/Dockerfiles/dovecot/docker-entrypoint.sh
+++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh
@@ -141,7 +141,7 @@ cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-passdb.conf
 # Autogenerated by mailcow
 driver = mysql
 connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
-default_pass_scheme = SSHA256
+default_pass_scheme = ${MAILCOW_PASS_SCHEME}
 password_query = SELECT password FROM mailbox WHERE active = '1' AND username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') AND JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) != '1' AND (JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')) = '1' OR ('%s' != 'imap' AND '%s' != 'pop3'))
 EOF
 
diff --git a/data/Dockerfiles/sogo/bootstrap-sogo.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh
index d3b16fa9..fef7958b 100755
--- a/data/Dockerfiles/sogo/bootstrap-sogo.sh
+++ b/data/Dockerfiles/sogo/bootstrap-sogo.sh
@@ -204,7 +204,7 @@ while read -r line gal
                     <key>type</key>
                     <string>sql</string>
                     <key>userPasswordAlgorithm</key>
-                    <string>ssha256</string>
+                    <string>${MAILCOW_PASS_SCHEME}</string>
                     <key>prependPasswordScheme</key>
                     <string>YES</string>
                     <key>viewURL</key>
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index 8bf8d59f..761ad86b 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -84,8 +84,25 @@ function ip_acl($ip, $networks) {
   return false;
 }
 function hash_password($password) {
-	$salt_str = bin2hex(openssl_random_pseudo_bytes(8));
-	return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
+  // default_pass_scheme is determined in vars.inc.php (or corresponding local file)
+  // in case default pass scheme is not defined, falling back to BLF-CRYPT.
+  global $default_pass_scheme;
+  $pw_hash = NULL;
+  switch (strtoupper($default_pass_scheme)) {
+    case "SSHA256":
+      $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+      $pw_hash = "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
+      break;
+    case "SSHA512":
+      $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+      $pw_hash = "{SSHA512}".base64_encode(hash('sha512', $password . $salt_str, true) . $salt_str);
+      break;
+    case "BLF-CRYPT":
+    default:
+      $pw_hash = "{BLF-CRYPT}" . password_hash($password, PASSWORD_BCRYPT);
+      break;
+  }
+  return $pw_hash;
 }
 function last_login($user) {
   global $pdo;
@@ -502,6 +519,12 @@ function verify_hash($hash, $password) {
     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;
+    }
   }
   return false;
 }
diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php
index 993b7975..1c02bf96 100644
--- a/data/web/inc/vars.inc.php
+++ b/data/web/inc/vars.inc.php
@@ -17,6 +17,7 @@ $database_name = getenv('DBNAME');
 
 // Other variables
 $mailcow_hostname = getenv('MAILCOW_HOSTNAME');
+$default_pass_scheme = getenv('MAILCOW_PASS_SCHEME');
 
 // Autodiscover settings
 // ===
diff --git a/docker-compose.yml b/docker-compose.yml
index d9251a28..651e5101 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -133,6 +133,7 @@ services:
         - DBUSER=${DBUSER}
         - DBPASS=${DBPASS}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+        - MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
         - IMAP_PORT=${IMAP_PORT:-143}
         - IMAPS_PORT=${IMAPS_PORT:-993}
         - POP_PORT=${POP_PORT:-110}
@@ -159,7 +160,7 @@ services:
             - phpfpm
 
     sogo-mailcow:
-      image: mailcow/sogo:1.91
+      image: mailcow/sogo:1.92
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
@@ -167,6 +168,7 @@ services:
         - TZ=${TZ}
         - LOG_LINES=${LOG_LINES:-9999}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+        - MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
         - ACL_ANYONE=${ACL_ANYONE:-disallow}
         - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
         - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
@@ -221,6 +223,7 @@ services:
         - DBPASS=${DBPASS}
         - TZ=${TZ}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
+        - MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
         - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
         - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
         - MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-1440}
diff --git a/generate_config.sh b/generate_config.sh
index e4d23f7a..0c4b43c3 100755
--- a/generate_config.sh
+++ b/generate_config.sh
@@ -117,6 +117,11 @@ cat << EOF > mailcow.conf
 
 MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
 
+# Password hash algorithm
+# Only certain password hash algorithm are supported. For a fully list of supported schemes,
+# see https://mailcow.github.io/mailcow-dockerized-docs/model-passwd/
+MAILCOW_PASS_SCHEME=BLF-CRYPT
+
 # ------------------------------
 # SQL database configuration
 # ------------------------------
diff --git a/update.sh b/update.sh
index 1bd1fe2e..adb34266 100755
--- a/update.sh
+++ b/update.sh
@@ -217,6 +217,7 @@ CONFIG_ARRAY=(
   "REDIS_PORT"
   "DOVECOT_MASTER_USER"
   "DOVECOT_MASTER_PASS"
+  "MAILCOW_PASS_SCHEME"
 )
 
 sed -i --follow-symlinks '$a\' mailcow.conf
@@ -390,6 +391,14 @@ for option in ${CONFIG_ARRAY[@]}; do
       echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
       echo "DOVECOT_MASTER_PASS=" >> mailcow.conf
   fi
+  elif [[ ${option} == "MAILCOW_PASS_SCHEME" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo '# Password hash algorithm' >> mailcow.conf
+      echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf
+      echo '# see https://mailcow.github.io/mailcow-dockerized-docs/model-passwd/' >> mailcow.conf
+      echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf
+  fi
   elif ! grep -q ${option} mailcow.conf; then
     echo "Adding new option \"${option}\" to mailcow.conf"
     echo "${option}=n" >> mailcow.conf