Bläddra i källkod

[Web] Feature: Add password policy

andryyy 4 år sedan
förälder
incheckning
5ea649b292

+ 52 - 8
data/web/admin.php

@@ -23,6 +23,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
         <li role="presentation"><a href="#tab-config-quarantine" aria-controls="tab-config-quarantine" role="tab" data-toggle="tab"><?=$lang['admin']['quarantine'];?></a></li>
         <li role="presentation"><a href="#tab-config-quota" aria-controls="tab-config-quota" role="tab" data-toggle="tab"><?=$lang['admin']['quota_notifications'];?></a></li>
         <li role="presentation"><a href="#tab-config-rsettings" aria-controls="tab-config-rsettings" role="tab" data-toggle="tab"><?=$lang['admin']['rspamd_settings_map'];?></a></li>
+        <li role="presentation"><a href="#tab-config-password-policy" aria-controls="tab-config-password-policy" role="tab" data-toggle="tab"><?=$lang['admin']['password_policy'];?></a></li>
         <li role="presentation"><a href="#tab-config-customize" aria-controls="tab-config-customize" role="tab" data-toggle="tab"><?=$lang['admin']['customize'];?></a></li>
       </ul>
     </li>
@@ -77,7 +78,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
                   foreach ($tfa_data['additional'] as $key_info) {
                 ?>
                 <form style="display:inline;" method="post">
-                  <input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>" />
+                  <input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>">
                   <div style="padding:4px;margin:4px" class="label label-keys label-<?=($_SESSION['tfa_id'] == $key_info['id']) ? 'success' : 'default'; ?>">
                   <?=$key_info['key_id'];?>
                   <a href="#" style="font-weight:bold;color:white" onClick="$(this).closest('form').submit()">[<?=$lang['admin']['remove'];?>]</a>
@@ -128,7 +129,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
                   </td>
                   <td style="min-width:240px;text-align: right">
                     <form style="display:inline;" method="post">
-                    <input type="hidden" name="unset_fido2_key" value="<?=$key_info['cid'];?>" />
+                    <input type="hidden" name="unset_fido2_key" value="<?=$key_info['cid'];?>">
                     <div class="btn-group">
                     <a href="#" class="btn btn-xs btn-default" data-cid="<?=$key_info['cid'];?>" data-subject="<?=base64_encode($key_info['subject']);?>" data-toggle="modal" data-target="#fido2ChangeFn"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['fido2']['rename'];?></a>
                     <a href="#" onClick='return confirm("<?=$lang['admin']['ays'];?>")?$(this).closest("form").submit():"";' class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> <?=$lang['admin']['remove'];?></a>
@@ -381,7 +382,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
           </form>
           </div>
           <div class="col-sm-3">
-            <img class="img-responsive" src="/img/rspamd_logo.png" alt="Rspamd UI" />
+            <img class="img-responsive" src="/img/rspamd_logo.png" alt="Rspamd UI">
           </div>
         </div>
       </div>
@@ -507,7 +508,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
               ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.');
           ?>
             <div class="row collapse in dkim_key_valid">
-              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" /></div>
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>"></div>
               <div class="col-md-3">
                 <p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong>
                   <p class="dkim-label"><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
@@ -526,7 +527,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
           else {
           ?>
           <div class="row collapse in dkim_key_missing">
-            <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled /></div>
+            <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$domain;?>" disabled></div>
             <div class="col-md-3">
               <p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($domain);?></strong><br><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
             </div>
@@ -541,7 +542,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
               ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.');
             ?>
               <div class="row collapse in dkim_key_valid">
-              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" /></div>
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>"></div>
                 <div class="col-md-2 col-md-offset-1">
                   <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong></small>
                     <p class="dkim-label"><span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span></p>
@@ -560,7 +561,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
             else {
             ?>
             <div class="row collapse in dkim_key_missing">
-              <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" disabled /></div>
+              <div class="col-md-1"><input class="dkim_missing" type="checkbox" data-id="dkim" name="multi_select" value="<?=$alias_domain;?>" disabled></div>
               <div class="col-md-2 col-md-offset-1">
                 <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
               </div>
@@ -577,7 +578,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
             ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] === true) ?: $dkim['privkey'] = base64_encode('Please set $SHOW_DKIM_PRIV_KEYS to true to show DKIM private keys.');
           ?>
             <div class="row collapse in dkim_key_unused">
-              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$blind;?>" /></div>
+              <div class="col-md-1"><input type="checkbox" data-id="dkim" name="multi_select" value="<?=$blind;?>"></div>
               <div class="col-md-3">
                 <p><?=$lang['admin']['domain'];?>: <strong><?=htmlspecialchars($blind);?></strong>
                   <p class="dkim-label"><span class="label label-warning"><?=$lang['admin']['dkim_key_unused'];?></span></p>
@@ -1212,6 +1213,49 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
     </div>
   </div>
 
+  <div role="tabpanel" class="tab-pane" id="tab-config-password-policy">
+    <div class="panel panel-default">
+      <div class="panel-heading"><?=$lang['admin']['password_policy'];?></div>
+      <div class="panel-body">
+        <?php $password_complexity = password_complexity('get'); ?>
+        <form class="form-horizontal" data-id="passwordpolicy" role="form" method="post">
+          <?php
+          foreach ($password_complexity as $name => $value) {
+          if ($name == 'length') {
+          ?>
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="<?=$name;?>"><?=$lang['admin']['password_length'];?>:</label>
+            <div class="col-sm-2">
+              <input type="number" class="form-control" min="3" max="64" name="<?=$name;?>" id="<?=$name;?>" value="<?=$value;?>" required>
+            </div>
+          </div>
+          <?php
+          } else {
+          ?>
+          <input type="hidden" name="<?=$name;?>" value="0">
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <label>
+                <input type="checkbox" name="<?=$name;?>" id="<?=$name;?>" value="1" <?=($value == 1) ? 'checked' : null;?>> <?=$lang['admin']["password_policy_$name"];?>
+              </label>
+            </div>
+          </div>
+          <?php
+          }
+          }
+          ?>
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <div class="btn-group">
+                <button class="btn btn-sm btn-success" data-item="passwordpolicy" data-action="edit_selected" data-id="passwordpolicy" data-api-url='edit/passwordpolicy' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+              </div>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+
   <div role="tabpanel" class="tab-pane" id="tab-sys-mails">
     <div class="panel panel-default">
       <div class="panel-heading"><?=$lang['admin']['sys_mails'];?></div>

+ 22 - 46
data/web/inc/functions.admin.inc.php

@@ -48,40 +48,23 @@ function admin($_action, $_data = null) {
           return false;
         }
       }
