git subrepo commit mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "308860af"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "3f1a5af8"
git-subrepo: version:  "0.4.5"
  origin:   "???"
  commit:   "???"
Change-Id: I5d51c14b45db54fe706be40a591ddbfcea50d4b0
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/functions.inc.php b/mailcow/src/mailcow-dockerized/data/web/inc/functions.inc.php
index 7ac0af5..3bab56b 100644
--- a/mailcow/src/mailcow-dockerized/data/web/inc/functions.inc.php
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/functions.inc.php
@@ -251,7 +251,7 @@
 

   return true;

 }

-function last_login($action, $username, $sasl_limit_days = 7) {

+function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {

   global $pdo;

   global $redis;

   $sasl_limit_days = intval($sasl_limit_days);

@@ -319,8 +319,11 @@
         $stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`

           WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"

             AND JSON_EXTRACT(`call`, "$[1]") = :username

-            AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET 1');

-        $stmt->execute(array(':username' => $username));

+            AND `type` = "success" ORDER BY `time` DESC LIMIT 1 OFFSET :offset');

+        $stmt->execute(array(

+          ':username' => $username,

+          ':offset' => $ui_offset

+        ));

         $ui = $stmt->fetch(PDO::FETCH_ASSOC);

       }

       else {

@@ -830,11 +833,15 @@
   $stmt->execute(array(':user' => $user));

   $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

   foreach ($rows as $row) {

+    // verify password

     if (verify_hash($row['password'], $pass)) {

-      if (get_tfa($user)['name'] != "none") {

+      // check for tfa authenticators

+      $authenticators = get_tfa($user);

+      if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {

+        // active tfa authenticators found, set pending user login

         $_SESSION['pending_mailcow_cc_username'] = $user;

         $_SESSION['pending_mailcow_cc_role'] = "admin";

-        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];

+        $_SESSION['pending_tfa_methods'] = $authenticators['additional'];

         unset($_SESSION['ldelay']);

         $_SESSION['return'][] =  array(

           'type' => 'info',

@@ -842,8 +849,7 @@
           'msg' => 'awaiting_tfa_confirmation'

         );

         return "pending";

-      }

-      else {

+      } else {

         unset($_SESSION['ldelay']);

         // Reactivate TFA if it was set to "deactivate TFA for next login"

         $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");

@@ -866,11 +872,14 @@
   $stmt->execute(array(':user' => $user));

   $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

   foreach ($rows as $row) {

+    // verify password

     if (verify_hash($row['password'], $pass) !== false) {

-      if (get_tfa($user)['name'] != "none") {

+      // check for tfa authenticators

+      $authenticators = get_tfa($user);

+      if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {

         $_SESSION['pending_mailcow_cc_username'] = $user;

         $_SESSION['pending_mailcow_cc_role'] = "domainadmin";

-        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];

+        $_SESSION['pending_tfa_methods'] = $authenticators['additional'];

         unset($_SESSION['ldelay']);

         $_SESSION['return'][] =  array(

           'type' => 'info',

@@ -929,15 +938,37 @@
     $stmt->execute(array(':user' => $user));

     $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));

   }

-  foreach ($rows as $row) {

+  foreach ($rows as $row) { 

+    // verify password

     if (verify_hash($row['password'], $pass) !== false) {

-      unset($_SESSION['ldelay']);

-      $_SESSION['return'][] =  array(

-        'type' => 'success',

-        'log' => array(__FUNCTION__, $user, '*'),

-        'msg' => array('logged_in_as', $user)

-      );

-      if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {

+      if (!array_key_exists("app_passwd_id", $row)){ 

+        // password is not a app password

+        // check for tfa authenticators

+        $authenticators = get_tfa($user);

+        if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 &&

+            $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) {

+          // authenticators found, init TFA flow

+          $_SESSION['pending_mailcow_cc_username'] = $user;

+          $_SESSION['pending_mailcow_cc_role'] = "user";

+          $_SESSION['pending_tfa_methods'] = $authenticators['additional'];

+          unset($_SESSION['ldelay']);

+          $_SESSION['return'][] =  array(

+            'type' => 'success',

+            'log' => array(__FUNCTION__, $user, '*'),

+            'msg' => array('logged_in_as', $user)

+          );

+          return "pending";

+        } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {

+          // no authenticators found, login successfull

+          // Reactivate TFA if it was set to "deactivate TFA for next login"

+          $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");

+          $stmt->execute(array(':user' => $user));

+

+          unset($_SESSION['ldelay']);

+          return "user";

+        }

+      } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {

+        // password is a app password

         $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';

         $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");

         $stmt->execute(array(

@@ -946,8 +977,10 @@
           ':username' => $user,

           ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])

         ));

+

+        unset($_SESSION['ldelay']);

+        return "user";

       }

-      return "user";

     }

   }

 

@@ -1140,49 +1173,49 @@
 function set_tfa($_data) {

   global $pdo;

   global $yubi;

-  global $u2f;

   global $tfa;

   $_data_log = $_data;

+  $access_denied = null;

   !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';

   $username = $_SESSION['mailcow_cc_username'];

-  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {

-      $_SESSION['return'][] =  array(

-        'type' => 'danger',

-        'log' => array(__FUNCTION__, $_data_log),

-        'msg' => 'access_denied'

-      );

-      return false;

-  }

-  $stmt = $pdo->prepare("SELECT `password` FROM `admin`

-      WHERE `username` = :username");

-  $stmt->execute(array(':username' => $username));

-  $row = $stmt->fetch(PDO::FETCH_ASSOC);

-  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));

-  if (!empty($num_results)) {

-    if (!verify_hash($row['password'], $_data["confirm_password"])) {

-      $_SESSION['return'][] =  array(

-        'type' => 'danger',

-        'log' => array(__FUNCTION__, $_data_log),

-        'msg' => 'access_denied'

-      );

-      return false;

+

+  // check for empty user and role

+  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;

+

+  // check admin confirm password

+  if ($access_denied === null) {

+    $stmt = $pdo->prepare("SELECT `password` FROM `admin`

+        WHERE `username` = :username");

+    $stmt->execute(array(':username' => $username));

+    $row = $stmt->fetch(PDO::FETCH_ASSOC);

+    if ($row) {

+      if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;

+      else $access_denied = false;

     }

   }

-  $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`

-      WHERE `username` = :username");

-  $stmt->execute(array(':username' => $username));

-  $row = $stmt->fetch(PDO::FETCH_ASSOC);

-  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));

