From ea1a02bd7d0bbc84291cf3a647fa13bc5b599522 Mon Sep 17 00:00:00 2001 From: El-Virus <36414402+El-Virus@users.noreply.github.com> Date: Sun, 26 Dec 2021 17:11:06 +0100 Subject: [PATCH 01/49] Fix "The operation is insecure." when trying to register fido2 device. navigator.credentials.create(); Doesn't accept a port in the "id" parameter. So, when trying to register a fido2 device via WebAuthn throws: "The operation is insecure." on firefox and "The relying party ID is not a registrable domain suffix of, nor equal to the current domain." on Chrome or Edge. This commit replaces `$_SERVER['HTTP_HOST']` with `$_SERVER['SERVER_NAME']` when initializing `$WebAuthn` which excludes the port to formulate correct requests. Now Mailcow allows the registration of fido2 devices when running in a non-standard port(eg. 443). --- data/web/inc/prerequisites.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index a5eb2c80..95a9c63b 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -60,7 +60,7 @@ $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider); // FIDO2 $formats = $GLOBALS['FIDO2_FORMATS']; -$WebAuthn = new \WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['HTTP_HOST'], $formats); +$WebAuthn = new \WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['SERVER_NAME'], $formats); $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/solo.pem'); $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/apple.pem'); $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates/nitro.pem'); From ea1a412749ed6a1cb9da75f33be93e4f5f4d3ead Mon Sep 17 00:00:00 2001 From: El-Virus <36414402+El-Virus@users.noreply.github.com> Date: Fri, 21 Jan 2022 15:46:44 +0100 Subject: [PATCH 02/49] Fix missing "lbuchs", after resolving last conflict It seems that when solving the conflict in my pr when the latest staging branch was merged to master, I accidentally deleted "lbuchs", I added it back --- data/web/inc/prerequisites.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index ac86016a..8e8f5e8f 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -61,7 +61,7 @@ $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider); // FIDO2 $formats = $GLOBALS['FIDO2_FORMATS']; -$WebAuthn = new \WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['SERVER_NAME'], $formats); +$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['SERVER_NAME'], $formats); // only include root ca's when needed if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates'); From f09a3df870df0e2b66732e720e719d54559df209 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 21 Feb 2022 10:46:24 +0100 Subject: [PATCH 03/49] [Web] add verify selected tfa --- data/web/css/build/008-mailcow.css | 10 + data/web/inc/ajax/destroy_tfa_auth.php | 2 +- data/web/inc/footer.inc.php | 24 +- data/web/inc/functions.inc.php | 328 ++++++++++++++----------- data/web/inc/triggers.inc.php | 35 +-- data/web/json_api.php | 10 +- data/web/templates/base.twig | 100 ++++++-- data/web/templates/modals/footer.twig | 216 +++++++++++----- 8 files changed, 473 insertions(+), 252 deletions(-) diff --git a/data/web/css/build/008-mailcow.css b/data/web/css/build/008-mailcow.css index d7533424..5a965537 100644 --- a/data/web/css/build/008-mailcow.css +++ b/data/web/css/build/008-mailcow.css @@ -255,3 +255,13 @@ code { .flag-icon { margin-right: 5px; } +.list-group-item.webauthn-authenticator-selection, +.list-group-item.totp-authenticator-selection, +.list-group-item.yubi_otp-authenticator-selection { + border-radius: 0px !important; +} +.pending-tfa-collapse { + padding: 10px; + background: #fbfbfb; + border: 1px solid #ededed; +} diff --git a/data/web/inc/ajax/destroy_tfa_auth.php b/data/web/inc/ajax/destroy_tfa_auth.php index 72c7f1e3..07873b55 100644 --- a/data/web/inc/ajax/destroy_tfa_auth.php +++ b/data/web/inc/ajax/destroy_tfa_auth.php @@ -2,5 +2,5 @@ session_start(); unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); -unset($_SESSION['pending_tfa_method']); +unset($_SESSION['pending_tfa_methods']); ?> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 72482707..b2f1d4d5 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -23,6 +23,27 @@ if (is_array($alertbox_log_parser)) { unset($_SESSION['return']); } +// map tfa details for twig +$pending_tfa_authmechs = []; +foreach($_SESSION['pending_tfa_methods'] as $authdata){ + $pending_tfa_authmechs[$authdata['authmech']] = false; +} +if (isset($pending_tfa_authmechs['webauthn'])) { + $pending_tfa_authmechs['webauthn'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && isset($pending_tfa_authmechs['yubi_otp'])) { + $pending_tfa_authmechs['yubi_otp'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && !isset($pending_tfa_authmechs['yubi_otp']) + && isset($pending_tfa_authmechs['totp'])) { + $pending_tfa_authmechs['totp'] = true; +} +if (isset($pending_tfa_authmechs['u2f'])) { + $pending_tfa_authmechs['u2f'] = true; +} + // globals $globalVariables = [ 'mailcow_info' => array( @@ -30,7 +51,8 @@ $globalVariables = [ 'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'] ), 'js_path' => '/cache/'.basename($JSPath), - 'pending_tfa_method' => @$_SESSION['pending_tfa_method'], + 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'], + 'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'lang_footer' => json_encode($lang['footer']), 'lang_acl' => json_encode($lang['acl']), diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 21a0d8ce..0b485c6e 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -830,11 +830,15 @@ function check_login($user, $pass, $app_passwd_data = false) { $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 +846,7 @@ function check_login($user, $pass, $app_passwd_data = false) { '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 +869,14 @@ function check_login($user, $pass, $app_passwd_data = false) { $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_method'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', @@ -1142,47 +1148,46 @@ function set_tfa($_data) { global $yubi; 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; - } - } - $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 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; } } + // 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": @@ -1265,9 +1270,6 @@ function set_tfa($_data) { case "webauthn": $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"]; - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'webauthn'"); - $stmt->execute(array(':username' => $username)); - $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')"); $stmt->execute(array( @@ -1439,25 +1441,27 @@ function unset_tfa_key($_data) { 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)); @@ -1470,6 +1474,8 @@ function unset_tfa_key($_data) { ); 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( @@ -1487,7 +1493,7 @@ function unset_tfa_key($_data) { return false; } } -function get_tfa($username = null) { +function get_tfa($username = null, $key_id = null) { global $pdo; if (isset($_SESSION['mailcow_cc_username'])) { $username = $_SESSION['mailcow_cc_username']; @@ -1495,92 +1501,116 @@ function get_tfa($username = null) { 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($key_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 key_id + $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; + } + 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"); + $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; + case "webauthn": + $data['name'] = "webauthn"; + $data['pretty'] = "WebAuthn"; + $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' 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: + $data['name'] = 'none'; + $data['pretty'] = "-"; + return $data; + break; } - 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"); - $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; - case "webauthn": - $data['name'] = "webauthn"; - $data['pretty'] = "WebAuthn"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' 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, $_data, $WebAuthn) { - global $pdo; - global $yubi; - global $u2f; - global $tfa; +function verify_tfa_login($username, $_data) { + global $pdo; + global $yubi; + global $u2f; + global $tfa; + global $WebAuthn; + + if ($_data['tfa_method'] != 'u2f'){ $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); + WHERE `username` = :username AND `key_id` = :key_id AND `active` = '1'"); + $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); switch ($row["authmech"]) { @@ -1597,9 +1627,10 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' + AND `key_id` = ':key_id' AND `active`='1' AND `secret` LIKE :modhex"); - $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); + $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':key_id' => $_data['key_id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); $yubico_auth = explode(':', $row['secret']); $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]); @@ -1636,8 +1667,9 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'totp' + AND `key_id` = :key_id AND `active`='1'"); - $stmt->execute(array(':username' => $username)); + $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { @@ -1666,13 +1698,6 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; } break; - // u2f - deprecated, should be removed - case "u2f": - // delete old keys that used u2f - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = :authmech AND `username` = :username"); - $stmt->execute(array(':authmech' => 'u2f', ':username' => $username)); - - return true; case "webauthn": $tokenData = json_decode($_data['token']); $clientDataJSON = base64_decode($tokenData->clientDataJSON); @@ -1681,7 +1706,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); $stmt->execute(array(':tokenId' => $tokenData->id)); $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC); @@ -1738,7 +1763,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; - $_SESSION['tfa_id'] = $process_webauthn['key_id']; + $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; unset($_SESSION["challenge"]); $_SESSION['return'][] = array( @@ -1759,6 +1784,17 @@ function verify_tfa_login($username, $_data, $WebAuthn) { } return false; + } 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; + } } function admin_api($access, $action, $data = null) { global $pdo; diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index cb3a3771..1e2bdb42 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -1,24 +1,24 @@ tfa_method; $_data['key_id'] = $post->key_id; + $_data['confirm_password'] = $post->confirm_password; $_data['registration'] = $data; set_tfa($_data); @@ -450,14 +451,15 @@ if (isset($_GET['query'])) { $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $cids[] = base64_decode($row['keyHandle']); - } - if (count($cids) == 0) { + if (count($rows) == 0) { print(json_encode(array( 'type' => 'error', 'msg' => 'Cannot find matching credentialIds' ))); + exit; + } + while($row = array_shift($rows)) { + $cids[] = base64_decode($row['keyHandle']); } $getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 08376e71..2691718b 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -176,15 +176,62 @@ function recursiveBase64StrToArrayBuffer(obj) { {% endfor %} // Confirm TFA modal - {% if pending_tfa_method %} + {% if pending_tfa_methods %} $('#ConfirmTFAModal').modal({ backdrop: 'static', keyboard: false }); + // validate Yubi OTP tfa + $("#pending_tfa_tab_yubi_otp").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(".webauthn-authenticator-selection").removeClass("active"); + + $("#collapseTotpTFA").collapse('hide'); + $("#collapseWebAuthnTFA").collapse('hide'); + }); + $(".yubi-authenticator-selection").click(function(){ + $(".yubi-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var key_id = $(this).children('span').first().text(); + $("#yubi_selected_key_id").val(key_id); + + $("#collapseYubiTFA").collapse('show'); + }); + // validate Time based OTP tfa + $("#pending_tfa_tab_totp").click(function(){ + $(".yubi-authenticator-selection").removeClass("active"); + $(".webauthn-authenticator-selection").removeClass("active"); + + $("#collapseYubiTFA").collapse('hide'); + $("#collapseWebAuthnTFA").collapse('hide'); + }); + $(".totp-authenticator-selection").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var key_id = $(this).children('span').first().text(); + $("#totp_selected_key_id").val(key_id); + + $("#collapseTotpTFA").collapse('show'); + }); // validate WebAuthn tfa - $('#start_webauthn_confirmation').click(function(){ - $('#webauthn_status_auth').html('

