Browse Source

Switch from argon2 to bcrypt (#2752)

* Switch from argon2 to bcrypt

Add support for Bcrypt as of "low" costs strength  is higher. 

ArgonID > Bcrypt  only after 1000ms or more

* code-breaking typo
divinity76 3 years ago
parent
commit
6c4226c106

+ 7 - 2
bin/v-add-mail-account

@@ -66,11 +66,16 @@ check_hestia_demo_mode
 #----------------------------------------------------------#
 
 # Generating hashed password
-if [ -n "$(doveadm pw -l | grep ARGON2ID)" ]; then
+
+if [ -n "$(doveadm pw -l | grep BLF-CRYPT)" ]; then
+    set +H # disable ! style history substitution
+    md5="$(doveadm pw -s BLF-CRYPT -p "$password")"
+elif [ -n "$(doveadm pw -l | grep ARGON2ID)" ]; then
+    # Fall back on Argon2id if bcrypt is not available
     set +H # disable ! style history substitution
     md5="$(doveadm pw -s ARGON2ID -p "$password")"
 else
-    # Fall back on MD5
+    # Fall back on MD5 if neither bcrypt nor argon2id is available
     salt=$(generate_password "$PW_MATRIX" "8")
     md5="{MD5}$($BIN/v-generate-password-hash md5 $salt <<<$password)"
 fi

+ 6 - 1
bin/v-change-mail-account-password

@@ -56,10 +56,15 @@ check_hestia_demo_mode
 #----------------------------------------------------------#
 
 # Generating hashed password
-if [ -n "$(doveadm pw -l | grep ARGON2ID)" ]; then
+if [ -n "$(doveadm pw -l | grep BLF-CRYPT)" ]; then
+    set +H # disable ! style history substitution
+    md5="$(doveadm pw -s BLF-CRYPT -p "$password")"
+elif [ -n "$(doveadm pw -l | grep ARGON2ID)" ]; then
+    # Fall back on Argon2id if bcrypt is not available
     set +H # disable ! style history substitution
     md5="$(doveadm pw -s ARGON2ID -p "$password")"
 else
+    # Fall back on MD5 if neither bcrypt nor argon2id is available
     salt=$(generate_password "$PW_MATRIX" "8")
     md5="{MD5}$($BIN/v-generate-password-hash md5 $salt <<<$password)"
 fi

+ 11 - 2
bin/v-check-mail-account-hash

@@ -35,7 +35,15 @@ is_password_valid
 #                       Action                             #
 #----------------------------------------------------------#
 
-if [ "$type" = "ARGONID2" ]; then
+if [ "$type" = "BCRYPT" ]; then
+    match=$(doveadm pw -s BLF-CRYPT -p "$password" -t $hash | grep "verified");
+    if [ -n "$match" ]; then
+        exit 0;
+    else
+        echo $match;
+        exit 2;
+    fi
+elif [ "$type" = "ARGONID2" ]; then
     match=$(doveadm pw -s ARGON2ID -p "$password" -t $hash | grep "verified");
     if [ -n "$match" ]; then
         exit 0;
@@ -44,10 +52,11 @@ if [ "$type" = "ARGONID2" ]; then
         exit 2;
     fi
 else
-    echo "Not supported"
+    echo "unsupported hash type.";
     exit 2;
 fi
 
+
 #----------------------------------------------------------#
 #                       Hestia                             #
 #----------------------------------------------------------#

+ 53 - 71
web/reset/mail/index.php

@@ -2,12 +2,14 @@
 // Init
 define('NO_AUTH_REQUIRED',true);
 define('NO_AUTH_REQUIRED2',true);
+header('Content-Type: text/plain; charset=utf-8');
 
 include($_SERVER['DOCUMENT_ROOT']."/inc/main.php");
 
 // Checking IP of incoming connection, checking is it NAT address
 $ok=0;
 $ip=$_SERVER['REMOTE_ADDR'];
+
 exec (HESTIA_CMD."v-list-sys-ips json", $output, $return_var);
 $output=implode('', $output);
 $arr=json_decode($output, true);
@@ -24,83 +26,63 @@ if ($ok==0) exit;
 if (isset($_SERVER['HTTP_X_REAL_IP']) || isset($_SERVER['HTTP_X_FORWARDED_FOR'])) exit;
 
 
-/**
- * md5 crypt() password
- *
- * @param string $password
- * @param string $salt
- * 
- * @throws InvalidArgumentException if salt is emptystring
- * @throws InvalidArgumentException if salt is longer than 8 characters
- * @return string
- */
-function md5crypt(string $pw, string $salt): string
-{
-    if (strlen($salt) < 1) {
-        // old implementation would crash with error "function generate_salt not defined", lets throw an exception instead
-        throw new InvalidArgumentException('salt not given!');
-    }
-    if (strlen($salt) > 8) {
-        throw new \InvalidArgumentException("maximum supported salt length for MD5 crypt is 8 characters!");
-    }
-    return crypt($pw, '$1$' . $salt);
-}
-
 
 // Check arguments
-if ((!empty($_POST['email'])) && (!empty($_POST['password'])) && (!empty($_POST['new']))) {
-    list($v_account, $v_domain) = explode('@', $_POST['email']);
-    $v_domain = escapeshellarg($v_domain);
-    $v_account = escapeshellarg($v_account);
-    $v_password = $_POST['password'];
 
-    // Get domain owner
-    exec (HESTIA_CMD."v-search-domain-owner ".$v_domain." 'mail'", $output, $return_var);
-    if ($return_var == 0) {
-        $v_user = $output[0];
-    }
-    unset($output);
+if (empty($_POST['email'])) {
+    echo "error email address not provided";
+    exit;
+}
+if (empty($_POST['password'])) {
+    echo "error old password provided";
+    exit;
+}
+if (empty($_POST['new'])) {
+    echo "error new password not provided";
+    exit;
+}
 
-    // Get current md5 hash
-    if (!empty($v_user)) {
-        exec (HESTIA_CMD."v-get-mail-account-value ".escapeshellarg($v_user)." ".$v_domain." ".$v_account." 'md5'", $output, $return_var);
-        if ($return_var == 0) {
-            $v_hash = $output[0];
-        }
-    }
-    unset($output);
+list($v_account, $v_domain) = explode('@', $_POST['email']);
+$v_domain = escapeshellarg($v_domain);
+$v_account = escapeshellarg($v_account);
+$v_password = $_POST['password'];
 
-    // Compare hashes
-    if (!empty($v_hash)) {
-        $salt = explode('$', $v_hash);
-        if($salt[0] == "{MD5}"){
-        $n_hash = md5crypt($v_password, $salt[2]);
-        $n_hash = '{MD5}'.$n_hash;
-        }else{
-            $v_password = escapeshellarg($v_password);
-            $s_hash = escapeshellarg($v_hash);
-            exec(HESTIA_CMD."v-check-mail-account-hash ARGONID2 ". $v_password ." ". $s_hash, $output, $return_var);
-            if($return_var != 0){
-                $n_hash = '';
-            }else{
-                $n_hash = $v_hash;
-            }
-        }
-        // Change password
-        if ( $v_hash == $n_hash ) {
-            $v_new_password = tempnam("/tmp","vst");
-            $fp = fopen($v_new_password, "w");
-            fwrite($fp, $_POST['new']."\n");
-            fclose($fp);
-            exec (HESTIA_CMD."v-change-mail-account-password ".escapeshellarg($v_user)." ".$v_domain." ".$v_account." ".$v_new_password, $output, $return_var);
-            if ($return_var == 0) {
-                echo "==ok==";
-                exit;
-            }
-        }
-    }
+// Get domain owner
+exec(HESTIA_CMD . "v-search-domain-owner " . $v_domain . " 'mail'", $output, $return_var);
+if ($return_var != 0 || empty($output[0])) {
+    echo "error domain owner not found";
+    exit;
 }
+$v_user = $output[0];
+unset($output);
 
-echo 'error';
 
+// Get current password hash (called "md5" for legacy reasons, it's not guaranteed to be md5)
+exec(HESTIA_CMD . "v-get-mail-account-value " . escapeshellarg($v_user) . " " . $v_domain . " " . $v_account . " 'md5'", $output, $return_var);
+if ($return_var != 0 || empty($output[0])) {
+    echo "error unable to get current account password hash";
+    exit;
+}
+$v_hash = $output[0];
+unset($output);
+
+// v_hash use doveadm password hash format, which is basically {HASH_NAME}normal_crypt_format,
+// so we just need to remove the {HASH_NAME} before we can ask password_verify if its correct or not.
+$hash_for_password_verify = explode('}', $v_hash, 2);
+$hash_for_password_verify = end($hash_for_password_verify);
+if (!password_verify($v_password, $hash_for_password_verify)) {
+    die("error old password does not match");
+}
+
+// Change password
+$fp = tmpfile();
+$new_password_file = stream_get_meta_data($fp)['uri'];
+fwrite($fp, $_POST['new'] . "\n");
+exec(HESTIA_CMD . "v-change-mail-account-password " . escapeshellarg($v_user) . " " . $v_domain . " " . $v_account . " " . escapeshellarg($new_password_file), $output, $return_var);
+fclose($fp);
+if ($return_var == 0) {
+    echo "==ok==";
+    exit;
+}
+echo 'error v-change-mail-account-password returned non-zero: ' . $return_var;
 exit;