-  if (!empty($num_results)) {

-    if (!verify_hash($row['password'], $_data["confirm_password"])) {

-      $_SESSION['return'][] =  array(

-        'type' => 'danger',

-        'log' => array(__FUNCTION__, $_data_log),

-        'msg' => 'access_denied'

-      );

-      return false;

+

+  // check mailbox confirm password

+  if ($access_denied === null) {

+    $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`

+        WHERE `username` = :username");

+    $stmt->execute(array(':username' => $username));

+    $row = $stmt->fetch(PDO::FETCH_ASSOC);

+    if ($row) {

+      if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;

+      else $access_denied = false;

     }

   }

+

+  // set access_denied error

+  if ($access_denied){

+    $_SESSION['return'][] =  array(

+      'type' => 'danger',

+      'log' => array(__FUNCTION__, $_data_log),

+      'msg' => 'access_denied'

+    );

+    return false;

+  }

+

   switch ($_data["tfa_method"]) {

     case "yubi_otp":

       $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];

@@ -1219,8 +1252,7 @@
         $yubico_modhex_id = substr($_data["otp_token"], 0, 12);

         $stmt = $pdo->prepare("DELETE FROM `tfa`

           WHERE `username` = :username

-            AND (`authmech` != 'yubi_otp')

-            OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");

+            AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");

         $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));

         $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES

           (:key_id, :username, 'yubi_otp', '1', :secret)");

@@ -1240,31 +1272,6 @@
         'msg' => array('object_modified', htmlspecialchars($username))

       );

     break;

-    case "u2f":

-      $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];

-      try {

-        $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_data['token']));

-        $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'u2f'");

-        $stmt->execute(array(':username' => $username));

-        $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'u2f', ?, ?, ?, ?, '1')");

-        $stmt->execute(array($username, $key_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));

-        $_SESSION['return'][] =  array(

-          'type' => 'success',

-          'log' => array(__FUNCTION__, $_data_log),

-          'msg' => array('object_modified', $username)

-        );

-        $_SESSION['regReq'] = null;

-      }

-      catch (Exception $e) {

-        $_SESSION['return'][] =  array(

-          'type' => 'danger',

-          'log' => array(__FUNCTION__, $_data_log),

-          'msg' => array('u2f_verification_failed', $e->getMessage())

-        );

-        $_SESSION['regReq'] = null;

-        return false;

-      }

-    break;

     case "totp":

       $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];

       if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {

@@ -1286,6 +1293,26 @@
         );

       }

     break;

+    case "webauthn":

+        $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];

+

+        $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)

+        VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");

+        $stmt->execute(array(

+            $username,

+            $key_id,

+            base64_encode($_data['registration']->credentialId),

+            $_data['registration']->credentialPublicKey,

+            $_data['registration']->certificate,

+            0

+        ));

+    

+        $_SESSION['return'][] =  array(

+            'type' => 'success',

+            'log' => array(__FUNCTION__, $_data_log),

+            'msg' => array('object_modified', $username)

+        );

+    break;

     case "none":

       $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");

       $stmt->execute(array(':username' => $username));

@@ -1360,8 +1387,8 @@
       if (!isset($_data['cid']) || empty($_data['cid'])) {

         return false;

       }

-      $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE TO_BASE64(`credentialId`) = :cid");

-      $stmt->execute(array(':cid' => $_data['cid']));

+      $stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE `credentialId` = :cid");

+      $stmt->execute(array(':cid' => base64_decode($_data['cid'])));

       $row = $stmt->fetch(PDO::FETCH_ASSOC);

       if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {

         return false;

@@ -1440,25 +1467,27 @@
   global $pdo;

   global $lang;

   $_data_log = $_data;

+  $access_denied = null;

   $id = intval($_data['unset_tfa_key']);

   $username = $_SESSION['mailcow_cc_username'];

-  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {

-    $_SESSION['return'][] =  array(

-      'type' => 'danger',

-      'log' => array(__FUNCTION__, $_data_log),

-      'msg' => 'access_denied'

-    );

-    return false;

-  }

+

+  // check for empty user and role

+  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;

+

   try {

-    if (!is_numeric($id)) {

-      $_SESSION['return'][] =  array(

+    if (!is_numeric($id)) $access_denied = true;

+    

+    // set access_denied error

+    if ($access_denied){

+      $_SESSION['return'][] = array(

         'type' => 'danger',

         'log' => array(__FUNCTION__, $_data_log),

         'msg' => 'access_denied'

       );

       return false;

-    }

+    } 

+

+    // check if it's last key

     $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`

       WHERE `username` = :username AND `active` = '1'");

     $stmt->execute(array(':username' => $username));

@@ -1471,6 +1500,8 @@
       );

       return false;

     }

+

+    // delete key

     $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");

     $stmt->execute(array(':username' => $username, ':id' => $id));

     $_SESSION['return'][] =  array(

@@ -1488,7 +1519,7 @@
     return false;

   }

 }