-      if (!empty($password) && !empty($password2)) {
-        if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data_log),
-            'msg' => 'password_complexity'
-          );
-          return false;
-        }
-        if ($password != $password2) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data_log),
-            'msg' => 'password_mismatch'
-          );
-          return false;
-        }
-        $password_hashed = hash_password($password);
-        $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
-          VALUES (:username, :password_hashed, '1', :active)");
-        $stmt->execute(array(
-          ':username' => $username,
-          ':password_hashed' => $password_hashed,
-          ':active' => $active
-        ));
+      if (password_check($password_new, $password_new2) !== true) {
+        return false;
+      }
+      // support pre hashed passwords
+      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_new;
       }
       else {
-        $_SESSION['return'][] = array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data_log),
-          'msg' => 'password_empty'
-        );
-        return false;
+        $password_hashed = hash_password($password_new);
       }
+      $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
+        VALUES (:username, :password_hashed, '1', :active)");
+      $stmt->execute(array(
+        ':username' => $username,
+        ':password_hashed' => $password_hashed,
+        ':active' => $active
+      ));
       $_SESSION['return'][] = array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -145,23 +128,16 @@ function admin($_action, $_data = null) {
           }
         }
         if (!empty($password) && !empty($password2)) {
-          if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => 'password_complexity'
-            );
-            continue;
+          if (password_check($password, $password2) !== true) {
+            return false;
           }
-          if ($password != $password2) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => 'password_mismatch'
-            );
-            continue;
+          // support pre hashed passwords
+          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 {
+            $password_hashed = hash_password($password);
           }