' + lang_tfa.init_webauthn + '

'); + $("#pending_tfa_tab_webauthn").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(".yubi-authenticator-selection").removeClass("active"); + + $("#collapseTotpTFA").collapse('hide'); + $("#collapseYubiTFA").collapse('hide'); + }); + $(".webauthn-authenticator-selection").click(function(){ + $(".webauthn-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var key_id = $(this).children('span').first().text(); + $("#webauthn_selected_key_id").val(key_id); + + $("#collapseWebAuthnTFA").collapse('show'); $(this).find('input[name=token]').focus(); if(document.getElementById("webauthn_auth_data") !== null) { @@ -198,30 +245,31 @@ function recursiveBase64StrToArrayBuffer(obj) { window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { - if (json.success === false) throw new Error(); + if (json.success === false) throw new Error(); + if (json.type === "error") throw new Error(json.msg); - recursiveBase64StrToArrayBuffer(json); - return json; + recursiveBase64StrToArrayBuffer(json); + return json; }).then(getCredentialArgs => { - // get credentials - return navigator.credentials.get(getCredentialArgs); + // get credentials + return navigator.credentials.get(getCredentialArgs); }).then(cred => { - return { - id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, - clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, - authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, - signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null - }; + return { + id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, + clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, + authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, + signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null + }; }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) { - // send request by submit - var form = document.getElementById('webauthn_auth_form'); - var auth = document.getElementById('webauthn_auth_data'); - auth.value = AuthenticatorAttestationResponse; - form.submit(); + // send request by submit + var form = document.getElementById('webauthn_auth_form'); + var auth = document.getElementById('webauthn_auth_data'); + auth.value = AuthenticatorAttestationResponse; + form.submit(); }).catch(function(err) { - var webauthn_return_code = document.getElementById('webauthn_return_code'); - webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; - webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; + var webauthn_return_code = document.getElementById('webauthn_return_code'); + webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; + webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; }); } }); @@ -237,7 +285,9 @@ function recursiveBase64StrToArrayBuffer(obj) { } }); }); - {% endif %} + {% endif %} + + // Validate FIDO2 $("#fido2-login").click(function(){ $('#fido2-alerts').html(); @@ -358,6 +408,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#start_webauthn_register").click(() => { var key_id = document.getElementsByName('key_id')[1].value; + var confirm_password = document.getElementsByName('confirm_password')[1].value; // fetch WebAuthn create args window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => { @@ -375,7 +426,8 @@ function recursiveBase64StrToArrayBuffer(obj) { clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null, key_id: key_id, - tfa_method: "webauthn" + tfa_method: "webauthn", + confirm_password: confirm_password }; }).then(JSON.stringify).then(AuthenticatorAttestationResponse => { // send request diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 690b9de0..6df4a10d 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -133,73 +133,171 @@ {% endif %} -{% if pending_tfa_method %} +{% if pending_tfa_methods %} From 4c6a2055c2997396c6b3bc35da5ed79b4c43f97d Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 21 Feb 2022 14:10:12 +0100 Subject: [PATCH 04/49] [Web] add verify selected tfa --- data/web/inc/functions.inc.php | 61 ++++++++++++++++----------- data/web/templates/base.twig | 12 +++--- data/web/templates/modals/footer.twig | 17 +++++--- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 0b485c6e..f93e7eca 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1493,7 +1493,7 @@ function unset_tfa_key($_data) { return false; } } -function get_tfa($username = null, $key_id = null) { +function get_tfa($username = null, $id = null) { global $pdo; if (isset($_SESSION['mailcow_cc_username'])) { $username = $_SESSION['mailcow_cc_username']; @@ -1502,7 +1502,7 @@ function get_tfa($username = null, $key_id = null) { return false; } - if (!isset($key_id)){ + 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'"); @@ -1520,10 +1520,10 @@ function get_tfa($username = null, $key_id = null) { $data['additional'] = $results; return $data; } else { - // fetch specific authenticator details by key_id + // fetch specific authenticator details by id $stmt = $pdo->prepare("SELECT * FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); + 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"])) { @@ -1531,9 +1531,10 @@ function get_tfa($username = null, $key_id = null) { 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 = $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)) { @@ -1545,9 +1546,10 @@ function get_tfa($username = null, $key_id = null) { 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 = $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)) { @@ -1563,9 +1565,10 @@ function get_tfa($username = null, $key_id = null) { 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 = $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)) { @@ -1576,9 +1579,10 @@ function get_tfa($username = null, $key_id = null) { case "webauthn": $data['name'] = "webauthn"; $data['pretty'] = "WebAuthn"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username"); + $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)) { @@ -1609,8 +1613,8 @@ function verify_tfa_login($username, $_data) { if ($_data['tfa_method'] != 'u2f'){ $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` - WHERE `username` = :username AND `key_id` = :key_id AND `active` = '1'"); - $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); + WHERE `username` = :username AND `id` = :id AND `active` = '1'"); + $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); switch ($row["authmech"]) { @@ -1627,10 +1631,10 @@ function verify_tfa_login($username, $_data) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' - AND `key_id` = ':key_id' + AND `id` = ':id' AND `active`='1' AND `secret` LIKE :modhex"); - $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':key_id' => $_data['key_id'])); + $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); $yubico_auth = explode(':', $row['secret']); $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]); @@ -1663,16 +1667,16 @@ function verify_tfa_login($username, $_data) { return false; break; case "totp": - try { + try { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'totp' - AND `key_id` = :key_id + AND `id` = :id AND `active`='1'"); - $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); + $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) { + if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { $_SESSION['tfa_id'] = $row['id']; $_SESSION['return'][] = array( 'type' => 'success', @@ -1680,7 +1684,7 @@ function verify_tfa_login($username, $_data) { 'msg' => 'verified_totp_login' ); return true; - } + } } $_SESSION['return'][] = array( 'type' => 'danger', @@ -1688,15 +1692,15 @@ function verify_tfa_login($username, $_data) { 'msg' => 'totp_verification_failed' ); return false; - } - catch (PDOException $e) { + } + 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']); @@ -1706,13 +1710,20 @@ function verify_tfa_login($username, $_data) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); - $stmt->execute(array(':tokenId' => $tokenData->id)); + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id"); + $stmt->execute(array(':id' => $_data['id'])); $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false; + if (empty($process_webauthn)){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_verification_failed', 'authenticator not found') + ); + return false; + } - if ($process_webauthn['publicKey'] === false) { + if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 2691718b..b51dcde2 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -194,8 +194,8 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".yubi-authenticator-selection").removeClass("active"); $(this).addClass("active"); - var key_id = $(this).children('span').first().text(); - $("#yubi_selected_key_id").val(key_id); + var id = $(this).children('input').first().val(); + $("#yubi_selected_id").val(id); $("#collapseYubiTFA").collapse('show'); }); @@ -211,8 +211,8 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".totp-authenticator-selection").removeClass("active"); $(this).addClass("active"); - var key_id = $(this).children('span').first().text(); - $("#totp_selected_key_id").val(key_id); + var id = $(this).children('input').first().val(); + $("#totp_selected_id").val(id); $("#collapseTotpTFA").collapse('show'); }); @@ -228,8 +228,8 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".webauthn-authenticator-selection").removeClass("active"); $(this).addClass("active"); - var key_id = $(this).children('span').first().text(); - $("#webauthn_selected_key_id").val(key_id); + var id = $(this).children('input').first().val(); + $("#webauthn_selected_id").val(id); $("#collapseWebAuthnTFA").collapse('show'); diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 6df4a10d..67cc3482 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -139,7 +139,7 @@ @@ -205,7 +206,7 @@
- Available Authenticators + Authenticators
{% for authenticator in pending_tfa_methods %} @@ -213,6 +214,7 @@ {{ authenticator["key_id"] }} + {% endif %} {% endfor %} @@ -223,7 +225,7 @@ Yubicon Icon - +
@@ -240,7 +242,7 @@ - Available Authenticators + Authenticators
{% for authenticator in pending_tfa_methods %} @@ -248,6 +250,7 @@ {{ authenticator["key_id"] }} + {% endif %} {% endfor %} @@ -258,7 +261,7 @@ -
+
From df33f1a1303d893e16a3724434eeff93ee0b528e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 22 Feb 2022 09:38:06 +0100 Subject: [PATCH 05/49] [Web] multiple tfa - domainadmin support --- data/web/inc/functions.inc.php | 2 +- data/web/templates/domainadmin.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index f93e7eca..384905d4 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -876,7 +876,7 @@ function check_login($user, $pass, $app_passwd_data = false) { 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'] = $authenticators['additional']; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', diff --git a/data/web/templates/domainadmin.twig b/data/web/templates/domainadmin.twig index 6aae54ba..3bc7ab40 100644 --- a/data/web/templates/domainadmin.twig +++ b/data/web/templates/domainadmin.twig @@ -28,7 +28,7 @@
From a2d57d43d1f61435b865d08ccd0225c06963d6ff Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 7 Mar 2022 11:41:13 +0100 Subject: [PATCH 06/49] [Web] multiple tfa - user support --- data/web/inc/functions.inc.php | 47 ++++++++++++++-------- data/web/inc/triggers.inc.php | 6 +-- data/web/templates/user/tab-user-auth.twig | 27 ++++++++++++- data/web/user.php | 1 + 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 384905d4..12efd60b 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -936,24 +936,39 @@ function check_login($user, $pass, $app_passwd_data = false) { $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); } 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) { - $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( - ':service' => $service, - ':app_id' => $row['app_passwd_id'], - ':username' => $user, - ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) - )); + // 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'] = "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 ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { + $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( + ':service' => $service, + ':app_id' => $row['app_passwd_id'], + ':username' => $user, + ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) + )); + } + + 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"); + $stmt->execute(array(':user' => $user)); + return "user"; } - return "user"; } } diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 1e2bdb42..aec043e9 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -61,9 +61,9 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { header("Location: /user"); } elseif ($as != "pending") { - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); unset($_SESSION['mailcow_cc_username']); unset($_SESSION['mailcow_cc_role']); } diff --git a/data/web/templates/user/tab-user-auth.twig b/data/web/templates/user/tab-user-auth.twig index c35eee39..cbf5f83d 100644 --- a/data/web/templates/user/tab-user-auth.twig +++ b/data/web/templates/user/tab-user-auth.twig @@ -15,6 +15,10 @@ {{ lang.user.open_webmail_sso }} {% endif %} +