-function get_tfa($username = null) {

+function get_tfa($username = null, $id = null) {

   global $pdo;

   if (isset($_SESSION['mailcow_cc_username'])) {

     $username = $_SESSION['mailcow_cc_username'];

@@ -1496,204 +1527,311 @@
   elseif (empty($username)) {

     return false;

   }

-  $stmt = $pdo->prepare("SELECT * FROM `tfa`

-      WHERE `username` = :username AND `active` = '1'");

-  $stmt->execute(array(':username' => $username));

-  $row = $stmt->fetch(PDO::FETCH_ASSOC);

 

-  if (isset($row["authmech"])) {

-    switch ($row["authmech"]) {

-      case "yubi_otp":

-        $data['name'] = "yubi_otp";

-        $data['pretty'] = "Yubico OTP";

-        $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");

-        $stmt->execute(array(

-          ':username' => $username,

-        ));

-        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

-        while($row = array_shift($rows)) {

-          $data['additional'][] = $row;

+  if (!isset($id)){

+    // fetch all tfa methods - just get information about possible authenticators

+    $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`

+        WHERE `username` = :username AND `active` = '1'");

+    $stmt->execute(array(':username' => $username));

+    $results = $stmt->fetchAll(PDO::FETCH_ASSOC);

+ 

+    // no tfa methods found

+    if (count($results) == 0) {

+        $data['name'] = 'none';

+        $data['pretty'] = "-";

+        $data['additional'] = array();

+        return $data;

+    }

+

+    $data['additional'] = $results;

+    return $data;

+  } else {

+    // fetch specific authenticator details by id

+    $stmt = $pdo->prepare("SELECT * FROM `tfa`

+    WHERE `username` = :username AND `id` = :id AND `active` = '1'");

+    $stmt->execute(array(':username' => $username, ':id' => $id));

+    $row = $stmt->fetch(PDO::FETCH_ASSOC);

+

+    if (isset($row["authmech"])) {

+        switch ($row["authmech"]) {

+          case "yubi_otp":

+            $data['name'] = "yubi_otp";

+            $data['pretty'] = "Yubico OTP";

+            $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id");

+            $stmt->execute(array(

+              ':username' => $username,

+              ':id' => $id

+            ));

+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

+            while($row = array_shift($rows)) {

+              $data['additional'][] = $row;

+            }

+            return $data;

+          break;

+          // u2f - deprecated, should be removed

+          case "u2f":

+            $data['name'] = "u2f";

+            $data['pretty'] = "Fido U2F";

+            $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id");

+            $stmt->execute(array(

+              ':username' => $username,

+              ':id' => $id

+            ));

+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

+            while($row = array_shift($rows)) {

+              $data['additional'][] = $row;

+            }

+            return $data;

+          break;

+          case "hotp":

+            $data['name'] = "hotp";

+            $data['pretty'] = "HMAC-based OTP";

+            return $data;

+          break;

+          case "totp":

+            $data['name'] = "totp";

+            $data['pretty'] = "Time-based OTP";

+            $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id");

+            $stmt->execute(array(

+              ':username' => $username,

+              ':id' => $id

+            ));

+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

+            while($row = array_shift($rows)) {

+              $data['additional'][] = $row;

+            }

+            return $data;

+          break;

+          case "webauthn":

+            $data['name'] = "webauthn";

+            $data['pretty'] = "WebAuthn";

+            $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id");

+            $stmt->execute(array(

+              ':username' => $username,

+              ':id' => $id

+            ));

+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

+            while($row = array_shift($rows)) {

+              $data['additional'][] = $row;

+            }

+            return $data;

+          break;

+          default:

+            $data['name'] = 'none';

+            $data['pretty'] = "-";

+            return $data;

+          break;

         }

-        return $data;

-      break;

-      case "u2f":

-        $data['name'] = "u2f";

-        $data['pretty'] = "Fido U2F";

-        $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");

-        $stmt->execute(array(

-          ':username' => $username,

-        ));

-        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

-        while($row = array_shift($rows)) {

-          $data['additional'][] = $row;

-        }

-        return $data;

-      break;

-      case "hotp":

-        $data['name'] = "hotp";

-        $data['pretty'] = "HMAC-based OTP";

-        return $data;

-      break;

-       case "totp":

-        $data['name'] = "totp";

-        $data['pretty'] = "Time-based OTP";

-        $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username");

-        $stmt->execute(array(

-          ':username' => $username,

-        ));

-        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

-        while($row = array_shift($rows)) {

-          $data['additional'][] = $row;

-        }

-        return $data;

-        break;

-      default:

+      }

+      else {

         $data['name'] = 'none';

         $data['pretty'] = "-";

         return $data;

-      break;

+      }

     }

-  }

-  else {

-    $data['name'] = 'none';

-    $data['pretty'] = "-";

-    return $data;

-  }

 }

-function verify_tfa_login($username, $token) {

+function verify_tfa_login($username, $_data) {

   global $pdo;

   global $yubi;

   global $u2f;

   global $tfa;

-  $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`

-      WHERE `username` = :username AND `active` = '1'");

-  $stmt->execute(array(':username' => $username));

-  $row = $stmt->fetch(PDO::FETCH_ASSOC);

+  global $WebAuthn;

 

-  switch ($row["authmech"]) {

-    case "yubi_otp":

-      if (!ctype_alnum($token) || strlen($token) != 44) {

-        $_SESSION['return'][] =  array(

-          'type' => 'danger',

-          'log' => array(__FUNCTION__, $username, '*'),

-          'msg' => array('yotp_verification_failed', 'token length error')

-        );

-        return false;

-      }

-      $yubico_modhex_id = substr($token, 0, 12);

-      $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`

-          WHERE `username` = :username

-          AND `authmech` = 'yubi_otp'

-          AND `active`='1'

-          AND `secret` LIKE :modhex");

-      $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));

-      $row = $stmt->fetch(PDO::FETCH_ASSOC);

-      $yubico_auth = explode(':', $row['secret']);

-      $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);

-      $yauth = $yubi->verify($token);

-      if (PEAR::isError($yauth)) {

-        $_SESSION['return'][] =  array(

-          'type' => 'danger',

-          'log' => array(__FUNCTION__, $username, '*'),

-          'msg' => array('yotp_verification_failed', $yauth->getMessage())

-        );

-        return false;

-      }

-      else {

-        $_SESSION['tfa_id'] = $row['id'];

-        $_SESSION['return'][] =  array(

-          'type' => 'success',

-          'log' => array(__FUNCTION__, $username, '*'),

-          'msg' => 'verified_yotp_login'

-        );

-        return true;

-      }

-      $_SESSION['return'][] =  array(

-        'type' => 'danger',

-        'log' => array(__FUNCTION__, $username, '*'),

-        'msg' => array('yotp_verification_failed', 'unknown')

-      );

-    return false;

-  break;

-  case "u2f":

-    try {

-      $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token));

-      $stmt = $pdo->prepare("SELECT `id` FROM `tfa` WHERE `keyHandle` = ?");

-      $stmt->execute(array($reg->keyHandle));

-      $row_key_id = $stmt->fetch(PDO::FETCH_ASSOC);

-      $_SESSION['tfa_id'] = $row_key_id['id'];

-      $_SESSION['authReq'] = null;

-      $_SESSION['return'][] =  array(

-        'type' => 'success',

-        'log' => array(__FUNCTION__, $username, '*'),

-        'msg' => 'verified_u2f_login'

-      );

-      return true;

-    }

-    catch (Exception $e) {

-      $_SESSION['return'][] =  array(

-        'type' => 'danger',

-        'log' => array(__FUNCTION__, $username, '*'),

-        'msg' => array('u2f_verification_failed', $e->getMessage())

-      );

-      $_SESSION['regReq'] = null;

-      return false;

-    }

-    $_SESSION['return'][] =  array(

-      'type' => 'danger',

-      'log' => array(__FUNCTION__, $username, '*'),

-      'msg' => array('u2f_verification_failed', 'unknown')

-    );

-    return false;

-  break;

-  case "hotp":

-      return false;

-  break;

-  case "totp":

-    try {

-      $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`

-          WHERE `username` = :username

-          AND `authmech` = 'totp'

-          AND `active`='1'");

-      $stmt->execute(array(':username' => $username));

-      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

-      foreach ($rows as $row) {

-        if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) {

-          $_SESSION['tfa_id'] = $row['id'];

-          $_SESSION['return'][] =  array(

-            'type' => 'success',

+  if ($_data['tfa_method'] != 'u2f'){

+

+    switch ($_data["tfa_method"]) {

+        case "yubi_otp":

+            if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {

+                $_SESSION['return'][] =  array(

+                    'type' => 'danger',

+                    'log' => array(__FUNCTION__, $username, '*'),

+                    'msg' => array('yotp_verification_failed', 'token length error')

+                );

+                return false;

+            }

+            $yubico_modhex_id = substr($_data['token'], 0, 12);

+            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`

+                WHERE `username` = :username

+                AND `authmech` = 'yubi_otp'

+                AND `active` = '1'

+                AND `secret` LIKE :modhex");

+            $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));

+            $row = $stmt->fetch(PDO::FETCH_ASSOC);

+            $yubico_auth = explode(':', $row['secret']);

+            $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);

+            $yauth = $yubi->verify($_data['token']);

+            if (PEAR::isError($yauth)) {

+                $_SESSION['return'][] =  array(

+                    'type' => 'danger',

+                    'log' => array(__FUNCTION__, $username, '*'),

+                    'msg' => array('yotp_verification_failed', $yauth->getMessage())

+                );

+                return false;

+            }

+            else {

+                $_SESSION['tfa_id'] = $row['id'];

+                $_SESSION['return'][] =  array(

+                    'type' => 'success',

+                    'log' => array(__FUNCTION__, $username, '*'),

+                    'msg' => 'verified_yotp_login'

+                );

+                return true;

+            }

+            $_SESSION['return'][] =  array(

+                'type' => 'danger',

+                'log' => array(__FUNCTION__, $username, '*'),

+                'msg' => array('yotp_verification_failed', 'unknown')

+            );

+            return false;

+        break;

+        case "hotp":

+            return false;

+        break;

+        case "totp":

+          try {

+            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`

+                WHERE `username` = :username

+                AND `authmech` = 'totp'

+                AND `id` = :id

+                AND `active`='1'");

+            $stmt->execute(array(':username' => $username, ':id' => $_data['id']));

+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

+            foreach ($rows as $row) {

+              if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {

+                $_SESSION['tfa_id'] = $row['id'];

+                $_SESSION['return'][] =  array(

+                    'type' => 'success',

+                    'log' => array(__FUNCTION__, $username, '*'),

+                    'msg' => 'verified_totp_login'

+                );

+                return true;

+              }

+            }

+            $_SESSION['return'][] =  array(

+                'type' => 'danger',

+                'log' => array(__FUNCTION__, $username, '*'),

+                'msg' => 'totp_verification_failed'

+            );

+            return false;

+          }

+          catch (PDOException $e) {

+            $_SESSION['return'][] =  array(

+                'type' => 'danger',

+                'log' => array(__FUNCTION__, $username, '*'),

+                'msg' => array('mysql_error', $e)

+            );

+            return false;

+          }

+        break;

+        case "webauthn":

+            $tokenData = json_decode($_data['token']);

+            $clientDataJSON = base64_decode($tokenData->clientDataJSON);

+            $authenticatorData = base64_decode($tokenData->authenticatorData);

+            $signature = base64_decode($tokenData->signature);

+            $id = base64_decode($tokenData->id);

+            $challenge = $_SESSION['challenge'];

+

+            $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");

+            $stmt->execute(array(':id' => $_data['id']));

+            $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);

+

+            if (empty($process_webauthn)){

+              $_SESSION['return'][] =  array(

+                  'type' => 'danger',

+                  'log' => array(__FUNCTION__, $username, '*'),

+                  'msg' => array('webauthn_verification_failed', 'authenticator not found')

+              );

+              return false;

+            } 

+            

+            if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {

+                $_SESSION['return'][] =  array(

+                    'type' => 'danger',

+                    'log' => array(__FUNCTION__, $username, '*'),

+                    'msg' => array('webauthn_verification_failed', 'publicKey not found')

+                );

+                return false;

+            }

+

+            try {

+                $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);

+            }

+            catch (Throwable $ex) {

+                $_SESSION['return'][] =  array(

+                    'type' => 'danger',

+                    'log' => array(__FUNCTION__, $username, '*'),

+                    'msg' => array('webauthn_verification_failed', $ex->getMessage())

+                );

+                return false;

+            }

+

+            $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");

+            $stmt->execute(array(':username' => $process_webauthn['username']));

+            $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);

+            if ($obj_props['superadmin'] === 1) {

+              $_SESSION["mailcow_cc_role"] = "admin";

+            }

+            elseif ($obj_props['superadmin'] === 0) {

+              $_SESSION["mailcow_cc_role"] = "domainadmin";

+            }

+            else {

+              $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");

+              $stmt->execute(array(':username' => $process_webauthn['username']));

+              $row = $stmt->fetch(PDO::FETCH_ASSOC);

+              if (!empty($row['username'])) {

+                $_SESSION["mailcow_cc_role"] = "user";

+              } else {

+                $_SESSION['return'][] =  array(

+                  'type' => 'danger',

+                  'log' => array(__FUNCTION__, $username, '*'),

+                  'msg' => array('webauthn_verification_failed', 'could not determine user role')

+                );

+                return false;

+              }

+            }

+

+            if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){

+                $_SESSION['return'][] =  array(

+                    'type' => 'danger',

+                    'log' => array(__FUNCTION__, $username, '*'),

+                    'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')

+                );

+                return false;

+            }

+

+            $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];

+            $_SESSION['tfa_id'] = $process_webauthn['id'];

+            $_SESSION['authReq'] = null;

+            unset($_SESSION["challenge"]);

+            $_SESSION['return'][] =  array(

+                'type' => 'success',

+                'log' => array("webauthn_login"),

+                'msg' => array('logged_in_as', $process_webauthn['username'])

+            );

+            return true;

+        break;

+        default:

+            $_SESSION['return'][] =  array(

+            'type' => 'danger',

             'log' => array(__FUNCTION__, $username, '*'),

-            'msg' => 'verified_totp_login'

-          );

-          return true;

-        }

-      }

-      $_SESSION['return'][] =  array(

-        'type' => 'danger',

-        'log' => array(__FUNCTION__, $username, '*'),

-        'msg' => 'totp_verification_failed'

-      );

-      return false;

+            'msg' => 'unknown_tfa_method'

+            );

+            return false;

+        break;

     }

-    catch (PDOException $e) {

-      $_SESSION['return'][] =  array(

-        'type' => 'danger',

-        'log' => array(__FUNCTION__, $username, '*'),

-        'msg' => array('mysql_error', $e)

-      );

-      return false;

-    }

-  break;

-  default:

-    $_SESSION['return'][] =  array(

-      'type' => 'danger',

-      'log' => array(__FUNCTION__, $username, '*'),

-      'msg' => 'unknown_tfa_method'

-    );

+

     return false;

-  break;

+  } else {

+    // delete old keys that used u2f

+    $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");

+    $stmt->execute(array(':username' => $username));

+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

+    if (count($rows) == 0) return false;

+

+    $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");

+    $stmt->execute(array(':username' => $username));

+    return true;

   }

-  return false;

 }

 function admin_api($access, $action, $data = null) {

   global $pdo;

@@ -1955,12 +2093,7 @@
     break;

   }

 }

-function get_u2f_registrations($username) {

-  global $pdo;

-  $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");

-  $sel->execute(array($username));

-  return $sel->fetchAll(PDO::FETCH_OBJ);

-}

+

 function get_logs($application, $lines = false) {

   if ($lines === false) {

     $lines = $GLOBALS['LOG_LINES'] - 1;