-          $password_hashed = hash_password($password);
           $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
           $stmt->execute(array(
             ':password_hashed' => $password_hashed,

+ 24 - 24
data/web/inc/functions.dkim.inc.php

@@ -21,7 +21,7 @@ function dkim($_action, $_data = null, $privkey = false) {
         if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('dkim_domain_or_sel_invalid', $domain)
           );
           continue;
@@ -29,7 +29,7 @@ function dkim($_action, $_data = null, $privkey = false) {
         if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('dkim_domain_or_sel_invalid', $domain)
           );
           continue;
@@ -37,7 +37,7 @@ function dkim($_action, $_data = null, $privkey = false) {
         if (!ctype_alnum(str_replace(['-', '_'], '', $dkim_selector))) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('dkim_domain_or_sel_invalid', $domain)
           );
           continue;
@@ -62,7 +62,7 @@ function dkim($_action, $_data = null, $privkey = false) {
           catch (RedisException $e) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+              'log' => array(__FUNCTION__, $_action, $_data),
               'msg' => array('redis_error', $e)
             );
             continue;
@@ -76,7 +76,7 @@ function dkim($_action, $_data = null, $privkey = false) {
             catch (RedisException $e) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+                'log' => array(__FUNCTION__, $_action, $_data),
                 'msg' => array('redis_error', $e)
               );
               continue;
@@ -84,14 +84,14 @@ function dkim($_action, $_data = null, $privkey = false) {
           }
           $_SESSION['return'][] = array(
             'type' => 'success',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('dkim_added', $domain)
           );
         }
         else {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('dkim_domain_or_sel_invalid', $domain)
           );
           continue;
@@ -102,7 +102,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if ($_SESSION['mailcow_cc_role'] != "admin") {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => 'access_denied'
         );
         return false;
@@ -112,7 +112,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if (empty($from_domain_dkim)) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('dkim_domain_or_sel_invalid', $from_domain)
         );
         continue;
@@ -128,14 +128,14 @@ function dkim($_action, $_data = null, $privkey = false) {
         catch (RedisException $e) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('redis_error', $e)
           );
           continue;
         }
         $_SESSION['return'][] = array(
           'type' => 'success',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('dkim_duplicated', $from_domain, $to_domain)
         );
       }
@@ -144,7 +144,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if ($_SESSION['mailcow_cc_role'] != "admin") {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => 'access_denied'
         );
         return false;
@@ -156,7 +156,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if ($ssl_error = openssl_error_string()) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('private_key_error', $ssl_error)
         );
         return false;
@@ -173,7 +173,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if (!is_valid_domain_name($domain)) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('dkim_domain_or_sel_invalid', $domain)
         );
         return false;
@@ -182,7 +182,7 @@ function dkim($_action, $_data = null, $privkey = false) {
         if ($overwrite_existing == 0) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('dkim_domain_or_sel_exists', $domain)
           );
           return false;
@@ -191,7 +191,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if (!ctype_alnum($dkim_selector)) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('dkim_domain_or_sel_invalid', $domain)
         );
         return false;
@@ -205,7 +205,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       catch (RedisException $e) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('redis_error', $e)
         );
         return false;
@@ -218,14 +218,14 @@ function dkim($_action, $_data = null, $privkey = false) {
       catch (RedisException $e) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('redis_error', $e)
         );
         return false;
       }
       $_SESSION['return'][] = array(
         'type' => 'success',
-        'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+        'log' => array(__FUNCTION__, $_action, $_data),
         'msg' => array('dkim_added', $domain)
       );
       return true;
@@ -270,7 +270,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if ($_SESSION['mailcow_cc_role'] != "admin") {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => 'access_denied'
         );
         return false;
@@ -286,7 +286,7 @@ function dkim($_action, $_data = null, $privkey = false) {
       if ($_SESSION['mailcow_cc_role'] != "admin") {
         $_SESSION['return'][] = array(
           'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => 'access_denied'
         );
         return false;
@@ -295,7 +295,7 @@ function dkim($_action, $_data = null, $privkey = false) {
         if (!is_valid_domain_name($domain)) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('dkim_domain_or_sel_invalid', $domain)
           );
           continue;
@@ -309,14 +309,14 @@ function dkim($_action, $_data = null, $privkey = false) {
         catch (RedisException $e) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+            'log' => array(__FUNCTION__, $_action, $_data),
             'msg' => array('redis_error', $e)
           );
           continue;
         }
         $_SESSION['return'][] = array(
           'type' => 'success',
-          'log' => array(__FUNCTION__, $_action, $_data, $privkey),
+          'log' => array(__FUNCTION__, $_action, $_data),
           'msg' => array('dkim_removed', htmlspecialchars($domain))
         );
       }

+ 48 - 87
data/web/inc/functions.domain_admin.inc.php