@@ -40,8 +44,27 @@

{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}
{{ mailboxdata.messages }} {{ lang.user.messages }}

-
-

{{ lang.user.change_password }}

+ + +
+ {# TFA #} +
+
{{ lang.tfa.tfa }}:
+
+

{{ tfa_data.pretty }}

+ {% include 'tfa_keys.twig' %} +
+
+
+
+
{{ lang.tfa.set_tfa }}:
+
+

diff --git a/data/web/user.php b/data/web/user.php index 5bf60917..d7faf791 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -76,6 +76,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' 'acl_json' => json_encode($_SESSION['acl']), 'user_spam_score' => mailbox('get', 'spam_score', $username), 'tfa_data' => $tfa_data, + 'tfa_id' => @$_SESSION['tfa_id'], 'fido2_data' => $fido2_data, 'mailboxdata' => $mailboxdata, 'clientconfigstr' => $clientconfigstr, From 84b4269c75d21deb4c7b03bbc14b528ed5f27232 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 14 Mar 2022 09:29:07 +0100 Subject: [PATCH 07/49] [Web] increase mysql publicKey field length --- data/web/inc/init_db.inc.php | 4 ++-- data/web/json_api.php | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 3cab461e..5705379d 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "18012022_1020"; + $db_version = "14032022_0921"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -699,7 +699,7 @@ function init_db_schema() { "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", "secret" => "VARCHAR(255) DEFAULT NULL", "keyHandle" => "VARCHAR(255) DEFAULT NULL", - "publicKey" => "VARCHAR(255) DEFAULT NULL", + "publicKey" => "VARCHAR(4096) DEFAULT NULL", "counter" => "INT NOT NULL DEFAULT '0'", "certificate" => "TEXT", "active" => "TINYINT(1) NOT NULL DEFAULT '0'" diff --git a/data/web/json_api.php b/data/web/json_api.php index 9a557e7b..79056bc6 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -175,15 +175,22 @@ if (isset($_GET['query'])) { // parse post data $post = trim(file_get_contents('php://input')); if ($post) $post = json_decode($post); - - // decode base64 strings - $clientDataJSON = base64_decode($post->clientDataJSON); - $attestationObject = base64_decode($post->attestationObject); // process registration data from authenticator try { + // decode base64 strings + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true); + + // safe authenticator in mysql `tfa` table + $_data['tfa_method'] = $post->tfa_method; + $_data['key_id'] = $post->key_id; + $_data['confirm_password'] = $post->confirm_password; + $_data['registration'] = $data; + set_tfa($_data); } catch (Throwable $ex) { // err @@ -194,12 +201,6 @@ if (isset($_GET['query'])) { exit; } - // safe authenticator in mysql `tfa` table - $_data['tfa_method'] = $post->tfa_method; - $_data['key_id'] = $post->key_id; - $_data['confirm_password'] = $post->confirm_password; - $_data['registration'] = $data; - set_tfa($_data); // send response $return = new stdClass(); From 3ef2b6cfa2a99c1adbd2e9e5e74b278274fa4ade Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 21 Feb 2022 10:46:24 +0100 Subject: [PATCH 08/49] [Web] add verify selected tfa --- data/web/css/build/008-mailcow.css | 10 + data/web/inc/ajax/destroy_tfa_auth.php | 2 +- data/web/inc/footer.inc.php | 24 +- data/web/inc/functions.inc.php | 328 ++++++++++++++----------- data/web/inc/triggers.inc.php | 35 +-- data/web/json_api.php | 10 +- data/web/templates/base.twig | 100 ++++++-- data/web/templates/modals/footer.twig | 216 +++++++++++----- 8 files changed, 473 insertions(+), 252 deletions(-) diff --git a/data/web/css/build/008-mailcow.css b/data/web/css/build/008-mailcow.css index 4d45a75e..5ce2afda 100644 --- a/data/web/css/build/008-mailcow.css +++ b/data/web/css/build/008-mailcow.css @@ -257,3 +257,13 @@ code { .flag-icon { margin-right: 5px; } +.list-group-item.webauthn-authenticator-selection, +.list-group-item.totp-authenticator-selection, +.list-group-item.yubi_otp-authenticator-selection { + border-radius: 0px !important; +} +.pending-tfa-collapse { + padding: 10px; + background: #fbfbfb; + border: 1px solid #ededed; +} diff --git a/data/web/inc/ajax/destroy_tfa_auth.php b/data/web/inc/ajax/destroy_tfa_auth.php index 72c7f1e3..07873b55 100644 --- a/data/web/inc/ajax/destroy_tfa_auth.php +++ b/data/web/inc/ajax/destroy_tfa_auth.php @@ -2,5 +2,5 @@ session_start(); unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); -unset($_SESSION['pending_tfa_method']); +unset($_SESSION['pending_tfa_methods']); ?> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 72482707..b2f1d4d5 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -23,6 +23,27 @@ if (is_array($alertbox_log_parser)) { unset($_SESSION['return']); } +// map tfa details for twig +$pending_tfa_authmechs = []; +foreach($_SESSION['pending_tfa_methods'] as $authdata){ + $pending_tfa_authmechs[$authdata['authmech']] = false; +} +if (isset($pending_tfa_authmechs['webauthn'])) { + $pending_tfa_authmechs['webauthn'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && isset($pending_tfa_authmechs['yubi_otp'])) { + $pending_tfa_authmechs['yubi_otp'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && !isset($pending_tfa_authmechs['yubi_otp']) + && isset($pending_tfa_authmechs['totp'])) { + $pending_tfa_authmechs['totp'] = true; +} +if (isset($pending_tfa_authmechs['u2f'])) { + $pending_tfa_authmechs['u2f'] = true; +} + // globals $globalVariables = [ 'mailcow_info' => array( @@ -30,7 +51,8 @@ $globalVariables = [ 'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'] ), 'js_path' => '/cache/'.basename($JSPath), - 'pending_tfa_method' => @$_SESSION['pending_tfa_method'], + 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'], + 'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'lang_footer' => json_encode($lang['footer']), 'lang_acl' => json_encode($lang['acl']), diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 21a0d8ce..0b485c6e 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -830,11 +830,15 @@ function check_login($user, $pass, $app_passwd_data = false) { $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 +846,7 @@ function check_login($user, $pass, $app_passwd_data = false) { '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 +869,14 @@ function check_login($user, $pass, $app_passwd_data = false) { $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_method'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', @@ -1142,47 +1148,46 @@ function set_tfa($_data) { global $yubi; 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; - } - } - $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 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; } } + // 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": @@ -1265,9 +1270,6 @@ function set_tfa($_data) { case "webauthn": $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"]; - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'webauthn'"); - $stmt->execute(array(':username' => $username)); - $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')"); $stmt->execute(array( @@ -1439,25 +1441,27 @@ function unset_tfa_key($_data) { 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)); @@ -1470,6 +1474,8 @@ function unset_tfa_key($_data) { ); 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( @@ -1487,7 +1493,7 @@ function unset_tfa_key($_data) { return false; } } -function get_tfa($username = null) { +function get_tfa($username = null, $key_id = null) { global $pdo; if (isset($_SESSION['mailcow_cc_username'])) { $username = $_SESSION['mailcow_cc_username']; @@ -1495,92 +1501,116 @@ function get_tfa($username = null) { 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($key_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 key_id + $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; + } + 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"); + $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; + case "webauthn": + $data['name'] = "webauthn"; + $data['pretty'] = "WebAuthn"; + $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' 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: + $data['name'] = 'none'; + $data['pretty'] = "-"; + return $data; + break; } - 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"); - $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; - case "webauthn": - $data['name'] = "webauthn"; - $data['pretty'] = "WebAuthn"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' 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, $_data, $WebAuthn) { - global $pdo; - global $yubi; - global $u2f; - global $tfa; +function verify_tfa_login($username, $_data) { + global $pdo; + global $yubi; + global $u2f; + global $tfa; + global $WebAuthn; + + if ($_data['tfa_method'] != 'u2f'){ $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); + WHERE `username` = :username AND `key_id` = :key_id AND `active` = '1'"); + $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); switch ($row["authmech"]) { @@ -1597,9 +1627,10 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' + AND `key_id` = ':key_id' AND `active`='1' AND `secret` LIKE :modhex"); - $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); + $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':key_id' => $_data['key_id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); $yubico_auth = explode(':', $row['secret']); $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]); @@ -1636,8 +1667,9 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'totp' + AND `key_id` = :key_id AND `active`='1'"); - $stmt->execute(array(':username' => $username)); + $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { @@ -1666,13 +1698,6 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; } break; - // u2f - deprecated, should be removed - case "u2f": - // delete old keys that used u2f - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = :authmech AND `username` = :username"); - $stmt->execute(array(':authmech' => 'u2f', ':username' => $username)); - - return true; case "webauthn": $tokenData = json_decode($_data['token']); $clientDataJSON = base64_decode($tokenData->clientDataJSON); @@ -1681,7 +1706,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); $stmt->execute(array(':tokenId' => $tokenData->id)); $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC); @@ -1738,7 +1763,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; - $_SESSION['tfa_id'] = $process_webauthn['key_id']; + $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; unset($_SESSION["challenge"]); $_SESSION['return'][] = array( @@ -1759,6 +1784,17 @@ function verify_tfa_login($username, $_data, $WebAuthn) { } return false; + } 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; + } } function admin_api($access, $action, $data = null) { global $pdo; diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index cb3a3771..1e2bdb42 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -1,24 +1,24 @@ tfa_method; $_data['key_id'] = $post->key_id; + $_data['confirm_password'] = $post->confirm_password; $_data['registration'] = $data; set_tfa($_data); @@ -450,14 +451,15 @@ if (isset($_GET['query'])) { $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $cids[] = base64_decode($row['keyHandle']); - } - if (count($cids) == 0) { + if (count($rows) == 0) { print(json_encode(array( 'type' => 'error', 'msg' => 'Cannot find matching credentialIds' ))); + exit; + } + while($row = array_shift($rows)) { + $cids[] = base64_decode($row['keyHandle']); } $getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 08376e71..2691718b 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -176,15 +176,62 @@ function recursiveBase64StrToArrayBuffer(obj) { {% endfor %} // Confirm TFA modal - {% if pending_tfa_method %} + {% if pending_tfa_methods %} $('#ConfirmTFAModal').modal({ backdrop: 'static', keyboard: false }); + // validate Yubi OTP tfa + $("#pending_tfa_tab_yubi_otp").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(".webauthn-authenticator-selection").removeClass("active"); + + $("#collapseTotpTFA").collapse('hide'); + $("#collapseWebAuthnTFA").collapse('hide'); + }); + $(".yubi-authenticator-selection").click(function(){ + $(".yubi-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var key_id = $(this).children('span').first().text(); + $("#yubi_selected_key_id").val(key_id); + + $("#collapseYubiTFA").collapse('show'); + }); + // validate Time based OTP tfa + $("#pending_tfa_tab_totp").click(function(){ + $(".yubi-authenticator-selection").removeClass("active"); + $(".webauthn-authenticator-selection").removeClass("active"); + + $("#collapseYubiTFA").collapse('hide'); + $("#collapseWebAuthnTFA").collapse('hide'); + }); + $(".totp-authenticator-selection").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var key_id = $(this).children('span').first().text(); + $("#totp_selected_key_id").val(key_id); + + $("#collapseTotpTFA").collapse('show'); + }); // validate WebAuthn tfa - $('#start_webauthn_confirmation').click(function(){ - $('#webauthn_status_auth').html('

' + lang_tfa.init_webauthn + '

'); + $("#pending_tfa_tab_webauthn").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(".yubi-authenticator-selection").removeClass("active"); + + $("#collapseTotpTFA").collapse('hide'); + $("#collapseYubiTFA").collapse('hide'); + }); + $(".webauthn-authenticator-selection").click(function(){ + $(".webauthn-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var key_id = $(this).children('span').first().text(); + $("#webauthn_selected_key_id").val(key_id); + + $("#collapseWebAuthnTFA").collapse('show'); $(this).find('input[name=token]').focus(); if(document.getElementById("webauthn_auth_data") !== null) { @@ -198,30 +245,31 @@ function recursiveBase64StrToArrayBuffer(obj) { window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { - if (json.success === false) throw new Error(); + if (json.success === false) throw new Error(); + if (json.type === "error") throw new Error(json.msg); - recursiveBase64StrToArrayBuffer(json); - return json; + recursiveBase64StrToArrayBuffer(json); + return json; }).then(getCredentialArgs => { - // get credentials - return navigator.credentials.get(getCredentialArgs); + // get credentials + return navigator.credentials.get(getCredentialArgs); }).then(cred => { - return { - id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, - clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, - authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, - signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null - }; + return { + id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, + clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, + authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, + signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null + }; }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) { - // send request by submit - var form = document.getElementById('webauthn_auth_form'); - var auth = document.getElementById('webauthn_auth_data'); - auth.value = AuthenticatorAttestationResponse; - form.submit(); + // send request by submit + var form = document.getElementById('webauthn_auth_form'); + var auth = document.getElementById('webauthn_auth_data'); + auth.value = AuthenticatorAttestationResponse; + form.submit(); }).catch(function(err) { - var webauthn_return_code = document.getElementById('webauthn_return_code'); - webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; - webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; + var webauthn_return_code = document.getElementById('webauthn_return_code'); + webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; + webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; }); } }); @@ -237,7 +285,9 @@ function recursiveBase64StrToArrayBuffer(obj) { } }); }); - {% endif %} + {% endif %} + + // Validate FIDO2 $("#fido2-login").click(function(){ $('#fido2-alerts').html(); @@ -358,6 +408,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#start_webauthn_register").click(() => { var key_id = document.getElementsByName('key_id')[1].value; + var confirm_password = document.getElementsByName('confirm_password')[1].value; // fetch WebAuthn create args window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => { @@ -375,7 +426,8 @@ function recursiveBase64StrToArrayBuffer(obj) { clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null, key_id: key_id, - tfa_method: "webauthn" + tfa_method: "webauthn", + confirm_password: confirm_password }; }).then(JSON.stringify).then(AuthenticatorAttestationResponse => { // send request diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 690b9de0..6df4a10d 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -133,73 +133,171 @@ {% endif %} -{% if pending_tfa_method %} +{% if pending_tfa_methods %} From 5fcccbc97d02c4b0cdd0c479bd15d6293b83dbb0 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 21 Feb 2022 14:10:12 +0100 Subject: [PATCH 09/49] [Web] add verify selected tfa --- data/web/inc/functions.inc.php | 61 ++++++++++++++++----------- data/web/templates/base.twig | 12 +++--- data/web/templates/modals/footer.twig | 17 +++++--- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 0b485c6e..f93e7eca 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1493,7 +1493,7 @@ function unset_tfa_key($_data) { return false; } } -function get_tfa($username = null, $key_id = null) { +function get_tfa($username = null, $id = null) { global $pdo; if (isset($_SESSION['mailcow_cc_username'])) { $username = $_SESSION['mailcow_cc_username']; @@ -1502,7 +1502,7 @@ function get_tfa($username = null, $key_id = null) { return false; } - if (!isset($key_id)){ + 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'"); @@ -1520,10 +1520,10 @@ function get_tfa($username = null, $key_id = null) { $data['additional'] = $results; return $data; } else { - // fetch specific authenticator details by key_id + // fetch specific authenticator details by id $stmt = $pdo->prepare("SELECT * FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); + 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"])) { @@ -1531,9 +1531,10 @@ function get_tfa($username = null, $key_id = null) { 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 = $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)) { @@ -1545,9 +1546,10 @@ function get_tfa($username = null, $key_id = null) { 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 = $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)) { @@ -1563,9 +1565,10 @@ function get_tfa($username = null, $key_id = null) { 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 = $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)) { @@ -1576,9 +1579,10 @@ function get_tfa($username = null, $key_id = null) { case "webauthn": $data['name'] = "webauthn"; $data['pretty'] = "WebAuthn"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username"); + $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)) { @@ -1609,8 +1613,8 @@ function verify_tfa_login($username, $_data) { if ($_data['tfa_method'] != 'u2f'){ $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` - WHERE `username` = :username AND `key_id` = :key_id AND `active` = '1'"); - $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); + WHERE `username` = :username AND `id` = :id AND `active` = '1'"); + $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); switch ($row["authmech"]) { @@ -1627,10 +1631,10 @@ function verify_tfa_login($username, $_data) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' - AND `key_id` = ':key_id' + AND `id` = ':id' AND `active`='1' AND `secret` LIKE :modhex"); - $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':key_id' => $_data['key_id'])); + $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); $yubico_auth = explode(':', $row['secret']); $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]); @@ -1663,16 +1667,16 @@ function verify_tfa_login($username, $_data) { return false; break; case "totp": - try { + try { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'totp' - AND `key_id` = :key_id + AND `id` = :id AND `active`='1'"); - $stmt->execute(array(':username' => $username, ':key_id' => $_data['key_id'])); + $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) { + if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { $_SESSION['tfa_id'] = $row['id']; $_SESSION['return'][] = array( 'type' => 'success', @@ -1680,7 +1684,7 @@ function verify_tfa_login($username, $_data) { 'msg' => 'verified_totp_login' ); return true; - } + } } $_SESSION['return'][] = array( 'type' => 'danger', @@ -1688,15 +1692,15 @@ function verify_tfa_login($username, $_data) { 'msg' => 'totp_verification_failed' ); return false; - } - catch (PDOException $e) { + } + 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']); @@ -1706,13 +1710,20 @@ function verify_tfa_login($username, $_data) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); - $stmt->execute(array(':tokenId' => $tokenData->id)); + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id"); + $stmt->execute(array(':id' => $_data['id'])); $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false; + if (empty($process_webauthn)){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_verification_failed', 'authenticator not found') + ); + return false; + } - if ($process_webauthn['publicKey'] === false) { + if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 2691718b..b51dcde2 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -194,8 +194,8 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".yubi-authenticator-selection").removeClass("active"); $(this).addClass("active"); - var key_id = $(this).children('span').first().text(); - $("#yubi_selected_key_id").val(key_id); + var id = $(this).children('input').first().val(); + $("#yubi_selected_id").val(id); $("#collapseYubiTFA").collapse('show'); }); @@ -211,8 +211,8 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".totp-authenticator-selection").removeClass("active"); $(this).addClass("active"); - var key_id = $(this).children('span').first().text(); - $("#totp_selected_key_id").val(key_id); + var id = $(this).children('input').first().val(); + $("#totp_selected_id").val(id); $("#collapseTotpTFA").collapse('show'); }); @@ -228,8 +228,8 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".webauthn-authenticator-selection").removeClass("active"); $(this).addClass("active"); - var key_id = $(this).children('span').first().text(); - $("#webauthn_selected_key_id").val(key_id); + var id = $(this).children('input').first().val(); + $("#webauthn_selected_id").val(id); $("#collapseWebAuthnTFA").collapse('show'); diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 6df4a10d..67cc3482 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -139,7 +139,7 @@ @@ -205,7 +206,7 @@
- Available Authenticators + Authenticators
{% for authenticator in pending_tfa_methods %} @@ -213,6 +214,7 @@ {{ authenticator["key_id"] }} + {% endif %} {% endfor %} @@ -223,7 +225,7 @@ Yubicon Icon - +
@@ -240,7 +242,7 @@ - Available Authenticators + Authenticators
{% for authenticator in pending_tfa_methods %} @@ -248,6 +250,7 @@ {{ authenticator["key_id"] }} + {% endif %} {% endfor %} @@ -258,7 +261,7 @@ -
+
From 21fadf6df2bad0a7d4edf18a1506303b1728edb0 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 22 Feb 2022 09:38:06 +0100 Subject: [PATCH 10/49] [Web] multiple tfa - domainadmin support --- data/web/inc/functions.inc.php | 2 +- data/web/templates/domainadmin.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index f93e7eca..384905d4 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -876,7 +876,7 @@ function check_login($user, $pass, $app_passwd_data = false) { 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'] = $authenticators['additional']; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', diff --git a/data/web/templates/domainadmin.twig b/data/web/templates/domainadmin.twig index 6aae54ba..3bc7ab40 100644 --- a/data/web/templates/domainadmin.twig +++ b/data/web/templates/domainadmin.twig @@ -28,7 +28,7 @@
From 49c506eed956fdc985f3b64e82336c27fca68e52 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 7 Mar 2022 11:41:13 +0100 Subject: [PATCH 11/49] [Web] multiple tfa - user support --- data/web/inc/functions.inc.php | 47 ++++++++++++++-------- data/web/inc/triggers.inc.php | 6 +-- data/web/templates/user/tab-user-auth.twig | 27 ++++++++++++- data/web/user.php | 1 + 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 384905d4..12efd60b 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -936,24 +936,39 @@ function check_login($user, $pass, $app_passwd_data = false) { $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); } 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) { - $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( - ':service' => $service, - ':app_id' => $row['app_passwd_id'], - ':username' => $user, - ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) - )); + // 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'] = "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 ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { + $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( + ':service' => $service, + ':app_id' => $row['app_passwd_id'], + ':username' => $user, + ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) + )); + } + + 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"); + $stmt->execute(array(':user' => $user)); + return "user"; } - return "user"; } } diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 1e2bdb42..aec043e9 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -61,9 +61,9 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { header("Location: /user"); } elseif ($as != "pending") { - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); unset($_SESSION['mailcow_cc_username']); unset($_SESSION['mailcow_cc_role']); } diff --git a/data/web/templates/user/tab-user-auth.twig b/data/web/templates/user/tab-user-auth.twig index e1f84fff..0d4c6f32 100644 --- a/data/web/templates/user/tab-user-auth.twig +++ b/data/web/templates/user/tab-user-auth.twig @@ -15,6 +15,10 @@ {{ lang.user.open_webmail_sso }} {% endif %} +

@@ -40,8 +44,27 @@

{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}
{{ mailboxdata.messages }} {{ lang.user.messages }}

-
-

{{ lang.user.change_password }}

+ + +
+ {# TFA #} +
+
{{ lang.tfa.tfa }}:
+
+

{{ tfa_data.pretty }}

+ {% include 'tfa_keys.twig' %} +
+
+
+
+
{{ lang.tfa.set_tfa }}:
+
+

diff --git a/data/web/user.php b/data/web/user.php index 5bf60917..d7faf791 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -76,6 +76,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' 'acl_json' => json_encode($_SESSION['acl']), 'user_spam_score' => mailbox('get', 'spam_score', $username), 'tfa_data' => $tfa_data, + 'tfa_id' => @$_SESSION['tfa_id'], 'fido2_data' => $fido2_data, 'mailboxdata' => $mailboxdata, 'clientconfigstr' => $clientconfigstr, From e7fe52a62522ef3f0f51d79e964c8209834073cc Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 14 Mar 2022 09:29:07 +0100 Subject: [PATCH 12/49] [Web] increase mysql publicKey field length --- data/web/inc/init_db.inc.php | 4 ++-- data/web/json_api.php | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 3cab461e..5705379d 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "18012022_1020"; + $db_version = "14032022_0921"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -699,7 +699,7 @@ function init_db_schema() { "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", "secret" => "VARCHAR(255) DEFAULT NULL", "keyHandle" => "VARCHAR(255) DEFAULT NULL", - "publicKey" => "VARCHAR(255) DEFAULT NULL", + "publicKey" => "VARCHAR(4096) DEFAULT NULL", "counter" => "INT NOT NULL DEFAULT '0'", "certificate" => "TEXT", "active" => "TINYINT(1) NOT NULL DEFAULT '0'" diff --git a/data/web/json_api.php b/data/web/json_api.php index 9a557e7b..79056bc6 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -175,15 +175,22 @@ if (isset($_GET['query'])) { // parse post data $post = trim(file_get_contents('php://input')); if ($post) $post = json_decode($post); - - // decode base64 strings - $clientDataJSON = base64_decode($post->clientDataJSON); - $attestationObject = base64_decode($post->attestationObject); // process registration data from authenticator try { + // decode base64 strings + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true); + + // safe authenticator in mysql `tfa` table + $_data['tfa_method'] = $post->tfa_method; + $_data['key_id'] = $post->key_id; + $_data['confirm_password'] = $post->confirm_password; + $_data['registration'] = $data; + set_tfa($_data); } catch (Throwable $ex) { // err @@ -194,12 +201,6 @@ if (isset($_GET['query'])) { exit; } - // safe authenticator in mysql `tfa` table - $_data['tfa_method'] = $post->tfa_method; - $_data['key_id'] = $post->key_id; - $_data['confirm_password'] = $post->confirm_password; - $_data['registration'] = $data; - set_tfa($_data); // send response $return = new stdClass(); From b185f83fc3e38ae44d87161e4f9d229bcc8f4bec Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 18 Mar 2022 08:37:22 +0100 Subject: [PATCH 13/49] [Web] tfa extra debugging --- data/web/inc/functions.inc.php | 94 ++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 12efd60b..c58662fd 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1626,12 +1626,28 @@ function verify_tfa_login($username, $_data) { global $tfa; global $WebAuthn; + // just for debugging + // remove later + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, 'tfa_post_data_log'), + 'msg' => $_data + ); + if ($_data['tfa_method'] != 'u2f'){ $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` WHERE `username` = :username AND `id` = :id AND `active` = '1'"); $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); + // just for debugging + // remove later + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, 'tfa_verify_authmech'), + 'msg' => $row + ); + switch ($row["authmech"]) { case "yubi_otp": if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) { @@ -1718,6 +1734,7 @@ function verify_tfa_login($username, $_data) { } break; case "webauthn": + // prepare authenticator data $tokenData = json_decode($_data['token']); $clientDataJSON = base64_decode($tokenData->clientDataJSON); $authenticatorData = base64_decode($tokenData->authenticatorData); @@ -1725,10 +1742,28 @@ function verify_tfa_login($username, $_data) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id"); + // just for debugging + // remove later + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, 'tfa_try_verify'), + 'msg' => 'try grab authenticator' + ); + + // fetch authenticator + $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); + // just for debugging + // remove later + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, 'tfa_authenticator_grabbed'), + 'msg' => 'grabbed authenticator from db' + ); + + // return err if no authenticator was found if (empty($process_webauthn)){ $_SESSION['return'][] = array( 'type' => 'danger', @@ -1738,6 +1773,7 @@ function verify_tfa_login($username, $_data) { return false; } + // return err if authenticator has no publicKey if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1746,6 +1782,8 @@ function verify_tfa_login($username, $_data) { ); return false; } + + // try verify authenticator try { $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); } @@ -1758,26 +1796,54 @@ function verify_tfa_login($username, $_data) { return false; } - + // just for debugging + // remove later + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, 'tfa_progress'), + 'msg' => 'authenticator verified, check user role' + ); + + // if verified, check user role $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"; + // is admin + $_SESSION["mailcow_cc_role"] = "admin"; } elseif ($obj_props['superadmin'] === 0) { - $_SESSION["mailcow_cc_role"] = "domainadmin"; + // is domainadmin + $_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 ($row['username'] == $process_webauthn['username']) { + // just for debugging + // remove later + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, 'tfa_progress'), + 'msg' => 'no admin or domainadmin role, check if normal user' + ); + + // no admin, check if normal user + $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']) { + // is user $_SESSION["mailcow_cc_role"] = "user"; - } + } else { + // err, no specific role found + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_verification_failed', 'could not determine user role') + ); + return false; + } } - + // check if fetched user and pendig_user matches if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ $_SESSION['return'][] = array( 'type' => 'danger', @@ -1787,7 +1853,15 @@ function verify_tfa_login($username, $_data) { return false; } + // just for debugging + // remove later + $_SESSION['return'][] = array( + 'type' => 'info', + 'log' => array(__FUNCTION__, 'tfa_success'), + 'msg' => 'tfa flow success' + ); + // set user session data and delete WebAuthn challenge session $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; From 70921b8d154d365f13b50b91a68ae5cc484a79a9 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 18 Mar 2022 08:45:02 +0100 Subject: [PATCH 14/49] [Web] tfa extra debugging --- data/web/inc/functions.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index c58662fd..e43024f1 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1829,7 +1829,7 @@ function verify_tfa_login($username, $_data) { $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']) { + if (!empty($row['username'])) { // is user $_SESSION["mailcow_cc_role"] = "user"; } else { From 6d3798ad08176cf542bb2fdea858be99cccbf190 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Sat, 19 Mar 2022 20:18:31 +0100 Subject: [PATCH 15/49] [Web] fix yubi otp --- data/web/inc/functions.inc.php | 73 +--------------------------------- 1 file changed, 2 insertions(+), 71 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index e43024f1..9c73a475 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1626,28 +1626,12 @@ function verify_tfa_login($username, $_data) { global $tfa; global $WebAuthn; - // just for debugging - // remove later - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, 'tfa_post_data_log'), - 'msg' => $_data - ); - if ($_data['tfa_method'] != 'u2f'){ $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` WHERE `username` = :username AND `id` = :id AND `active` = '1'"); $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); - // just for debugging - // remove later - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, 'tfa_verify_authmech'), - 'msg' => $row - ); - switch ($row["authmech"]) { case "yubi_otp": if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) { @@ -1662,8 +1646,8 @@ function verify_tfa_login($username, $_data) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' - AND `id` = ':id' - AND `active`='1' + AND `id` = :id + AND `active` = '1' AND `secret` LIKE :modhex"); $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':id' => $_data['id'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); @@ -1734,7 +1718,6 @@ function verify_tfa_login($username, $_data) { } break; case "webauthn": - // prepare authenticator data $tokenData = json_decode($_data['token']); $clientDataJSON = base64_decode($tokenData->clientDataJSON); $authenticatorData = base64_decode($tokenData->authenticatorData); @@ -1742,28 +1725,10 @@ function verify_tfa_login($username, $_data) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - // just for debugging - // remove later - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, 'tfa_try_verify'), - 'msg' => 'try grab authenticator' - ); - - // fetch authenticator $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); - // just for debugging - // remove later - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, 'tfa_authenticator_grabbed'), - 'msg' => 'grabbed authenticator from db' - ); - - // return err if no authenticator was found if (empty($process_webauthn)){ $_SESSION['return'][] = array( 'type' => 'danger', @@ -1773,7 +1738,6 @@ function verify_tfa_login($username, $_data) { return false; } - // return err if authenticator has no publicKey if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1783,7 +1747,6 @@ function verify_tfa_login($username, $_data) { return false; } - // try verify authenticator try { $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); } @@ -1796,44 +1759,22 @@ function verify_tfa_login($username, $_data) { return false; } - // just for debugging - // remove later - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, 'tfa_progress'), - 'msg' => 'authenticator verified, check user role' - ); - - // if verified, check user role $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) { - // is admin $_SESSION["mailcow_cc_role"] = "admin"; } elseif ($obj_props['superadmin'] === 0) { - // is domainadmin $_SESSION["mailcow_cc_role"] = "domainadmin"; } else { - // just for debugging - // remove later - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, 'tfa_progress'), - 'msg' => 'no admin or domainadmin role, check if normal user' - ); - - // no admin, check if normal user $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'])) { - // is user $_SESSION["mailcow_cc_role"] = "user"; } else { - // err, no specific role found $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), @@ -1843,7 +1784,6 @@ function verify_tfa_login($username, $_data) { } } - // check if fetched user and pendig_user matches if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ $_SESSION['return'][] = array( 'type' => 'danger', @@ -1853,15 +1793,6 @@ function verify_tfa_login($username, $_data) { return false; } - // just for debugging - // remove later - $_SESSION['return'][] = array( - 'type' => 'info', - 'log' => array(__FUNCTION__, 'tfa_success'), - 'msg' => 'tfa flow success' - ); - - // set user session data and delete WebAuthn challenge session $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; From 3029a2d33d4b67ccabac8a7e6f96fba9ced05fff Mon Sep 17 00:00:00 2001 From: Niklas Meyer <62480600+DerLinkman@users.noreply.github.com> Date: Tue, 17 May 2022 15:26:01 +0200 Subject: [PATCH 16/49] Change DB Date to newer Date than staging --- data/web/inc/init_db.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 1e6b8133..be9078ff 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "02052022_1500"; + $db_version = "17052022_1525"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); From 3c9502f24114545b494fce65657f428dc7ea3772 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 17 May 2022 19:02:52 +0200 Subject: [PATCH 17/49] add webauthn console log --- data/web/templates/base.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index b51dcde2..945d4501 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -245,6 +245,7 @@ function recursiveBase64StrToArrayBuffer(obj) { window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { + console.log(json); if (json.success === false) throw new Error(); if (json.type === "error") throw new Error(json.msg); From 4ec982163edc014ec04c63927bebd6cc3e9c8bcf Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 18 May 2022 09:39:50 +0200 Subject: [PATCH 18/49] restrict webauthn-tfa-get-args sql query --- data/web/json_api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/json_api.php b/data/web/json_api.php index 4b0e294c..53e47af6 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -452,7 +452,7 @@ if (isset($_GET['query'])) { } break; case "webauthn-tfa-get-args": - $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); + $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = `webauthn`"); $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); if (count($rows) == 0) { From 7d5990bf0fff79411d9e783cb5182705d367f5cc Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 18 May 2022 10:03:10 +0200 Subject: [PATCH 19/49] restrict webauthn-tfa-get-args sql query --- data/web/json_api.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/web/json_api.php b/data/web/json_api.php index 53e47af6..2c8f13fb 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -452,8 +452,11 @@ if (isset($_GET['query'])) { } break; case "webauthn-tfa-get-args": - $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = `webauthn`"); - $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username'])); + $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech"); + $stmt->execute(array( + ':username' => $_SESSION['pending_mailcow_cc_username'], + ':authmech' => 'webauthn' + )); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); if (count($rows) == 0) { print(json_encode(array( From 0eb254577374963160770d9ab08759c6e76ac04d Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 7 Jun 2022 09:01:04 +0200 Subject: [PATCH 20/49] [WebAuthn] send empty transports array to fix android bug --- data/web/json_api.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/json_api.php b/data/web/json_api.php index 2c8f13fb..0ebc95bc 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -421,7 +421,7 @@ if (isset($_GET['query'])) { // } $ids = NULL; - $getArgs = $WebAuthn->getGetArgs($ids, 30, true, true, true, true, $GLOBALS['FIDO2_UV_FLAG_LOGIN']); + $getArgs = $WebAuthn->getGetArgs($ids, 30, false, false, false, false, $GLOBALS['FIDO2_UV_FLAG_LOGIN']); print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); return; @@ -469,7 +469,7 @@ if (isset($_GET['query'])) { $cids[] = base64_decode($row['keyHandle']); } - $getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); + $getArgs = $WebAuthn->getGetArgs($cids, 30, false, false, false, false, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); $getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId); print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); From 581be02e530496e17065d40550868d1e82e46c8f Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Tue, 14 Jun 2022 15:02:40 +0200 Subject: [PATCH 21/49] [Dovecot] Update to 2.3.19.1 --- data/Dockerfiles/dovecot/Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 4a914904..4e90052b 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -2,7 +2,7 @@ FROM debian:bullseye-slim LABEL maintainer "Andre Peters " ARG DEBIAN_FRONTEND=noninteractive -ARG DOVECOT=2.3.18 +ARG DOVECOT=2.3.19.1 ENV LC_ALL C ENV GOSU_VERSION 1.14 diff --git a/docker-compose.yml b/docker-compose.yml index e3b08637..09951cb3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -215,7 +215,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:1.162 + image: mailcow/dovecot:1.17 depends_on: - mysql-mailcow dns: From 3fe776ee6947401b6f42fd3678369b3f75a9ee50 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 14 Jun 2022 18:55:26 +0200 Subject: [PATCH 22/49] Update SOGo to 5.7.0 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index e3b08637..3208e88b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -168,7 +168,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.108 + image: mailcow/sogo:1.109 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From 537a7908f1a1a70c3ddadc4c4804a26b8e9bbfb4 Mon Sep 17 00:00:00 2001 From: Markus Ritzmann Date: Wed, 15 Jun 2022 15:49:29 +0200 Subject: [PATCH 23/49] Clamd: Fix Docker Healthcheck --- data/Dockerfiles/clamd/Dockerfile | 8 +++++++- data/Dockerfiles/clamd/healthcheck.sh | 9 +++++++++ docker-compose.yml | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100755 data/Dockerfiles/clamd/healthcheck.sh diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index a4971133..cba56e03 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -8,8 +8,14 @@ RUN apk upgrade --no-cache \ bind-tools \ bash -COPY clamd.sh ./ +# init +COPY clamd.sh /clamd.sh RUN chmod +x /sbin/tini +# healthcheck +COPY healthcheck.sh /healthcheck.sh +RUN chmod +x /healthcheck.sh +HEALTHCHECK --start-period=6m CMD "/healthcheck.sh" + ENTRYPOINT [] CMD ["/sbin/tini", "-g", "--", "/clamd.sh"] \ No newline at end of file diff --git a/data/Dockerfiles/clamd/healthcheck.sh b/data/Dockerfiles/clamd/healthcheck.sh new file mode 100755 index 00000000..6c18ac06 --- /dev/null +++ b/data/Dockerfiles/clamd/healthcheck.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "SKIP_CLAMD=y, skipping ClamAV..." + exit 0 +fi + +# run clamd healthcheck +/usr/local/bin/clamdcheck.sh diff --git a/docker-compose.yml b/docker-compose.yml index e3b08637..b8276411 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,7 @@ services: - redis clamd-mailcow: - image: mailcow/clamd:1.52 + image: mailcow/clamd:1.53 restart: always depends_on: - unbound-mailcow From 6fb967cf79209ab4e143fe24e9e5461993ba68be Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 4 Jul 2022 17:01:35 +0200 Subject: [PATCH 24/49] extra tfa register debugging --- data/web/templates/base.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 945d4501..37c27e21 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -415,6 +415,7 @@ function recursiveBase64StrToArrayBuffer(obj) { window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { + console.log(json); if (json.success === false) throw new Error(json.msg); recursiveBase64StrToArrayBuffer(json); From 52e92cc0db159f6e576401c4959a1240a7d945f8 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 4 Jul 2022 17:17:31 +0200 Subject: [PATCH 25/49] fix sql query for tfa registration --- data/web/json_api.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/web/json_api.php b/data/web/json_api.php index 0ebc95bc..22b747bc 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -430,8 +430,11 @@ if (isset($_GET['query'])) { case "webauthn-tfa-registration": if (isset($_SESSION["mailcow_cc_role"])) { // Exclude existing CredentialIds, if any - $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); - $stmt->execute(array(':username' => $_SESSION['mailcow_cc_username'])); + $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech"); + $stmt->execute(array( + ':username' => $_SESSION['mailcow_cc_username'], + ':authmech' => 'webauthn' + )); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { $excludeCredentialIds[] = base64_decode($row['keyHandle']); From 14bc105d4386e87ce40763a4d55ffd427b744239 Mon Sep 17 00:00:00 2001 From: Rafael Kraut Date: Tue, 5 Jul 2022 11:51:05 +0200 Subject: [PATCH 26/49] [Web] Remove default selection for sync job target mailbox (#4661) + Don't cache that form, closes #4642 --- data/web/templates/modals/mailbox.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/templates/modals/mailbox.twig b/data/web/templates/modals/mailbox.twig index 4097145f..17e06ed2 100644 --- a/data/web/templates/modals/mailbox.twig +++ b/data/web/templates/modals/mailbox.twig @@ -435,11 +435,11 @@