@@ -65,61 +65,44 @@ function domain_admin($_action, $_data = null) {
           return false;
         }
       }
-      if (!empty($password) && !empty($password2)) {
-        if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
-          $_SESSION['return'][] = array(
-            'type' => 'danger',
-            'log' => array(__FUNCTION__, $_action, $_data_log),
-            'msg' => 'password_complexity'
-          );
-          return false;
-        }
-        if ($password != $password2) {
+      if (password_check($password, $password2) !== true) {
+        continue;
+      }
+      // support pre hashed passwords
+      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 {
+        $password_hashed = hash_password($password);
+      }
+      $valid_domains = 0;
+      foreach ($domains as $domain) {
+        if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
           $_SESSION['return'][] = array(
             'type' => 'danger',
             'log' => array(__FUNCTION__, $_action, $_data_log),
-            'msg' => 'password_mismatch'
+            'msg' => array('domain_invalid', htmlspecialchars($domain))
           );
-          return false;
-        }
-        $password_hashed = hash_password($password);
-        $valid_domains = 0;
-        foreach ($domains as $domain) {
-          if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => array('domain_invalid', htmlspecialchars($domain))
-            );
-            continue;
-          }
-          $valid_domains++;
-          $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
-              VALUES (:username, :domain, :created, :active)");
-          $stmt->execute(array(
-            ':username' => $username,
-            ':domain' => $domain,
-            ':created' => date('Y-m-d H:i:s'),
-            ':active' => $active
-          ));
-        }
-        if ($valid_domains != 0) {
-          $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
-            VALUES (:username, :password_hashed, '0', :active)");
-          $stmt->execute(array(
-            ':username' => $username,
-            ':password_hashed' => $password_hashed,
-            ':active' => $active
-          ));
+          continue;
         }
+        $valid_domains++;
+        $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
+            VALUES (:username, :domain, :created, :active)");
+        $stmt->execute(array(
+          ':username' => $username,
+          ':domain' => $domain,
+          ':created' => date('Y-m-d H:i:s'),
+          ':active' => $active
+        ));
       }
-      else {
-        $_SESSION['return'][] = array(
-          'type' => 'danger',
-          'log' => array(__FUNCTION__, $_action, $_data_log),
-          'msg' => 'password_empty'
-        );
-        return false;
+      if ($valid_domains != 0) {
+        $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
+          VALUES (:username, :password_hashed, '0', :active)");
+        $stmt->execute(array(
+          ':username' => $username,
+          ':password_hashed' => $password_hashed,
+          ':active' => $active
+        ));
       }
       $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
       $stmt->execute(array(
@@ -219,23 +202,16 @@ function domain_admin($_action, $_data = null) {
             }
           }
           if (!empty($password) && !empty($password2)) {
-            if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
-              $_SESSION['return'][] = array(
-                'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_data_log),
-                'msg' => 'password_complexity'
-              );
-              continue;
+            if (password_check($password, $password2) !== true) {
+              return false;
             }
-            if ($password != $password2) {
-              $_SESSION['return'][] = array(
-                'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_data_log),
-                'msg' => 'password_mismatch'
-              );
-              continue;
+            // support pre hashed passwords
+            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 {
+              $password_hashed = hash_password($password);
             }
-            $password_hashed = hash_password($password);
             $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
             $stmt->execute(array(
               ':password_hashed' => $password_hashed,
@@ -296,30 +272,15 @@ function domain_admin($_action, $_data = null) {
           );
           return false;
         }
-        if (!empty($password_new2) && !empty($password_new)) {
-          if ($password_new2 != $password_new) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => 'password_mismatch'
-            );
-            return false;
-          }
-          if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password_new)) {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_data_log),
-              'msg' => 'password_complexity'
-            );
-            return false;
-          }
-          $password_hashed = hash_password($password_new);
-          $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
-          $stmt->execute(array(
-            ':password_hashed' => $password_hashed,
-            ':username' => $username
-          ));
+        if (password_check($password_new, $password_new2) !== true) {
+          return false;
         }
+        $password_hashed = hash_password($password_new);
+        $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
+        $stmt->execute(array(
+          ':password_hashed' => $password_hashed,
+          ':username' => $username
+        ));
         $_SESSION['return'][] = array(
           'type' => 'success',
           'log' => array(__FUNCTION__, $_action, $_data_log),

+ 126 - 0
data/web/inc/functions.inc.php

@@ -114,6 +114,132 @@ function hash_password($password) {
   }
   return $pw_hash;
 }
+function password_complexity($_action, $_data = null) {
+	global $redis;
+	global $lang;
+  switch ($_action) {
+    case 'edit':
+      if ($_SESSION['mailcow_cc_role'] != "admin") {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data),
+          'msg' => 'access_denied'
+        );
+        return false;
+      }
+      $is_now = password_complexity('get');
+      if (!empty($is_now)) {
+        $length = (isset($_data['length']) && intval($_data['length']) >= 3) ? intval($_data['length']) : $is_now['length'];
+        $chars = (isset($_data['chars'])) ? intval($_data['chars']) : $is_now['chars'];
+        $lowerupper = (isset($_data['lowerupper'])) ? intval($_data['lowerupper']) : $is_now['lowerupper'];
+        $special_chars = (isset($_data['special_chars'])) ? intval($_data['special_chars']) : $is_now['special_chars'];
+        $numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
+      }
+      try {
+        $redis->hMSet('PASSWD_POLICY', [
+          'length' => $length,
+          'chars' => $chars,
+          'special_chars' => $special_chars,
+          'lowerupper' => $lowerupper,
+          'numbers' => $numbers
+        ]);
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data),
+          'msg' => array('redis_error', $e)
+        );
+        return false;
+      }
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_data),
+        'msg' => print_r($_data, true)
+      );
+    break;
+    case 'get':
+      try {
+        $length = $redis->hGet('PASSWD_POLICY', 'length');
+        $chars = $redis->hGet('PASSWD_POLICY', 'chars');
+        $special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars');
+        $lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper');
+        $numbers = $redis->hGet('PASSWD_POLICY', 'numbers');
+        return array(
+          'length' => $length,
+          'chars' => $chars,
+          'special_chars' => $special_chars,
+          'lowerupper' => $lowerupper,
+          'numbers' => $numbers
+        );
+      }
+      catch (RedisException $e) {
+        $_SESSION['return'][] = array(
+          'type' => 'danger',
+          'log' => array(__FUNCTION__, $_action, $_data),
+          'msg' => array('redis_error', $e)
+        );
+        return false;
+      }
+      return false;
+    break;
+    case 'html':
+      $policies = password_complexity('get');
+      foreach ($policies as $name => $value) {
+        if ($value != 0) {
+          $policy_text[] = sprintf($lang['admin']["password_policy_$name"], $value);
+        }
+      }
+      return '<p class="help-block small">- ' . implode('<br>- ', $policy_text) . '</p>';
+    break;
+  }
+}
+function password_check($password1, $password2) {
+  $password_complexity = password_complexity('get');
+
+  if (empty($password1) || empty($password2)) {
+    $_SESSION['return'][] = array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $_action, $_type),
+      'msg' => 'password_complexity'
+    );
+    return false;
+  }
+
+  if ($password1 != $password2) {
+    $_SESSION['return'][] = array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $_action, $_type),
+      'msg' => 'password_mismatch'
+    );
+    return false;
+  }
+
+  $given_password['length'] = strlen($password1);
+  $given_password['special_chars'] = preg_match('/[^a-zA-Z\d]/', $password1);
+  $given_password['chars'] = preg_match('/[a-zA-Z]/',$password1);
+  $given_password['numbers'] = preg_match('/\d/', $password1);
+  $lower = strlen(preg_replace("/[^a-z]/", '', $password1));
+  $upper = strlen(preg_replace("/[^A-Z]/", '', $password1));
+  $given_password['lowerupper'] = ($lower > 0 && $upper > 0) ? true : false;
+
+  if (
+    ($given_password['length'] < $password_complexity['length']) ||
+    ($password_complexity['special_chars'] == 1 && (intval($given_password['special_chars']) != $password_complexity['special_chars'])) ||
+    ($password_complexity['chars'] == 1 && (intval($given_password['chars']) != $password_complexity['chars'])) ||
+    ($password_complexity['numbers'] == 1 && (intval($given_password['numbers']) != $password_complexity['numbers'])) ||
+    ($password_complexity['lowerupper'] == 1 && (intval($given_password['lowerupper']) != $password_complexity['lowerupper']))
+  ) {
+    $_SESSION['return'][] = array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $_action, $_type),
+      'msg' => 'password_complexity'
+    );
+    return false;
+  }
+
+  return true;
+}
 function last_login($user) {
   global $pdo;
   $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`

+ 23 - 55
data/web/inc/functions.mailbox.inc.php

@@ -1045,32 +1045,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          if (!empty($password) && !empty($password2)) {
-            if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
-              $_SESSION['return'][] = array(
-                'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                'msg' => 'password_complexity'
-              );
-              return false;
-            }
-            if ($password != $password2) {
-              $_SESSION['return'][] = array(
-                'type' => 'danger',
-                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                'msg' => 'password_mismatch'
-              );
-              return false;
-            }
-            $password_hashed = hash_password($password);
+          if (password_check($password, $password2) !== true) {
+            return false;
+          }
+          // support pre hashed passwords
+          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 {
-            $_SESSION['return'][] = array(
-              'type' => 'danger',
-              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-              'msg' => 'password_empty'
-            );
-            return false;
+            $password_hashed = hash_password($password);
           }
           if ($MailboxData['count'] >= $DomainData['mailboxes']) {
             $_SESSION['return'][] = array(
@@ -2599,38 +2582,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 ));
               }
             }
-            if (!empty($password) && !empty($password2)) {
-              if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
-                $_SESSION['return'][] = array(
-                  'type' => 'danger',
-                  'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                  'msg' => 'password_complexity'
-                );
-                continue;
-              }
-              if ($password != $password2) {
-                $_SESSION['return'][] = array(
-                  'type' => 'danger',
-                  'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                  'msg' => 'password_mismatch'
-                );
-                continue;
-              }
-              // support pre hashed passwords
-              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 {
-                $password_hashed = hash_password($password);
-              }
-              $stmt = $pdo->prepare("UPDATE `mailbox` SET
-                  `password` = :password_hashed
-                    WHERE `username` = :username");
-              $stmt->execute(array(
-                ':password_hashed' => $password_hashed,
-                ':username' => $username
-              ));
+            if (password_check($password, $password2) !== true) {
+              continue;
+            }
+            // support pre hashed passwords
+            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 {
+              $password_hashed = hash_password($password);
             }
+            $stmt = $pdo->prepare("UPDATE `mailbox` SET
+                `password` = :password_hashed
+                  WHERE `username` = :username");
+            $stmt->execute(array(
+              ':password_hashed' => $password_hashed,
+              ':username' => $username
+            ));
             // We could either set alias = 1 if alias = 2 or tune the Postfix alias table (that's what we did, TODO: to it the other way)
             $stmt = $pdo->prepare("UPDATE `alias` SET
                 `active` = :active

+ 0 - 8
data/web/inc/vars.inc.php

@@ -90,14 +90,6 @@ $AVAILABLE_LANGUAGES = array('ca', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu
 // WARNING: Only lumen is loaded locally. Enabling any other theme, will download external sources.
 $DEFAULT_THEME = 'lumen';
 
-// Password complexity as regular expression
-// Min. 6 characters
-$PASSWD_REGEP = '.{6,}';
-// Min. 6 characters, which must include at least one uppercase letter, one lowercase letter and one number
-// $PASSWD_REGEP = '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{6,}$';
-// Min. 6 characters, which must include at least one letter and one number
-// $PASSWD_REGEP = '^(?=.*[0-9])(?=.*[A-Za-z]).{6,}$';
-
 // Show DKIM private keys - false by default
 $SHOW_DKIM_PRIV_KEYS = false;
 

+ 8 - 1
data/web/js/build/014-mailcow.js

@@ -95,7 +95,14 @@ $(document).ready(function() {
   // selectpicker
   $('select').selectpicker();
 
-  // haveibeenpwned?
+  // haveibeenpwned and passwd policy
+  $.ajax({
+    url: '/api/v1/get/passwordpolicy/html',
+    type: 'GET',
+    success: function(res) {
+      $(".hibp-out").after(res);
+    }
+  });
   $('[data-hibp]').after('<p class="small haveibeenpwned">↪ Check against haveibeenpwned.com</p><span class="hibp-out"></span>');
   $('[data-hibp]').on('input', function() {
     out_field = $(this).next('.haveibeenpwned').next('.hibp-out').text('').attr('class', 'hibp-out');

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 694 - 682
data/web/js/site/pwgen.js


+ 17 - 0
data/web/json_api.php

@@ -446,6 +446,20 @@ if (isset($_GET['query'])) {
             }
           break;
 
+          case "passwordpolicy":
+            switch ($object) {
+              case "html":
+                $password_complexity_rules = password_complexity('html');
+                if ($password_complexity_rules !== false) {
+                  process_get_return($password_complexity_rules);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+            }
+          break;
+
           case "app-passwd":
             switch ($object) {
               case "all":
@@ -1561,6 +1575,9 @@ if (isset($_GET['query'])) {
         case "app_links":
           process_edit_return(customize('edit', 'app_links', $attr));
         break;
+        case "passwordpolicy":
+          process_edit_return(password_complexity('edit', $attr));
+        break;
         case "relayhost":
           process_edit_return(relayhost('edit', array_merge(array('id' => $items), $attr)));
         break;

+ 8 - 1
data/web/lang/lang.de.json

@@ -234,6 +234,13 @@
         "oauth2_revoke_tokens": "Alle Client Tokens entfernen",
         "optional": "Optional",
         "password": "Passwort",
+        "password_length": "Passwortlänge",
+        "password_policy": "Passwortrichtlinie",
+        "password_policy_chars": "Muss ein alphabetisches Zeichen enthalten",
+        "password_policy_length": "Mindestlänge des Passwortes ist %d Zeichen",
+        "password_policy_numbers": "Muss eine Ziffer enthalten",
+        "password_policy_lowerupper": "Muss Großbuchstaben und Kleinbuchstaben enthalten",
+        "password_policy_special_chars": "Muss Sonderzeichen enthalten",
         "password_repeat": "Passwort wiederholen",
         "priority": "Gewichtung",
         "private_key": "Private Key",
@@ -907,6 +914,7 @@
         "object_modified": "Änderungen an Objekt %s wurden gespeichert",
         "pushover_settings_edited": "Pushover Konfiguration gespeichert, bitte den Zugang im Anschluss verifizieren.",
         "qlearn_spam": "Nachricht ID %s wurde als Spam gelernt und gelöscht",
+        "password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert",
         "queue_command_success": "Queue-Aufgabe erfolgreich ausgeführt",
         "recipient_map_entry_deleted": "Empfängerumschreibung mit der ID %s wurde gelöscht",
         "recipient_map_entry_saved": "Empfängerumschreibung für Objekt \"%s\" wurde gespeichert",
@@ -1027,7 +1035,6 @@
         "messages": "Nachrichten",
         "never": "Niemals",
         "new_password": "Neues Passwort",
-        "new_password_description": "Mindestanforderung: 6 Zeichen lang, Buchstaben und Zahlen.",
         "new_password_repeat": "Neues Passwort (Wiederholung)",
         "no_active_filter": "Kein aktiver Filter vorhanden",
         "no_last_login": "Keine letzte UI-Anmeldung gespeichert",

+ 8 - 1
data/web/lang/lang.en.json

@@ -232,6 +232,13 @@
         "oauth2_revoke_tokens": "Revoke all client tokens",
         "optional": "optional",
         "password": "Password",
+        "password_length": "Password length",
+        "password_policy": "Password policy",
+        "password_policy_chars": "Must contain at least one alphabetic character",
+        "password_policy_length": "Minimum password length is %d",
+        "password_policy_numbers": "Must contain at least one number",
+        "password_policy_lowerupper": "Must contain lowercase and uppercase characters",
+        "password_policy_special_chars": "Must contain special chars",
         "password_repeat": "Confirmation password (repeat)",
         "priority": "Priority",
         "private_key": "Private key",
@@ -906,6 +913,7 @@
         "pushover_settings_edited": "Pushover settings successfully set, please verify credentials.",
         "qlearn_spam": "Message ID %s was learned as spam and deleted",
         "queue_command_success": "Queue command completed successfully",
+        "password_policy_saved": "Password policy was saved successfully",
         "recipient_map_entry_deleted": "Recipient map ID %s has been deleted",
         "recipient_map_entry_saved": "Recipient map entry \"%s\" has been saved",
         "relayhost_added": "Map entry %s has been added",
@@ -1025,7 +1033,6 @@
         "messages": "messages",
         "never": "Never",
         "new_password": "New password",
-        "new_password_description": "Requirement: 6 characters long, letters and numbers.",
         "new_password_repeat": "Confirmation password (repeat)",
         "no_active_filter": "No active filter available",
         "no_last_login": "No last UI login information",

Vissa filer visades inte eftersom för många filer har ändrats