Make alias domains selectable in sender acl, a lot of code changes, added challenges for u2f to json_api, added U2F as TFA
parent
bd744ed91e
commit
badef73191
|
@ -14,32 +14,48 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
|
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
|
||||||
<?php $admindetails = get_admin_details(); ?>
|
<?php $admindetails = get_admin_details(); ?>
|
||||||
<input type="hidden" name="admin_user_now" value="<?=htmlspecialchars($admindetails['username']);?>">
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-sm-2" for="admin_user"><?=$lang['admin']['admin'];?>:</label>
|
<label class="control-label col-sm-3" for="admin_user"><?=$lang['admin']['admin'];?>:</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
<input type="text" class="form-control" name="admin_user" id="admin_user" value="<?=htmlspecialchars($admindetails['username']);?>" required>
|
<input type="text" class="form-control" name="admin_user" id="admin_user" value="<?=htmlspecialchars($admindetails['username']);?>" required>
|
||||||
↳ <kbd>a-z A-Z - _ .</kbd>
|
↳ <kbd>a-z A-Z - _ .</kbd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-sm-2" for="admin_pass"><?=$lang['admin']['password'];?>:</label>
|
<label class="control-label col-sm-3" for="admin_pass"><?=$lang['admin']['password'];?>:</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
<input type="password" class="form-control" name="admin_pass" id="admin_pass" placeholder="<?=$lang['admin']['unchanged_if_empty'];?>">
|
<input type="password" class="form-control" name="admin_pass" id="admin_pass" placeholder="<?=$lang['admin']['unchanged_if_empty'];?>">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-sm-2" for="admin_pass2"><?=$lang['admin']['password_repeat'];?>:</label>
|
<label class="control-label col-sm-3" for="admin_pass2"><?=$lang['admin']['password_repeat'];?>:</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
<input type="password" class="form-control" name="admin_pass2" id="admin_pass2">
|
<input type="password" class="form-control" name="admin_pass2" id="admin_pass2">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-3 col-sm-9">
|
||||||
<button type="submit" name="set_admin_account" class="btn btn-default"><?=$lang['admin']['save'];?></button>
|
<button type="submit" name="edit_admin_account" class="btn btn-default"><?=$lang['admin']['save'];?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?>:</div>
|
||||||
|
<div class="col-sm-9 col-xs-7">
|
||||||
|
<p><?=get_tfa()['pretty'];?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?>:</div>
|
||||||
|
<div class="col-md-9 col-xs-7">
|
||||||
|
<select data-width="auto" id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
|
||||||
|
<option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
|
||||||
|
<option value="u2f"><?=$lang['tfa']['u2f'];?></option>
|
||||||
|
<option value="none"><?=$lang['tfa']['none'];?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -56,6 +72,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
<th class="sort-table" style="min-width: 100px;"><?=$lang['admin']['username'];?></th>
|
<th class="sort-table" style="min-width: 100px;"><?=$lang['admin']['username'];?></th>
|
||||||
<th class="sort-table" style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th>
|
<th class="sort-table" style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th>
|
||||||
<th class="sort-table" style="min-width: 76px;"><?=$lang['admin']['active'];?></th>
|
<th class="sort-table" style="min-width: 76px;"><?=$lang['admin']['active'];?></th>
|
||||||
|
<th class="sort-table" style="min-width: 76px;"><?=$lang['tfa']['tfa'];?></th>
|
||||||
<th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['admin']['action'];?></th>
|
<th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['admin']['action'];?></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -75,6 +92,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
?>
|
?>
|
||||||
</td>
|
</td>
|
||||||
<td><?=$da_data['active'];?></td>
|
<td><?=$da_data['active'];?></td>
|
||||||
|
<td><?=empty($da_data['tfa_active_int']) ? "✘" : "✔";?></td>
|
||||||
<td style="text-align: right;">
|
<td style="text-align: right;">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a href="edit.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['admin']['edit'];?></a>
|
<a href="edit.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['admin']['edit'];?></a>
|
||||||
|
@ -266,7 +284,6 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /container -->
|
</div> <!-- /container -->
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" integrity="sha384-YWP9O4NjmcGo4oEJFXvvYSEzuHIvey+LbXkBNJ1Kd0yfugEZN9NCQNpRYBVC1RvA" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" integrity="sha384-YWP9O4NjmcGo4oEJFXvvYSEzuHIvey+LbXkBNJ1Kd0yfugEZN9NCQNpRYBVC1RvA" crossorigin="anonymous"></script>
|
||||||
<script src="js/sorttable.js"></script>
|
<script src="js/sorttable.js"></script>
|
||||||
<script src="js/admin.js"></script>
|
<script src="js/admin.js"></script>
|
||||||
|
|
|
@ -66,11 +66,11 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||||
<h4><?=$lang['edit']['domain_admin'];?></h4>
|
<h4><?=$lang['edit']['domain_admin'];?></h4>
|
||||||
<br />
|
<br />
|
||||||
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
|
<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
|
||||||
<input type="hidden" name="username" value="<?=htmlspecialchars($domain_admin);?>">
|
<input type="hidden" name="username_now" value="<?=htmlspecialchars($domain_admin);?>">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-sm-2" for="username"><?=$lang['edit']['username'];?></label>
|
<label class="control-label col-sm-2" for="username"><?=$lang['edit']['username'];?></label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input class="form-control" type="text" disabled value="<?=htmlspecialchars($domain_admin);?>" />
|
<input class="form-control" type="text" name="username" value="<?=htmlspecialchars($domain_admin);?>" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -113,7 +113,14 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<button type="submit" name="trigger_edit_domain_admin" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
|
<div class="checkbox">
|
||||||
|
<label><input type="checkbox" name="delete_tfa"> <?=$lang['tfa']['delete_tfa'];?></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" name="edit_domain_admin" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -575,7 +582,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] ==
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<button type="submit" name="trigger_edit_syncjob" value="1" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
|
<button type="submit" name="edit_syncjob" value="1" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -1,146 +0,0 @@
|
||||||
<?php
|
|
||||||
function get_admin_details() {
|
|
||||||
// No parameter to be given, only one admin should exist
|
|
||||||
global $pdo;
|
|
||||||
global $lang;
|
|
||||||
$data = array();
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != 'admin') {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin`WHERE `superadmin`='1' AND active='1'");
|
|
||||||
$stmt->execute();
|
|
||||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
function edit_admin($postarray) {
|
|
||||||
global $lang;
|
|
||||||
global $pdo;
|
|
||||||
$username = $postarray['username'];
|
|
||||||
$password = $postarray['password'];
|
|
||||||
$password2 = $postarray['password2'];
|
|
||||||
isset($postarray['active']) ? $active = '1' : $active = '0';
|
|
||||||
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($postarray['domain'])) {
|
|
||||||
foreach ($postarray['domain'] as $domain) {
|
|
||||||
if (!is_valid_domain_name($domain)) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['domain_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['username_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($postarray['domain'])) {
|
|
||||||
foreach ($postarray['domain'] as $domain) {
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
|
||||||
VALUES (:username, :domain, :created, :active)");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
':domain' => $domain,
|
|
||||||
':created' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($password) && !empty($password2)) {
|
|
||||||
if ($password != $password2) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['password_mismatch'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$password_hashed = hash_password($password);
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':password_hashed' => $password_hashed,
|
|
||||||
':username' => $username,
|
|
||||||
':modified' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
':modified' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,142 +0,0 @@
|
||||||
<?php
|
|
||||||
function dkim_add_key($postarray) {
|
|
||||||
global $lang;
|
|
||||||
global $pdo;
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
|
||||||
// $_SESSION['return'] = array(
|
|
||||||
// 'type' => 'danger',
|
|
||||||
// 'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
// );
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
$key_length = intval($postarray['key_size']);
|
|
||||||
$domain = $postarray['domain'];
|
|
||||||
if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty(glob($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'))) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = array(
|
|
||||||
"digest_alg" => "sha256",
|
|
||||||
"private_key_bits" => $key_length,
|
|
||||||
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
|
||||||
);
|
|
||||||
if ($keypair_ressource = openssl_pkey_new($config)) {
|
|
||||||
$key_details = openssl_pkey_get_details($keypair_ressource);
|
|
||||||
$pubKey = implode(array_slice(
|
|
||||||
array_filter(
|
|
||||||
explode(PHP_EOL, $key_details['key'])
|
|
||||||
), 1, -1)
|
|
||||||
);
|
|
||||||
// Save public key to file
|
|
||||||
file_put_contents($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim', $pubKey);
|
|
||||||
// Save private key to file
|
|
||||||
openssl_pkey_export_to_file($keypair_ressource, $GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim');
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'msg' => sprintf($lang['success']['dkim_added'])
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function dkim_get_key_details($domain) {
|
|
||||||
$data = array();
|
|
||||||
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
|
||||||
$dkim_pubkey_file = escapeshellarg($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
|
|
||||||
if (file_exists(substr($dkim_pubkey_file, 1, -1))) {
|
|
||||||
$data['pubkey'] = file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
|
|
||||||
$data['length'] = (strlen($data['pubkey']) < 391) ? 1024 : 2048;
|
|
||||||
$data['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
function dkim_get_blind_keys() {
|
|
||||||
global $lang;
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$domains = array();
|
|
||||||
$dnstxt_folder = scandir($GLOBALS["MC_DKIM_TXTS"]);
|
|
||||||
$dnstxt_files = array_diff($dnstxt_folder, array('.', '..'));
|
|
||||||
foreach($dnstxt_files as $file) {
|
|
||||||
$domains[] = substr($file, 0, -5);
|
|
||||||
}
|
|
||||||
return array_diff($domains, array_merge(mailbox_get_domains(), mailbox_get_alias_domains()));
|
|
||||||
}
|
|
||||||
function dkim_delete_key($postarray) {
|
|
||||||
global $lang;
|
|
||||||
$domain = $postarray['domain'];
|
|
||||||
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
|
||||||
// $_SESSION['return'] = array(
|
|
||||||
// 'type' => 'danger',
|
|
||||||
// 'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
// );
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
if (!is_valid_domain_name($domain)) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'), $out, $return);
|
|
||||||
if ($return != "0") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['dkim_remove_failed'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim'), $out, $return);
|
|
||||||
if ($return != "0") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['dkim_remove_failed'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'msg' => sprintf($lang['success']['dkim_removed'])
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
|
@ -1,381 +0,0 @@
|
||||||
<?php
|
|
||||||
function add_domain_admin($postarray) {
|
|
||||||
global $lang;
|
|
||||||
global $pdo;
|
|
||||||
$username = strtolower(trim($postarray['username']));
|
|
||||||
$password = $postarray['password'];
|
|
||||||
$password2 = $postarray['password2'];
|
|
||||||
isset($postarray['active']) ? $active = '1' : $active = '0';
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (empty($postarray['domain'])) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['domain_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['username_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
|
||||||
WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(':username' => $username));
|
|
||||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
|
|
||||||
WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(':username' => $username));
|
|
||||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
|
||||||
WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(':username' => $username));
|
|
||||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
foreach ($num_results as $num_results_each) {
|
|
||||||
if ($num_results_each != 0) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username))
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!empty($password) && !empty($password2)) {
|
|
||||||
if ($password != $password2) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['password_mismatch'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$password_hashed = hash_password($password);
|
|
||||||
foreach ($postarray['domain'] as $domain) {
|
|
||||||
if (!is_valid_domain_name($domain)) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['domain_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
|
||||||
VALUES (:username, :domain, :created, :active)");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
':domain' => $domain,
|
|
||||||
':created' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
delete_domain_admin(array('username' => $username));
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`)
|
|
||||||
VALUES (:username, :password_hashed, '0', :created, :modified, :active)");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
':password_hashed' => $password_hashed,
|
|
||||||
':created' => date('Y-m-d H:i:s'),
|
|
||||||
':modified' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['password_empty'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'msg' => sprintf($lang['success']['domain_admin_added'], htmlspecialchars($username))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function delete_domain_admin($postarray) {
|
|
||||||
global $pdo;
|
|
||||||
global $lang;
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$username = $postarray['username'];
|
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['username_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
));
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'msg' => sprintf($lang['success']['domain_admin_removed'], htmlspecialchars($username))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function get_domain_admins() {
|
|
||||||
global $pdo;
|
|
||||||
global $lang;
|
|
||||||
$domainadmins = array();
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT DISTINCT
|
|
||||||
`username`
|
|
||||||
FROM `domain_admins`
|
|
||||||
WHERE `username` IN (
|
|
||||||
SELECT `username` FROM `admin`
|
|
||||||
WHERE `superadmin`!='1'
|
|
||||||
)");
|
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
while ($row = array_shift($rows)) {
|
|
||||||
$domainadmins[] = $row['username'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return $domainadmins;
|
|
||||||
}
|
|
||||||
function get_domain_admin_details($domain_admin) {
|
|
||||||
global $pdo;
|
|
||||||
global $lang;
|
|
||||||
$domainadmindata = array();
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $domain_admin))) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['username_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT
|
|
||||||
`created`,
|
|
||||||
`active` AS `active_int`,
|
|
||||||
CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
|
|
||||||
FROM `domain_admins`
|
|
||||||
WHERE `username`= :domain_admin");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':domain_admin' => $domain_admin
|
|
||||||
));
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
$domainadmindata['active'] = $row['active'];
|
|
||||||
$domainadmindata['active_int'] = $row['active_int'];
|
|
||||||
$domainadmindata['created'] = $row['created'];
|
|
||||||
// GET SELECTED
|
|
||||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
|
||||||
WHERE `domain` IN (
|
|
||||||
SELECT `domain` FROM `domain_admins`
|
|
||||||
WHERE `username`= :domain_admin)");
|
|
||||||
$stmt->execute(array(':domain_admin' => $domain_admin));
|
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
while($row = array_shift($rows)) {
|
|
||||||
$domainadmindata['selected_domains'][] = $row['domain'];
|
|
||||||
}
|
|
||||||
// GET UNSELECTED
|
|
||||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
|
||||||
WHERE `domain` NOT IN (
|
|
||||||
SELECT `domain` FROM `domain_admins`
|
|
||||||
WHERE `username`= :domain_admin)");
|
|
||||||
$stmt->execute(array(':domain_admin' => $domain_admin));
|
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
while($row = array_shift($rows)) {
|
|
||||||
$domainadmindata['unselected_domains'][] = $row['domain'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return $domainadmindata;
|
|
||||||
}
|
|
||||||
function edit_domain_admin($postarray) {
|
|
||||||
global $lang;
|
|
||||||
global $pdo;
|
|
||||||
$username = $postarray['username'];
|
|
||||||
$password = $postarray['password'];
|
|
||||||
$password2 = $postarray['password2'];
|
|
||||||
isset($postarray['active']) ? $active = '1' : $active = '0';
|
|
||||||
|
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['access_denied'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($postarray['domain'])) {
|
|
||||||
foreach ($postarray['domain'] as $domain) {
|
|
||||||
if (!is_valid_domain_name($domain)) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['domain_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['username_invalid'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($postarray['domain'])) {
|
|
||||||
foreach ($postarray['domain'] as $domain) {
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
|
||||||
VALUES (:username, :domain, :created, :active)");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
':domain' => $domain,
|
|
||||||
':created' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($password) && !empty($password2)) {
|
|
||||||
if ($password != $password2) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => sprintf($lang['danger']['password_mismatch'])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$password_hashed = hash_password($password);
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':password_hashed' => $password_hashed,
|
|
||||||
':username' => $username,
|
|
||||||
':modified' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `active` = :active WHERE `username` = :username");
|
|
||||||
$stmt->execute(array(
|
|
||||||
':username' => $username,
|
|
||||||
':modified' => date('Y-m-d H:i:s'),
|
|
||||||
':active' => $active
|
|
||||||
));
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'MySQL: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$_SESSION['return'] = array(
|
|
||||||
'type' => 'success',
|
|
||||||
'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
include("inc/tfa_modals.php");
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin"):
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin"):
|
||||||
?>
|
?>
|
||||||
<div id="RestartSOGo" class="modal fade" role="dialog">
|
<div id="RestartSOGo" class="modal fade" role="dialog">
|
||||||
|
@ -26,6 +27,7 @@ endif;
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-switch/3.3.2/js/bootstrap-switch.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-switch/3.3.2/js/bootstrap-switch.min.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/7.0.2/bootstrap-slider.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/7.0.2/bootstrap-slider.min.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.9.4/js/bootstrap-select.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.9.4/js/bootstrap-select.js"></script>
|
||||||
|
<script src="/js/u2f-api.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// Select language and reopen active URL without POST
|
// Select language and reopen active URL without POST
|
||||||
function setLang(sel) {
|
function setLang(sel) {
|
||||||
|
@ -34,6 +36,90 @@ function setLang(sel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
// Confirm TFA modal
|
||||||
|
<?php if (isset($_SESSION['pending_tfa_method'])):?>
|
||||||
|
$('#ConfirmTFAModal').modal({
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: false
|
||||||
|
});
|
||||||
|
$('#ConfirmTFAModal').on('shown.bs.modal', function(){
|
||||||
|
$(this).find('#token').focus();
|
||||||
|
// If U2F
|
||||||
|
if(document.getElementById("u2f_auth_data") !== null) {
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
cache: false,
|
||||||
|
dataType: 'script',
|
||||||
|
url: "json_api.php",
|
||||||
|
data: {
|
||||||
|
'action':'get_u2f_auth_challenge',
|
||||||
|
'object':'<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>',
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTimeout(function() {
|
||||||
|
console.log("sign: ", req);
|
||||||
|
u2f.sign(req, function(data) {
|
||||||
|
var form = document.getElementById('u2f_auth_form');
|
||||||
|
var auth = document.getElementById('u2f_auth_data');
|
||||||
|
console.log("Authenticate callback", data);
|
||||||
|
auth.value = JSON.stringify(data);
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
// Set TFA modals
|
||||||
|
$('#selectTFA').change(function () {
|
||||||
|
if ($(this).val() == "yubi_otp") {
|
||||||
|
$('#YubiOTPModal').modal('show');
|
||||||
|
$("option:selected").prop("selected", false);
|
||||||
|
}
|
||||||
|
if ($(this).val() == "u2f") {
|
||||||
|
$('#U2FModal').modal('show');
|
||||||
|
$("option:selected").prop("selected", false);
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
cache: false,
|
||||||
|
dataType: 'script',
|
||||||
|
url: "json_api.php",
|
||||||
|
data: {
|
||||||
|
'action':'get_u2f_reg_challenge',
|
||||||
|
'object':'<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>',
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTimeout(function() {
|
||||||
|
console.log("Register: ", req);
|
||||||
|
u2f.register([req], sigs, function(data) {
|
||||||
|
var form = document.getElementById('u2f_reg_form');
|
||||||
|
var reg = document.getElementById('u2f_register_data');
|
||||||
|
console.log("Register callback", data);
|
||||||
|
if (data.errorCode && data.errorCode != 0) {
|
||||||
|
var u2f_return_code = document.getElementById('u2f_return_code');
|
||||||
|
u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
|
||||||
|
if (data.errorCode == "4") { data.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; }
|
||||||
|
u2f_return_code.innerHTML = 'Error code: ' + data.errorCode;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reg.value = JSON.stringify(data);
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
if ($(this).val() == "none") {
|
||||||
|
$('#DisableTFAModal').modal('show');
|
||||||
|
$("option:selected").prop("selected", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate tooltips
|
||||||
$(function () {
|
$(function () {
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -68,7 +68,7 @@
|
||||||
<li <?=(preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/mailbox.php"><?=$lang['header']['mailboxes'];?></a></li>
|
<li <?=(preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/mailbox.php"><?=$lang['header']['mailboxes'];?></a></li>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
if ($_SESSION['mailcow_cc_role'] == "user") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
?>
|
?>
|
||||||
<li <?=(preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/user.php"><?=$lang['header']['user_settings'];?></a></li>
|
<li <?=(preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/user.php"><?=$lang['header']['user_settings'];?></a></li>
|
||||||
<?php
|
<?php
|
||||||
|
|
|
@ -127,6 +127,19 @@ CREATE TABLE IF NOT EXISTS `imapsync` (
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `tfa` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(255) NOT NULL,
|
||||||
|
`authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp'),
|
||||||
|
`secret` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`keyHandle` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`publicKey` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`counter` INT NOT NULL DEFAULT '0',
|
||||||
|
`certificate` TEXT,
|
||||||
|
`active` TINYINT(1) NOT NULL DEFAULT '0',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
DROP VIEW IF EXISTS grouped_mail_aliases;
|
DROP VIEW IF EXISTS grouped_mail_aliases;
|
||||||
DROP VIEW IF EXISTS grouped_sender_acl;
|
DROP VIEW IF EXISTS grouped_sender_acl;
|
||||||
DROP VIEW IF EXISTS grouped_domain_alias_address;
|
DROP VIEW IF EXISTS grouped_domain_alias_address;
|
||||||
|
@ -148,7 +161,7 @@ SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '
|
||||||
LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;
|
LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS sogo_acl (
|
CREATE TABLE IF NOT EXISTS sogo_acl (
|
||||||
c_folder_id INTeger NOT NULL,
|
c_folder_id INTEGER NOT NULL,
|
||||||
c_object character varying(255) NOT NULL,
|
c_object character varying(255) NOT NULL,
|
||||||
c_uid character varying(255) NOT NULL,
|
c_uid character varying(255) NOT NULL,
|
||||||
c_role character varying(80) NOT NULL
|
c_role character varying(80) NOT NULL
|
||||||
|
|
|
@ -0,0 +1,506 @@
|
||||||
|
<?php
|
||||||
|
/* Copyright (c) 2014 Yubico AB
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are
|
||||||
|
* met:
|
||||||
|
*
|
||||||
|
* * Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* * Redistributions in binary form must reproduce the above
|
||||||
|
* copyright notice, this list of conditions and the following
|
||||||
|
* disclaimer in the documentation and/or other materials provided
|
||||||
|
* with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace u2flib_server;
|
||||||
|
|
||||||
|
/** Constant for the version of the u2f protocol */
|
||||||
|
const U2F_VERSION = "U2F_V2";
|
||||||
|
|
||||||
|
/** Error for the authentication message not matching any outstanding
|
||||||
|
* authentication request */
|
||||||
|
const ERR_NO_MATCHING_REQUEST = 1;
|
||||||
|
|
||||||
|
/** Error for the authentication message not matching any registration */
|
||||||
|
const ERR_NO_MATCHING_REGISTRATION = 2;
|
||||||
|
|
||||||
|
/** Error for the signature on the authentication message not verifying with
|
||||||
|
* the correct key */
|
||||||
|
const ERR_AUTHENTICATION_FAILURE = 3;
|
||||||
|
|
||||||
|
/** Error for the challenge in the registration message not matching the
|
||||||
|
* registration challenge */
|
||||||
|
const ERR_UNMATCHED_CHALLENGE = 4;
|
||||||
|
|
||||||
|
/** Error for the attestation signature on the registration message not
|
||||||
|
* verifying */
|
||||||
|
const ERR_ATTESTATION_SIGNATURE = 5;
|
||||||
|
|
||||||
|
/** Error for the attestation verification not verifying */
|
||||||
|
const ERR_ATTESTATION_VERIFICATION = 6;
|
||||||
|
|
||||||
|
/** Error for not getting good random from the system */
|
||||||
|
const ERR_BAD_RANDOM = 7;
|
||||||
|
|
||||||
|
/** Error when the counter is lower than expected */
|
||||||
|
const ERR_COUNTER_TOO_LOW = 8;
|
||||||
|
|
||||||
|
/** Error decoding public key */
|
||||||
|
const ERR_PUBKEY_DECODE = 9;
|
||||||
|
|
||||||
|
/** Error user-agent returned error */
|
||||||
|
const ERR_BAD_UA_RETURNING = 10;
|
||||||
|
|
||||||
|
/** Error old OpenSSL version */
|
||||||
|
const ERR_OLD_OPENSSL = 11;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
const PUBKEY_LEN = 65;
|
||||||
|
|
||||||
|
class U2F
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
private $appId;
|
||||||
|
|
||||||
|
/** @var null|string */
|
||||||
|
private $attestDir;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
private $FIXCERTS = array(
|
||||||
|
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
|
||||||
|
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
|
||||||
|
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
|
||||||
|
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
|
||||||
|
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
|
||||||
|
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $appId Application id for the running application
|
||||||
|
* @param string|null $attestDir Directory where trusted attestation roots may be found
|
||||||
|
* @throws Error If OpenSSL older than 1.0.0 is used
|
||||||
|
*/
|
||||||
|
public function __construct($appId, $attestDir = null)
|
||||||
|
{
|
||||||
|
if(OPENSSL_VERSION_NUMBER < 0x10000000) {
|
||||||
|
throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
|
||||||
|
}
|
||||||
|
$this->appId = $appId;
|
||||||
|
$this->attestDir = $attestDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to get a registration request to send to a user.
|
||||||
|
* Returns an array of one registration request and a array of sign requests.
|
||||||
|
*
|
||||||
|
* @param array $registrations List of current registrations for this
|
||||||
|
* user, to prevent the user from registering the same authenticator several
|
||||||
|
* times.
|
||||||
|
* @return array An array of two elements, the first containing a
|
||||||
|
* RegisterRequest the second being an array of SignRequest
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public function getRegisterData(array $registrations = array())
|
||||||
|
{
|
||||||
|
$challenge = $this->createChallenge();
|
||||||
|
$request = new RegisterRequest($challenge, $this->appId);
|
||||||
|
$signs = $this->getAuthenticateData($registrations);
|
||||||
|
return array($request, $signs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to verify and unpack a registration message.
|
||||||
|
*
|
||||||
|
* @param RegisterRequest $request this is a reply to
|
||||||
|
* @param object $response response from a user
|
||||||
|
* @param bool $includeCert set to true if the attestation certificate should be
|
||||||
|
* included in the returned Registration object
|
||||||
|
* @return Registration
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public function doRegister($request, $response, $includeCert = true)
|
||||||
|
{
|
||||||
|
if( !is_object( $request ) ) {
|
||||||
|
throw new \InvalidArgumentException('$request of doRegister() method only accepts object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !is_object( $response ) ) {
|
||||||
|
throw new \InvalidArgumentException('$response of doRegister() method only accepts object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
|
||||||
|
throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !is_bool( $includeCert ) ) {
|
||||||
|
throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawReg = $this->base64u_decode($response->registrationData);
|
||||||
|
$regData = array_values(unpack('C*', $rawReg));
|
||||||
|
$clientData = $this->base64u_decode($response->clientData);
|
||||||
|
$cli = json_decode($clientData);
|
||||||
|
|
||||||
|
if($cli->challenge !== $request->challenge) {
|
||||||
|
throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE );
|
||||||
|
}
|
||||||
|
|
||||||
|
$registration = new Registration();
|
||||||
|
$offs = 1;
|
||||||
|
$pubKey = substr($rawReg, $offs, PUBKEY_LEN);
|
||||||
|
$offs += PUBKEY_LEN;
|
||||||
|
// decode the pubKey to make sure it's good
|
||||||
|
$tmpKey = $this->pubkey_to_pem($pubKey);
|
||||||
|
if($tmpKey === null) {
|
||||||
|
throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
|
||||||
|
}
|
||||||
|
$registration->publicKey = base64_encode($pubKey);
|
||||||
|
$khLen = $regData[$offs++];
|
||||||
|
$kh = substr($rawReg, $offs, $khLen);
|
||||||
|
$offs += $khLen;
|
||||||
|
$registration->keyHandle = $this->base64u_encode($kh);
|
||||||
|
|
||||||
|
// length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes)
|
||||||
|
$certLen = 4;
|
||||||
|
$certLen += ($regData[$offs + 2] << 8);
|
||||||
|
$certLen += $regData[$offs + 3];
|
||||||
|
|
||||||
|
$rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen));
|
||||||
|
$offs += $certLen;
|
||||||
|
$pemCert = "-----BEGIN CERTIFICATE-----\r\n";
|
||||||
|
$pemCert .= chunk_split(base64_encode($rawCert), 64);
|
||||||
|
$pemCert .= "-----END CERTIFICATE-----";
|
||||||
|
if($includeCert) {
|
||||||
|
$registration->certificate = base64_encode($rawCert);
|
||||||
|
}
|
||||||
|
if($this->attestDir) {
|
||||||
|
if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
|
||||||
|
throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!openssl_pkey_get_public($pemCert)) {
|
||||||
|
throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
|
||||||
|
}
|
||||||
|
$signature = substr($rawReg, $offs);
|
||||||
|
|
||||||
|
$dataToVerify = chr(0);
|
||||||
|
$dataToVerify .= hash('sha256', $request->appId, true);
|
||||||
|
$dataToVerify .= hash('sha256', $clientData, true);
|
||||||
|
$dataToVerify .= $kh;
|
||||||
|
$dataToVerify .= $pubKey;
|
||||||
|
|
||||||
|
if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) {
|
||||||
|
return $registration;
|
||||||
|
} else {
|
||||||
|
throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to get an authentication request.
|
||||||
|
*
|
||||||
|
* @param array $registrations An array of the registrations to create authentication requests for.
|
||||||
|
* @return array An array of SignRequest
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public function getAuthenticateData(array $registrations)
|
||||||
|
{
|
||||||
|
$sigs = array();
|
||||||
|
foreach ($registrations as $reg) {
|
||||||
|
if( !is_object( $reg ) ) {
|
||||||
|
throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sig = new SignRequest();
|
||||||
|
$sig->appId = $this->appId;
|
||||||
|
$sig->keyHandle = $reg->keyHandle;
|
||||||
|
$sig->challenge = $this->createChallenge();
|
||||||
|
$sigs[] = $sig;
|
||||||
|
}
|
||||||
|
return $sigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to verify an authentication response
|
||||||
|
*
|
||||||
|
* @param array $requests An array of outstanding authentication requests
|
||||||
|
* @param array $registrations An array of current registrations
|
||||||
|
* @param object $response A response from the authenticator
|
||||||
|
* @return Registration
|
||||||
|
* @throws Error
|
||||||
|
*
|
||||||
|
* The Registration object returned on success contains an updated counter
|
||||||
|
* that should be saved for future authentications.
|
||||||
|
* If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of
|
||||||
|
* token cloning or similar and appropriate action should be taken.
|
||||||
|
*/
|
||||||
|
public function doAuthenticate(array $requests, array $registrations, $response)
|
||||||
|
{
|
||||||
|
if( !is_object( $response ) ) {
|
||||||
|
throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
|
||||||
|
throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var object|null $req */
|
||||||
|
$req = null;
|
||||||
|
|
||||||
|
/** @var object|null $reg */
|
||||||
|
$reg = null;
|
||||||
|
|
||||||
|
$clientData = $this->base64u_decode($response->clientData);
|
||||||
|
$decodedClient = json_decode($clientData);
|
||||||
|
foreach ($requests as $req) {
|
||||||
|
if( !is_object( $req ) ) {
|
||||||
|
throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$req = null;
|
||||||
|
}
|
||||||
|
if($req === null) {
|
||||||
|
throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST );
|
||||||
|
}
|
||||||
|
foreach ($registrations as $reg) {
|
||||||
|
if( !is_object( $reg ) ) {
|
||||||
|
throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($reg->keyHandle === $response->keyHandle) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$reg = null;
|
||||||
|
}
|
||||||
|
if($reg === null) {
|
||||||
|
throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION );
|
||||||
|
}
|
||||||
|
$pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey));
|
||||||
|
if($pemKey === null) {
|
||||||
|
throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
|
||||||
|
}
|
||||||
|
|
||||||
|
$signData = $this->base64u_decode($response->signatureData);
|
||||||
|
$dataToVerify = hash('sha256', $req->appId, true);
|
||||||
|
$dataToVerify .= substr($signData, 0, 5);
|
||||||
|
$dataToVerify .= hash('sha256', $clientData, true);
|
||||||
|
$signature = substr($signData, 5);
|
||||||
|
|
||||||
|
if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
|
||||||
|
$ctr = unpack("Nctr", substr($signData, 1, 4));
|
||||||
|
$counter = $ctr['ctr'];
|
||||||
|
/* TODO: wrap-around should be handled somehow.. */
|
||||||
|
if($counter > $reg->counter) {
|
||||||
|
$reg->counter = $counter;
|
||||||
|
return $reg;
|
||||||
|
} else {
|
||||||
|
throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function get_certs()
|
||||||
|
{
|
||||||
|
$files = array();
|
||||||
|
$dir = $this->attestDir;
|
||||||
|
if($dir && $handle = opendir($dir)) {
|
||||||
|
while(false !== ($entry = readdir($handle))) {
|
||||||
|
if(is_file("$dir/$entry")) {
|
||||||
|
$files[] = "$dir/$entry";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir($handle);
|
||||||
|
}
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function base64u_encode($data)
|
||||||
|
{
|
||||||
|
return trim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function base64u_decode($data)
|
||||||
|
{
|
||||||
|
return base64_decode(strtr($data, '-_', '+/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @return null|string
|
||||||
|
*/
|
||||||
|
private function pubkey_to_pem($key)
|
||||||
|
{
|
||||||
|
if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert the public key to binary DER format first
|
||||||
|
* Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480
|
||||||
|
*
|
||||||
|
* SEQUENCE(2 elem) 30 59
|
||||||
|
* SEQUENCE(2 elem) 30 13
|
||||||
|
* OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01
|
||||||
|
* OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07
|
||||||
|
* BIT STRING(520 bit) 03 42 ..key..
|
||||||
|
*/
|
||||||
|
$der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
|
||||||
|
$der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
|
||||||
|
$der .= "\0".$key;
|
||||||
|
|
||||||
|
$pem = "-----BEGIN PUBLIC KEY-----\r\n";
|
||||||
|
$pem .= chunk_split(base64_encode($der), 64);
|
||||||
|
$pem .= "-----END PUBLIC KEY-----";
|
||||||
|
|
||||||
|
return $pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
private function createChallenge()
|
||||||
|
{
|
||||||
|
$challenge = openssl_random_pseudo_bytes(32, $crypto_strong );
|
||||||
|
if( $crypto_strong !== true ) {
|
||||||
|
throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
$challenge = $this->base64u_encode( $challenge );
|
||||||
|
|
||||||
|
return $challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes a certificate where the signature contains unused bits.
|
||||||
|
*
|
||||||
|
* @param string $cert
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private function fixSignatureUnusedBits($cert)
|
||||||
|
{
|
||||||
|
if(in_array(hash('sha256', $cert), $this->FIXCERTS)) {
|
||||||
|
$cert[strlen($cert) - 257] = "\0";
|
||||||
|
}
|
||||||
|
return $cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for building a registration request
|
||||||
|
*
|
||||||
|
* @package u2flib_server
|
||||||
|
*/
|
||||||
|
class RegisterRequest
|
||||||
|
{
|
||||||
|
/** Protocol version */
|
||||||
|
public $version = U2F_VERSION;
|
||||||
|
|
||||||
|
/** Registration challenge */
|
||||||
|
public $challenge;
|
||||||
|
|
||||||
|
/** Application id */
|
||||||
|
public $appId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $challenge
|
||||||
|
* @param string $appId
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct($challenge, $appId)
|
||||||
|
{
|
||||||
|
$this->challenge = $challenge;
|
||||||
|
$this->appId = $appId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for building up an authentication request
|
||||||
|
*
|
||||||
|
* @package u2flib_server
|
||||||
|
*/
|
||||||
|
class SignRequest
|
||||||
|
{
|
||||||
|
/** Protocol version */
|
||||||
|
public $version = U2F_VERSION;
|
||||||
|
|
||||||
|
/** Authentication challenge */
|
||||||
|
public $challenge;
|
||||||
|
|
||||||
|
/** Key handle of a registered authenticator */
|
||||||
|
public $keyHandle;
|
||||||
|
|
||||||
|
/** Application id */
|
||||||
|
public $appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class returned for successful registrations
|
||||||
|
*
|
||||||
|
* @package u2flib_server
|
||||||
|
*/
|
||||||
|
class Registration
|
||||||
|
{
|
||||||
|
/** The key handle of the registered authenticator */
|
||||||
|
public $keyHandle;
|
||||||
|
|
||||||
|
/** The public key of the registered authenticator */
|
||||||
|
public $publicKey;
|
||||||
|
|
||||||
|
/** The attestation certificate of the registered authenticator */
|
||||||
|
public $certificate;
|
||||||
|
|
||||||
|
/** The counter associated with this registration */
|
||||||
|
public $counter = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class, returned on errors
|
||||||
|
*
|
||||||
|
* @package u2flib_server
|
||||||
|
*/
|
||||||
|
class Error extends \Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Override constructor and make message and code mandatory
|
||||||
|
* @param string $message
|
||||||
|
* @param int $code
|
||||||
|
* @param \Exception|null $previous
|
||||||
|
*/
|
||||||
|
public function __construct($message, $code, \Exception $previous = null) {
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,475 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Class for verifying Yubico One-Time-Passcodes
|
||||||
|
*
|
||||||
|
* @category Auth
|
||||||
|
* @package Auth_Yubico
|
||||||
|
* @author Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>
|
||||||
|
* @copyright 2007-2015 Yubico AB
|
||||||
|
* @license http://opensource.org/licenses/bsd-license.php New BSD License
|
||||||
|
* @version 2.0
|
||||||
|
* @link http://www.yubico.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once 'PEAR.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for verifying Yubico One-Time-Passcodes
|
||||||
|
*
|
||||||
|
* Simple example:
|
||||||
|
* <code>
|
||||||
|
* require_once 'Auth/Yubico.php';
|
||||||
|
* $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
|
||||||
|
*
|
||||||
|
* # Generate a new id+key from https://api.yubico.com/get-api-key/
|
||||||
|
* $yubi = new Auth_Yubico('42', 'FOOBAR=');
|
||||||
|
* $auth = $yubi->verify($otp);
|
||||||
|
* if (PEAR::isError($auth)) {
|
||||||
|
* print "<p>Authentication failed: " . $auth->getMessage();
|
||||||
|
* print "<p>Debug output from server: " . $yubi->getLastResponse();
|
||||||
|
* } else {
|
||||||
|
* print "<p>You are authenticated!";
|
||||||
|
* }
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
class Auth_Yubico
|
||||||
|
{
|
||||||
|
/**#@+
|
||||||
|
* @access private
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yubico client ID
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
var $_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yubico client key
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
var $_key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL part of validation server
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
var $_url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List with URL part of validation servers
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
var $_url_list;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* index to _url_list
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
var $_url_index;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last query to server
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
var $_lastquery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from server
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
var $_response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag whether to use https or not.
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
var $_https;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag whether to verify HTTPS server certificates or not.
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
var $_httpsverify;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* Sets up the object
|
||||||
|
* @param string $id The client identity
|
||||||
|
* @param string $key The client MAC key (optional)
|
||||||
|
* @param boolean $https Flag whether to use https (optional)
|
||||||
|
* @param boolean $httpsverify Flag whether to use verify HTTPS
|
||||||
|
* server certificates (optional,
|
||||||
|
* default true)
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function __construct($id, $key = '', $https = 0, $httpsverify = 1)
|
||||||
|
{
|
||||||
|
$this->_id = $id;
|
||||||
|
$this->_key = base64_decode($key);
|
||||||
|
$this->_https = $https;
|
||||||
|
$this->_httpsverify = $httpsverify;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Auth_Yubico($id, $key = '', $https = 0, $httpsverify = 1)
|
||||||
|
{
|
||||||
|
self::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify to use a different URL part for verification.
|
||||||
|
* The default is "api.yubico.com/wsapi/verify".
|
||||||
|
*
|
||||||
|
* @param string $url New server URL part to use
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function setURLpart($url)
|
||||||
|
{
|
||||||
|
$this->_url = $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get URL part to use for validation.
|
||||||
|
*
|
||||||
|
* @return string Server URL part
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function getURLpart()
|
||||||
|
{
|
||||||
|
if ($this->_url) {
|
||||||
|
return $this->_url;
|
||||||
|
} else {
|
||||||
|
return "api.yubico.com/wsapi/verify";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get next URL part from list to use for validation.
|
||||||
|
*
|
||||||
|
* @return mixed string with URL part of false if no more URLs in list
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function getNextURLpart()
|
||||||
|
{
|
||||||
|
if ($this->_url_list) $url_list=$this->_url_list;
|
||||||
|
else $url_list=array('api.yubico.com/wsapi/2.0/verify',
|
||||||
|
'api2.yubico.com/wsapi/2.0/verify',
|
||||||
|
'api3.yubico.com/wsapi/2.0/verify',
|
||||||
|
'api4.yubico.com/wsapi/2.0/verify',
|
||||||
|
'api5.yubico.com/wsapi/2.0/verify');
|
||||||
|
|
||||||
|
if ($this->_url_index>=count($url_list)) return false;
|
||||||
|
else return $url_list[$this->_url_index++];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets index to URL list
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function URLreset()
|
||||||
|
{
|
||||||
|
$this->_url_index=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add another URLpart.
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function addURLpart($URLpart)
|
||||||
|
{
|
||||||
|
$this->_url_list[]=$URLpart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the last query sent to the server, if any.
|
||||||
|
*
|
||||||
|
* @return string Request to server
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function getLastQuery()
|
||||||
|
{
|
||||||
|
return $this->_lastquery;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the last data received from the server, if any.
|
||||||
|
*
|
||||||
|
* @return string Output from server
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function getLastResponse()
|
||||||
|
{
|
||||||
|
return $this->_response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse input string into password, yubikey prefix,
|
||||||
|
* ciphertext, and OTP.
|
||||||
|
*
|
||||||
|
* @param string Input string to parse
|
||||||
|
* @param string Optional delimiter re-class, default is '[:]'
|
||||||
|
* @return array Keyed array with fields
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function parsePasswordOTP($str, $delim = '[:]')
|
||||||
|
{
|
||||||
|
if (!preg_match("/^((.*)" . $delim . ")?" .
|
||||||
|
"(([cbdefghijklnrtuv]{0,16})" .
|
||||||
|
"([cbdefghijklnrtuv]{32}))$/i",
|
||||||
|
$str, $matches)) {
|
||||||
|
/* Dvorak? */
|
||||||
|
if (!preg_match("/^((.*)" . $delim . ")?" .
|
||||||
|
"(([jxe\.uidchtnbpygk]{0,16})" .
|
||||||
|
"([jxe\.uidchtnbpygk]{32}))$/i",
|
||||||
|
$str, $matches)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
$ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$ret['otp'] = $matches[3];
|
||||||
|
}
|
||||||
|
$ret['password'] = $matches[2];
|
||||||
|
$ret['prefix'] = $matches[4];
|
||||||
|
$ret['ciphertext'] = $matches[5];
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO? Add functions to get parsed parts of server response? */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse parameters from last response
|
||||||
|
*
|
||||||
|
* example: getParameters("timestamp", "sessioncounter", "sessionuse");
|
||||||
|
*
|
||||||
|
* @param array @parameters Array with strings representing
|
||||||
|
* parameters to parse
|
||||||
|
* @return array parameter array from last response
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function getParameters($parameters)
|
||||||
|
{
|
||||||
|
if ($parameters == null) {
|
||||||
|
$parameters = array('timestamp', 'sessioncounter', 'sessionuse');
|
||||||
|
}
|
||||||
|
$param_array = array();
|
||||||
|
foreach ($parameters as $param) {
|
||||||
|
if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
|
||||||
|
return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
|
||||||
|
}
|
||||||
|
$param_array[$param]=$out[1];
|
||||||
|
}
|
||||||
|
return $param_array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify Yubico OTP against multiple URLs
|
||||||
|
* Protocol specification 2.0 is used to construct validation requests
|
||||||
|
*
|
||||||
|
* @param string $token Yubico OTP
|
||||||
|
* @param int $use_timestamp 1=>send request with ×tamp=1 to
|
||||||
|
* get timestamp and session information
|
||||||
|
* in the response
|
||||||
|
* @param boolean $wait_for_all If true, wait until all
|
||||||
|
* servers responds (for debugging)
|
||||||
|
* @param string $sl Sync level in percentage between 0
|
||||||
|
* and 100 or "fast" or "secure".
|
||||||
|
* @param int $timeout Max number of seconds to wait
|
||||||
|
* for responses
|
||||||
|
* @return mixed PEAR error on error, true otherwise
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
function verify($token, $use_timestamp=null, $wait_for_all=False,
|
||||||
|
$sl=null, $timeout=null)
|
||||||
|
{
|
||||||
|
/* Construct parameters string */
|
||||||
|
$ret = $this->parsePasswordOTP($token);
|
||||||
|
if (!$ret) {
|
||||||
|
return PEAR::raiseError('Could not parse Yubikey OTP');
|
||||||
|
}
|
||||||
|
$params = array('id'=>$this->_id,
|
||||||
|
'otp'=>$ret['otp'],
|
||||||
|
'nonce'=>md5(uniqid(rand())));
|
||||||
|
/* Take care of protocol version 2 parameters */
|
||||||
|
if ($use_timestamp) $params['timestamp'] = 1;
|
||||||
|
if ($sl) $params['sl'] = $sl;
|
||||||
|
if ($timeout) $params['timeout'] = $timeout;
|
||||||
|
ksort($params);
|
||||||
|
$parameters = '';
|
||||||
|
foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
|
||||||
|
$parameters = ltrim($parameters, "&");
|
||||||
|
|
||||||
|
/* Generate signature. */
|
||||||
|
if($this->_key <> "") {
|
||||||
|
$signature = base64_encode(hash_hmac('sha1', $parameters,
|
||||||
|
$this->_key, true));
|
||||||
|
$signature = preg_replace('/\+/', '%2B', $signature);
|
||||||
|
$parameters .= '&h=' . $signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate and prepare request. */
|
||||||
|
$this->_lastquery=null;
|
||||||
|
$this->URLreset();
|
||||||
|
$mh = curl_multi_init();
|
||||||
|
$ch = array();
|
||||||
|
while($URLpart=$this->getNextURLpart())
|
||||||
|
{
|
||||||
|
/* Support https. */
|
||||||
|
if ($this->_https) {
|
||||||
|
$query = "https://";
|
||||||
|
} else {
|
||||||
|
$query = "http://";
|
||||||
|
}
|
||||||
|
$query .= $URLpart . "?" . $parameters;
|
||||||
|
|
||||||
|
if ($this->_lastquery) { $this->_lastquery .= " "; }
|
||||||
|
$this->_lastquery .= $query;
|
||||||
|
|
||||||
|
$handle = curl_init($query);
|
||||||
|
curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
|
||||||
|
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
if (!$this->_httpsverify) {
|
||||||
|
curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
|
||||||
|
curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
|
||||||
|
}
|
||||||
|
curl_setopt($handle, CURLOPT_FAILONERROR, true);
|
||||||
|
/* If timeout is set, we better apply it here as well
|
||||||
|
in case the validation server fails to follow it.
|
||||||
|
*/
|
||||||
|
if ($timeout) curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
|
||||||
|
curl_multi_add_handle($mh, $handle);
|
||||||
|
|
||||||
|
$ch[(int)$handle] = $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute and read request. */
|
||||||
|
$this->_response=null;
|
||||||
|
$replay=False;
|
||||||
|
$valid=False;
|
||||||
|
do {
|
||||||
|
/* Let curl do its work. */
|
||||||
|
while (($mrc = curl_multi_exec($mh, $active))
|
||||||
|
== CURLM_CALL_MULTI_PERFORM)
|
||||||
|
;
|
||||||
|
|
||||||
|
while ($info = curl_multi_info_read($mh)) {
|
||||||
|
if ($info['result'] == CURLE_OK) {
|
||||||
|
|
||||||
|
/* We have a complete response from one server. */
|
||||||
|
|
||||||
|
$str = curl_multi_getcontent($info['handle']);
|
||||||
|
$cinfo = curl_getinfo ($info['handle']);
|
||||||
|
|
||||||
|
if ($wait_for_all) { # Better debug info
|
||||||
|
$this->_response .= 'URL=' . $cinfo['url'] ."\n"
|
||||||
|
. $str . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
|
||||||
|
$status = $out[1];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* There are 3 cases.
|
||||||
|
*
|
||||||
|
* 1. OTP or Nonce values doesn't match - ignore
|
||||||
|
* response.
|
||||||
|
*
|
||||||
|
* 2. We have a HMAC key. If signature is invalid -
|
||||||
|
* ignore response. Return if status=OK or
|
||||||
|
* status=REPLAYED_OTP.
|
||||||
|
*
|
||||||
|
* 3. Return if status=OK or status=REPLAYED_OTP.
|
||||||
|
*/
|
||||||
|
if (!preg_match("/otp=".$params['otp']."/", $str) ||
|
||||||
|
!preg_match("/nonce=".$params['nonce']."/", $str)) {
|
||||||
|
/* Case 1. Ignore response. */
|
||||||
|
}
|
||||||
|
elseif ($this->_key <> "") {
|
||||||
|
/* Case 2. Verify signature first */
|
||||||
|
$rows = explode("\r\n", trim($str));
|
||||||
|
$response=array();
|
||||||
|
while (list($key, $val) = each($rows)) {
|
||||||
|
/* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
|
||||||
|
$val = preg_replace('/=/', '#', $val, 1);
|
||||||
|
$row = explode("#", $val);
|
||||||
|
$response[$row[0]] = $row[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
|
||||||
|
sort($parameters);
|
||||||
|
$check=Null;
|
||||||
|
foreach ($parameters as $param) {
|
||||||
|
if (array_key_exists($param, $response)) {
|
||||||
|
if ($check) $check = $check . '&';
|
||||||
|
$check = $check . $param . '=' . $response[$param];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$checksignature =
|
||||||
|
base64_encode(hash_hmac('sha1', utf8_encode($check),
|
||||||
|
$this->_key, true));
|
||||||
|
|
||||||
|
if($response['h'] == $checksignature) {
|
||||||
|
if ($status == 'REPLAYED_OTP') {
|
||||||
|
if (!$wait_for_all) { $this->_response = $str; }
|
||||||
|
$replay=True;
|
||||||
|
}
|
||||||
|
if ($status == 'OK') {
|
||||||
|
if (!$wait_for_all) { $this->_response = $str; }
|
||||||
|
$valid=True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Case 3. We check the status directly */
|
||||||
|
if ($status == 'REPLAYED_OTP') {
|
||||||
|
if (!$wait_for_all) { $this->_response = $str; }
|
||||||
|
$replay=True;
|
||||||
|
}
|
||||||
|
if ($status == 'OK') {
|
||||||
|
if (!$wait_for_all) { $this->_response = $str; }
|
||||||
|
$valid=True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$wait_for_all && ($valid || $replay))
|
||||||
|
{
|
||||||
|
/* We have status=OK or status=REPLAYED_OTP, return. */
|
||||||
|
foreach ($ch as $h) {
|
||||||
|
curl_multi_remove_handle($mh, $h);
|
||||||
|
curl_close($h);
|
||||||
|
}
|
||||||
|
curl_multi_close($mh);
|
||||||
|
if ($replay) return PEAR::raiseError('REPLAYED_OTP');
|
||||||
|
if ($valid) return true;
|
||||||
|
return PEAR::raiseError($status);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_multi_remove_handle($mh, $info['handle']);
|
||||||
|
curl_close($info['handle']);
|
||||||
|
unset ($ch[(int)$info['handle']]);
|
||||||
|
}
|
||||||
|
curl_multi_select($mh);
|
||||||
|
}
|
||||||
|
} while ($active);
|
||||||
|
|
||||||
|
/* Typically this is only reached for wait_for_all=true or
|
||||||
|
* when the timeout is reached and there is no
|
||||||
|
* OK/REPLAYED_REQUEST answer (think firewall).
|
||||||
|
*/
|
||||||
|
|
||||||
|
foreach ($ch as $h) {
|
||||||
|
curl_multi_remove_handle ($mh, $h);
|
||||||
|
curl_close ($h);
|
||||||
|
}
|
||||||
|
curl_multi_close ($mh);
|
||||||
|
|
||||||
|
if ($replay) return PEAR::raiseError('REPLAYED_OTP');
|
||||||
|
if ($valid) return true;
|
||||||
|
return PEAR::raiseError('NO_VALID_ANSWER');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
File diff suppressed because it is too large
Load Diff
|
@ -17,11 +17,21 @@ if (isset($_POST["logout"])) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once 'inc/vars.inc.php';
|
require_once 'inc/vars.inc.php';
|
||||||
|
|
||||||
if (file_exists('./inc/vars.local.inc.php')) {
|
if (file_exists('./inc/vars.local.inc.php')) {
|
||||||
include_once 'inc/vars.local.inc.php';
|
include_once 'inc/vars.local.inc.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yubi OTP API
|
||||||
|
if (!empty($YUBI_API['ID']) && !empty($YUBI_API['KEY'])) {
|
||||||
|
require_once 'inc/lib/Yubico.php';
|
||||||
|
$yubi = new Auth_Yubico($YUBI_API['ID'], $YUBI_API['KEY']);
|
||||||
|
}
|
||||||
|
// U2F API
|
||||||
|
require_once 'inc/lib/U2F.php';
|
||||||
|
$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://";
|
||||||
|
$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']);
|
||||||
|
|
||||||
|
// PDO
|
||||||
$dsn = "$database_type:host=$database_host;dbname=$database_name";
|
$dsn = "$database_type:host=$database_host;dbname=$database_name";
|
||||||
$opt = [
|
$opt = [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
@ -36,6 +46,7 @@ catch (PDOException $e) {
|
||||||
<center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>🐮 Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
|
<center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>🐮 Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['mailcow_locale'] = strtolower(trim($DEFAULT_LANG));
|
$_SESSION['mailcow_locale'] = strtolower(trim($DEFAULT_LANG));
|
||||||
setcookie('language', $DEFAULT_LANG);
|
setcookie('language', $DEFAULT_LANG);
|
||||||
if (isset($_COOKIE['language'])) {
|
if (isset($_COOKIE['language'])) {
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
<div class="modal fade" id="YubiOTPModal" tabindex="-1" role="dialog" aria-labelledby="YubiOTPModalLabel">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header"><b><?=$lang['tfa']['yubi_otp'];?></b></div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form role="form" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
|
||||||
|
<input type="text" name="otp_token" class="form-control" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
|
||||||
|
<input type="hidden" name="tfa_method" value="yubi_otp">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-default" type="submit" name="set_tfa"><?=$lang['user']['save_changes'];?></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="U2FModal" tabindex="-1" role="dialog" aria-labelledby="U2FModalLabel">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header"><b><?=$lang['tfa']['u2f'];?></b></div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form role="form" method="post" id="u2f_reg_form">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
<p>Please enter your current password and use your U2F USB stick to confirm your identity.</p>
|
||||||
|
<div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
|
||||||
|
<input type="hidden" name="token" id="u2f_register_data"/>
|
||||||
|
<input type="hidden" name="tfa_method" value="u2f">
|
||||||
|
<input type="hidden" name="set_tfa"/><br/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="DisableTFAModal" tabindex="-1" role="dialog" aria-labelledby="DisableTFAModalLabel">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header"><b><?=$lang['tfa']['delete_tfa'];?></b></div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form role="form" method="post">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<input type="hidden" name="tfa_method" value="none">
|
||||||
|
<button class="btn btn-danger" type="submit" name="set_tfa"><?=$lang['tfa']['delete_tfa'];?></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if (isset($_SESSION['pending_tfa_method'])):
|
||||||
|
$tfa_method = $_SESSION['pending_tfa_method'];
|
||||||
|
?>
|
||||||
|
<div class="modal fade" id="ConfirmTFAModal" tabindex="-1" role="dialog" aria-labelledby="ConfirmTFAModalLabel">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header"><button type="button" class="close" data-dismiss="modal">×</button><b><?=$lang['tfa'][$tfa_method];?></b></div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<?php
|
||||||
|
switch ($tfa_method) {
|
||||||
|
case "yubi_otp":
|
||||||
|
?>
|
||||||
|
<form role="form" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
|
||||||
|
<input type="text" name="token" id="token" class="form-control" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
|
||||||
|
<input type="hidden" name="tfa_method" value="yubi_otp">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-default" type="submit" name="verify_tfa_login"><?=$lang['login']['login'];?></button>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
case "u2f":
|
||||||
|
?>
|
||||||
|
<form role="form" method="post" id="u2f_auth_form">
|
||||||
|
<p>Please use your U2F USB stick to confirm your identity now.</p>
|
||||||
|
<div class="alert alert-danger" style="display:none" id="u2f_return_code"></div>
|
||||||
|
<input type="hidden" name="token" id="u2f_auth_data"/>
|
||||||
|
<input type="hidden" name="tfa_method" value="u2f">
|
||||||
|
<input type="hidden" name="verify_tfa_login"/><br/>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
case "totp":
|
||||||
|
?>
|
||||||
|
<div class="empty"></div>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
case "hotp":
|
||||||
|
?>
|
||||||
|
<div class="empty"></div>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
endif;
|
||||||
|
?>
|
|
@ -1,4 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
|
if (isset($_POST["verify_tfa_login"])) {
|
||||||
|
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST["token"])) {
|
||||||
|
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
|
||||||
|
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
|
||||||
|
unset($_SESSION['pending_mailcow_cc_username']);
|
||||||
|
unset($_SESSION['pending_mailcow_cc_role']);
|
||||||
|
unset($_SESSION['pending_tfa_method']);
|
||||||
|
header("Location: /user.php");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
|
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
|
||||||
$login_user = strtolower(trim($_POST["login_user"]));
|
$login_user = strtolower(trim($_POST["login_user"]));
|
||||||
$as = check_login($login_user, $_POST["pass_user"]);
|
$as = check_login($login_user, $_POST["pass_user"]);
|
||||||
|
@ -17,13 +28,19 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
|
||||||
$_SESSION['mailcow_cc_role'] = "user";
|
$_SESSION['mailcow_cc_role'] = "user";
|
||||||
header("Location: /user.php");
|
header("Location: /user.php");
|
||||||
}
|
}
|
||||||
else {
|
elseif ($as != "pending") {
|
||||||
|
unset($_SESSION['pending_mailcow_cc_username']);
|
||||||
|
unset($_SESSION['pending_mailcow_cc_role']);
|
||||||
|
unset($_SESSION['pending_tfa_method']);
|
||||||
|
unset($_SESSION['mailcow_cc_username']);
|
||||||
|
unset($_SESSION['mailcow_cc_role']);
|
||||||
$_SESSION['return'] = array(
|
$_SESSION['return'] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'msg' => $lang['danger']['login_failed']
|
'msg' => $lang['danger']['login_failed']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
|
||||||
if (isset($_GET["duallogin"])) {
|
if (isset($_GET["duallogin"])) {
|
||||||
if (filter_var($_GET["duallogin"], FILTER_VALIDATE_EMAIL)) {
|
if (filter_var($_GET["duallogin"], FILTER_VALIDATE_EMAIL)) {
|
||||||
|
@ -40,8 +57,8 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_POST["set_admin_account"])) {
|
if (isset($_POST["edit_admin_account"])) {
|
||||||
set_admin_account($_POST);
|
edit_admin_account($_POST);
|
||||||
}
|
}
|
||||||
if (isset($_POST["dkim_delete_key"])) {
|
if (isset($_POST["dkim_delete_key"])) {
|
||||||
dkim_delete_key($_POST);
|
dkim_delete_key($_POST);
|
||||||
|
@ -55,9 +72,6 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
|
||||||
if (isset($_POST["delete_domain_admin"])) {
|
if (isset($_POST["delete_domain_admin"])) {
|
||||||
delete_domain_admin($_POST);
|
delete_domain_admin($_POST);
|
||||||
}
|
}
|
||||||
if (isset($_POST["edit_domain_admin"])) {
|
|
||||||
edit_domain_admin($_POST);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") {
|
||||||
if (isset($_POST["edit_user_account"])) {
|
if (isset($_POST["edit_user_account"])) {
|
||||||
|
@ -87,56 +101,64 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user
|
||||||
if (isset($_POST["delete_syncjob"])) {
|
if (isset($_POST["delete_syncjob"])) {
|
||||||
delete_syncjob($_POST);
|
delete_syncjob($_POST);
|
||||||
}
|
}
|
||||||
if (isset($_POST["trigger_set_time_limited_aliases"])) {
|
if (isset($_POST["set_time_limited_aliases"])) {
|
||||||
set_time_limited_aliases($_POST);
|
set_time_limited_aliases($_POST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
|
if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
|
||||||
|
if (isset($_POST["edit_domain_admin"])) {
|
||||||
|
edit_domain_admin($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["set_tfa"])) {
|
||||||
|
set_tfa($_POST);
|
||||||
|
}
|
||||||
if (isset($_POST["trigger_add_policy_list_item"])) {
|
if (isset($_POST["trigger_add_policy_list_item"])) {
|
||||||
add_policy_list_item($_POST);
|
add_policy_list_item($_POST);
|
||||||
}
|
}
|
||||||
if (isset($_POST["trigger_delete_policy_list_item"])) {
|
if (isset($_POST["trigger_delete_policy_list_item"])) {
|
||||||
delete_policy_list_item($_POST);
|
delete_policy_list_item($_POST);
|
||||||
}
|
}
|
||||||
if (isset($_POST["trigger_mailbox_action"])) {
|
if (isset($_POST["mailbox_add_domain"])) {
|
||||||
switch ($_POST["trigger_mailbox_action"]) {
|
|
||||||
case "adddomain":
|
|
||||||
mailbox_add_domain($_POST);
|
mailbox_add_domain($_POST);
|
||||||
break;
|
|
||||||
case "addalias":
|
|
||||||
mailbox_add_alias($_POST);
|
|
||||||
break;
|
|
||||||
case "editalias":
|
|
||||||
mailbox_edit_alias($_POST);
|
|
||||||
break;
|
|
||||||
case "addaliasdomain":
|
|
||||||
mailbox_add_alias_domain($_POST);
|
|
||||||
break;
|
|
||||||
case "addmailbox":
|
|
||||||
mailbox_add_mailbox($_POST);
|
|
||||||
break;
|
|
||||||
case "editdomain":
|
|
||||||
mailbox_edit_domain($_POST);
|
|
||||||
break;
|
|
||||||
case "editmailbox":
|
|
||||||
mailbox_edit_mailbox($_POST);
|
|
||||||
break;
|
|
||||||
case "deletedomain":
|
|
||||||
mailbox_delete_domain($_POST);
|
|
||||||
break;
|
|
||||||
case "deletealias":
|
|
||||||
mailbox_delete_alias($_POST);
|
|
||||||
break;
|
|
||||||
case "deletealiasdomain":
|
|
||||||
mailbox_delete_alias_domain($_POST);
|
|
||||||
break;
|
|
||||||
case "editaliasdomain":
|
|
||||||
mailbox_edit_alias_domain($_POST);
|
|
||||||
break;
|
|
||||||
case "deletemailbox":
|
|
||||||
mailbox_delete_mailbox($_POST);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
if (isset($_POST["mailbox_add_alias"])) {
|
||||||
|
mailbox_add_alias($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_add_alias_domain"])) {
|
||||||
|
mailbox_add_alias_domain($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_add_mailbox"])) {
|
||||||
|
mailbox_add_mailbox($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_add_mailbox"])) {
|
||||||
|
mailbox_add_mailbox($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_edit_alias"])) {
|
||||||
|
mailbox_edit_alias($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_edit_domain"])) {
|
||||||
|
mailbox_edit_domain($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_edit_mailbox"])) {
|
||||||
|
mailbox_edit_mailbox($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_edit_alias_domain"])) {
|
||||||
|
mailbox_edit_alias_domain($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["trigger_delete_policy_list_item"])) {
|
||||||
|
delete_policy_list_item($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_delete_domain"])) {
|
||||||
|
mailbox_delete_domain($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_delete_alias"])) {
|
||||||
|
mailbox_delete_alias($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_delete_alias_domain"])) {
|
||||||
|
mailbox_delete_alias_domain($_POST);
|
||||||
|
}
|
||||||
|
if (isset($_POST["mailbox_delete_mailbox"])) {
|
||||||
|
mailbox_delete_mailbox($_POST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
error_reporting(E_ERROR | E_WARNING);
|
// error_reporting(E_ERROR | E_WARNING);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
PLEASE USE THE FILE "vars.local.inc.php" TO OVERWRITE SETTINGS AND MAKE THEM PERSISTENT!
|
PLEASE USE THE FILE "vars.local.inc.php" TO OVERWRITE SETTINGS AND MAKE THEM PERSISTENT!
|
||||||
|
@ -33,4 +34,9 @@ $DEFAULT_LANG = "en";
|
||||||
// simplex, slate, spacelab, superhero, united, yeti
|
// simplex, slate, spacelab, superhero, united, yeti
|
||||||
// See https://bootswatch.com/
|
// See https://bootswatch.com/
|
||||||
$DEFAULT_THEME = "lumen";
|
$DEFAULT_THEME = "lumen";
|
||||||
|
|
||||||
|
// If you want to use Yubico TFA methods, setup an ID and a key here: https://upgrade.yubico.com/getapikey/
|
||||||
|
// Remember to override this value using vars.local.inc.php, do not change it here.
|
||||||
|
$YUBI_API['ID'] = "";
|
||||||
|
$YUBI_API['KEY'] = "";
|
||||||
?>
|
?>
|
||||||
|
|
|
@ -0,0 +1,651 @@
|
||||||
|
// Copyright 2014-2015 Google Inc. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file or at
|
||||||
|
// https://developers.google.com/open-source/licenses/bsd
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @fileoverview The U2F api.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** Namespace for the U2F api.
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
var u2f = u2f || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The U2F extension id
|
||||||
|
* @type {string}
|
||||||
|
* @const
|
||||||
|
*/
|
||||||
|
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message types for messsages to/from the extension
|
||||||
|
* @const
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
u2f.MessageTypes = {
|
||||||
|
'U2F_REGISTER_REQUEST': 'u2f_register_request',
|
||||||
|
'U2F_SIGN_REQUEST': 'u2f_sign_request',
|
||||||
|
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
|
||||||
|
'U2F_SIGN_RESPONSE': 'u2f_sign_response'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response status codes
|
||||||
|
* @const
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
u2f.ErrorCodes = {
|
||||||
|
'OK': 0,
|
||||||
|
'OTHER_ERROR': 1,
|
||||||
|
'BAD_REQUEST': 2,
|
||||||
|
'CONFIGURATION_UNSUPPORTED': 3,
|
||||||
|
'DEVICE_INELIGIBLE': 4,
|
||||||
|
'TIMEOUT': 5
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message type for registration requests
|
||||||
|
* @typedef {{
|
||||||
|
* type: u2f.MessageTypes,
|
||||||
|
* signRequests: Array<u2f.SignRequest>,
|
||||||
|
* registerRequests: ?Array<u2f.RegisterRequest>,
|
||||||
|
* timeoutSeconds: ?number,
|
||||||
|
* requestId: ?number
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
u2f.Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A message for registration responses
|
||||||
|
* @typedef {{
|
||||||
|
* type: u2f.MessageTypes,
|
||||||
|
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
|
||||||
|
* requestId: ?number
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
u2f.Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error object for responses
|
||||||
|
* @typedef {{
|
||||||
|
* errorCode: u2f.ErrorCodes,
|
||||||
|
* errorMessage: ?string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
u2f.Error;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data object for a single sign request.
|
||||||
|
* @typedef {{
|
||||||
|
* version: string,
|
||||||
|
* challenge: string,
|
||||||
|
* keyHandle: string,
|
||||||
|
* appId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
u2f.SignRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data object for a sign response.
|
||||||
|
* @typedef {{
|
||||||
|
* keyHandle: string,
|
||||||
|
* signatureData: string,
|
||||||
|
* clientData: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
u2f.SignResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data object for a registration request.
|
||||||
|
* @typedef {{
|
||||||
|
* version: string,
|
||||||
|
* challenge: string,
|
||||||
|
* appId: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
u2f.RegisterRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data object for a registration response.
|
||||||
|
* @typedef {{
|
||||||
|
* registrationData: string,
|
||||||
|
* clientData: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
u2f.RegisterResponse;
|
||||||
|
|
||||||
|
|
||||||
|
// Low level MessagePort API support
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a MessagePort to the U2F extension using the
|
||||||
|
* available mechanisms.
|
||||||
|
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
||||||
|
*/
|
||||||
|
u2f.getMessagePort = function(callback) {
|
||||||
|
if (typeof chrome != 'undefined' && chrome.runtime) {
|
||||||
|
// The actual message here does not matter, but we need to get a reply
|
||||||
|
// for the callback to run. Thus, send an empty signature request
|
||||||
|
// in order to get a failure response.
|
||||||
|
var msg = {
|
||||||
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||||
|
signRequests: []
|
||||||
|
};
|
||||||
|
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
|
||||||
|
if (!chrome.runtime.lastError) {
|
||||||
|
// We are on a whitelisted origin and can talk directly
|
||||||
|
// with the extension.
|
||||||
|
u2f.getChromeRuntimePort_(callback);
|
||||||
|
} else {
|
||||||
|
// chrome.runtime was available, but we couldn't message
|
||||||
|
// the extension directly, use iframe
|
||||||
|
u2f.getIframePort_(callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (u2f.isAndroidChrome_()) {
|
||||||
|
u2f.getAuthenticatorPort_(callback);
|
||||||
|
} else {
|
||||||
|
// chrome.runtime was not available at all, which is normal
|
||||||
|
// when this origin doesn't have access to any extensions.
|
||||||
|
u2f.getIframePort_(callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect chrome running on android based on the browser's useragent.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.isAndroidChrome_ = function() {
|
||||||
|
var userAgent = navigator.userAgent;
|
||||||
|
return userAgent.indexOf('Chrome') != -1 &&
|
||||||
|
userAgent.indexOf('Android') != -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects directly to the extension via chrome.runtime.connect
|
||||||
|
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.getChromeRuntimePort_ = function(callback) {
|
||||||
|
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
|
||||||
|
{'includeTlsChannelId': true});
|
||||||
|
setTimeout(function() {
|
||||||
|
callback(new u2f.WrappedChromeRuntimePort_(port));
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a 'port' abstraction to the Authenticator app.
|
||||||
|
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.getAuthenticatorPort_ = function(callback) {
|
||||||
|
setTimeout(function() {
|
||||||
|
callback(new u2f.WrappedAuthenticatorPort_());
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
|
||||||
|
* @param {Port} port
|
||||||
|
* @constructor
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.WrappedChromeRuntimePort_ = function(port) {
|
||||||
|
this.port_ = port;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a return a sign request.
|
||||||
|
* @param {Array<u2f.SignRequest>} signRequests
|
||||||
|
* @param {number} timeoutSeconds
|
||||||
|
* @param {number} reqId
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
|
||||||
|
function(signRequests, timeoutSeconds, reqId) {
|
||||||
|
return {
|
||||||
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||||
|
signRequests: signRequests,
|
||||||
|
timeoutSeconds: timeoutSeconds,
|
||||||
|
requestId: reqId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a return a register request.
|
||||||
|
* @param {Array<u2f.SignRequest>} signRequests
|
||||||
|
* @param {Array<u2f.RegisterRequest>} signRequests
|
||||||
|
* @param {number} timeoutSeconds
|
||||||
|
* @param {number} reqId
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
|
||||||
|
function(signRequests, registerRequests, timeoutSeconds, reqId) {
|
||||||
|
return {
|
||||||
|
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
||||||
|
signRequests: signRequests,
|
||||||
|
registerRequests: registerRequests,
|
||||||
|
timeoutSeconds: timeoutSeconds,
|
||||||
|
requestId: reqId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts a message on the underlying channel.
|
||||||
|
* @param {Object} message
|
||||||
|
*/
|
||||||
|
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
|
||||||
|
this.port_.postMessage(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the HTML 5 addEventListener interface. Works only for the
|
||||||
|
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {function({data: Object})} handler
|
||||||
|
*/
|
||||||
|
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
|
||||||
|
function(eventName, handler) {
|
||||||
|
var name = eventName.toLowerCase();
|
||||||
|
if (name == 'message' || name == 'onmessage') {
|
||||||
|
this.port_.onMessage.addListener(function(message) {
|
||||||
|
// Emulate a minimal MessageEvent object
|
||||||
|
handler({'data': message});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('WrappedChromeRuntimePort only supports onMessage');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the Authenticator app with a MessagePort interface.
|
||||||
|
* @constructor
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_ = function() {
|
||||||
|
this.requestId_ = -1;
|
||||||
|
this.requestObject_ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the Authenticator intent.
|
||||||
|
* @param {Object} message
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
|
||||||
|
var intentLocation = /** @type {string} */ (message);
|
||||||
|
document.location = intentLocation;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the HTML 5 addEventListener interface.
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {function({data: Object})} handler
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
|
||||||
|
function(eventName, handler) {
|
||||||
|
var name = eventName.toLowerCase();
|
||||||
|
if (name == 'message') {
|
||||||
|
var self = this;
|
||||||
|
/* Register a callback to that executes when
|
||||||
|
* chrome injects the response. */
|
||||||
|
window.addEventListener(
|
||||||
|
'message', self.onRequestUpdate_.bind(self, handler), false);
|
||||||
|
} else {
|
||||||
|
console.error('WrappedAuthenticatorPort only supports message');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked when a response is received from the Authenticator.
|
||||||
|
* @param function({data: Object}) callback
|
||||||
|
* @param {Object} message message Object
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
|
||||||
|
function(callback, message) {
|
||||||
|
var messageObject = JSON.parse(message.data);
|
||||||
|
var intentUrl = messageObject['intentURL'];
|
||||||
|
|
||||||
|
var errorCode = messageObject['errorCode'];
|
||||||
|
var responseObject = null;
|
||||||
|
if (messageObject.hasOwnProperty('data')) {
|
||||||
|
responseObject = /** @type {Object} */ (
|
||||||
|
JSON.parse(messageObject['data']));
|
||||||
|
responseObject['requestId'] = this.requestId_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sign responses from the authenticator do not conform to U2F,
|
||||||
|
* convert to U2F here. */
|
||||||
|
responseObject = this.doResponseFixups_(responseObject);
|
||||||
|
callback({'data': responseObject});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixup the response provided by the Authenticator to conform with
|
||||||
|
* the U2F spec.
|
||||||
|
* @param {Object} responseData
|
||||||
|
* @return {Object} the U2F compliant response object
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
|
||||||
|
function(responseObject) {
|
||||||
|
if (responseObject.hasOwnProperty('responseData')) {
|
||||||
|
return responseObject;
|
||||||
|
} else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
|
||||||
|
// Only sign responses require fixups. If this is not a response
|
||||||
|
// to a sign request, then an internal error has occurred.
|
||||||
|
return {
|
||||||
|
'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
|
||||||
|
'responseData': {
|
||||||
|
'errorCode': u2f.ErrorCodes.OTHER_ERROR,
|
||||||
|
'errorMessage': 'Internal error: invalid response from Authenticator'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Non-conformant sign response, do fixups. */
|
||||||
|
var encodedChallengeObject = responseObject['challenge'];
|
||||||
|
if (typeof encodedChallengeObject !== 'undefined') {
|
||||||
|
var challengeObject = JSON.parse(atob(encodedChallengeObject));
|
||||||
|
var serverChallenge = challengeObject['challenge'];
|
||||||
|
var challengesList = this.requestObject_['signData'];
|
||||||
|
var requestChallengeObject = null;
|
||||||
|
for (var i = 0; i < challengesList.length; i++) {
|
||||||
|
var challengeObject = challengesList[i];
|
||||||
|
if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
|
||||||
|
requestChallengeObject = challengeObject;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var responseData = {
|
||||||
|
'errorCode': responseObject['resultCode'],
|
||||||
|
'keyHandle': responseObject['keyHandle'],
|
||||||
|
'signatureData': responseObject['signature'],
|
||||||
|
'clientData': encodedChallengeObject
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
|
||||||
|
'responseData': responseData,
|
||||||
|
'requestId': responseObject['requestId']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base URL for intents to Authenticator.
|
||||||
|
* @const
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
|
||||||
|
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a return a sign request.
|
||||||
|
* @param {Array<u2f.SignRequest>} signRequests
|
||||||
|
* @param {number} timeoutSeconds (ignored for now)
|
||||||
|
* @param {number} reqId
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
|
||||||
|
function(signRequests, timeoutSeconds, reqId) {
|
||||||
|
if (!signRequests || signRequests.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/* TODO(fixme): stash away requestId, as the authenticator app does
|
||||||
|
* not return it for sign responses. */
|
||||||
|
this.requestId_ = reqId;
|
||||||
|
/* TODO(fixme): stash away the signRequests, to deal with the legacy
|
||||||
|
* response format returned by the Authenticator app. */
|
||||||
|
this.requestObject_ = {
|
||||||
|
'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||||
|
'signData': signRequests,
|
||||||
|
'requestId': reqId,
|
||||||
|
'timeout': timeoutSeconds
|
||||||
|
};
|
||||||
|
|
||||||
|
var appId = signRequests[0]['appId'];
|
||||||
|
var intentUrl =
|
||||||
|
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
|
||||||
|
';S.appId=' + encodeURIComponent(appId) +
|
||||||
|
';S.eventId=' + reqId +
|
||||||
|
';S.challenges=' +
|
||||||
|
encodeURIComponent(
|
||||||
|
JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
|
||||||
|
return intentUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the browser data objects from the challenge list
|
||||||
|
* @param {Array} challenges list of challenges
|
||||||
|
* @return {Array} list of browser data objects
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_
|
||||||
|
.prototype.getBrowserDataList_ = function(challenges) {
|
||||||
|
return challenges
|
||||||
|
.map(function(challenge) {
|
||||||
|
var browserData = {
|
||||||
|
'typ': 'navigator.id.getAssertion',
|
||||||
|
'challenge': challenge['challenge']
|
||||||
|
};
|
||||||
|
var challengeObject = {
|
||||||
|
'challenge' : browserData,
|
||||||
|
'keyHandle' : challenge['keyHandle']
|
||||||
|
};
|
||||||
|
return challengeObject;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a return a register request.
|
||||||
|
* @param {Array<u2f.SignRequest>} signRequests
|
||||||
|
* @param {Array<u2f.RegisterRequest>} enrollChallenges
|
||||||
|
* @param {number} timeoutSeconds (ignored for now)
|
||||||
|
* @param {number} reqId
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
|
||||||
|
function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
|
||||||
|
if (!enrollChallenges || enrollChallenges.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Assume the appId is the same for all enroll challenges.
|
||||||
|
var appId = enrollChallenges[0]['appId'];
|
||||||
|
var registerRequests = [];
|
||||||
|
for (var i = 0; i < enrollChallenges.length; i++) {
|
||||||
|
var registerRequest = {
|
||||||
|
'challenge': enrollChallenges[i]['challenge'],
|
||||||
|
'version': enrollChallenges[i]['version']
|
||||||
|
};
|
||||||
|
if (enrollChallenges[i]['appId'] != appId) {
|
||||||
|
// Only include the appId when it differs from the first appId.
|
||||||
|
registerRequest['appId'] = enrollChallenges[i]['appId'];
|
||||||
|
}
|
||||||
|
registerRequests.push(registerRequest);
|
||||||
|
}
|
||||||
|
var registeredKeys = [];
|
||||||
|
if (signRequests) {
|
||||||
|
for (i = 0; i < signRequests.length; i++) {
|
||||||
|
var key = {
|
||||||
|
'keyHandle': signRequests[i]['keyHandle'],
|
||||||
|
'version': signRequests[i]['version']
|
||||||
|
};
|
||||||
|
// Only include the appId when it differs from the appId that's
|
||||||
|
// being registered now.
|
||||||
|
if (signRequests[i]['appId'] != appId) {
|
||||||
|
key['appId'] = signRequests[i]['appId'];
|
||||||
|
}
|
||||||
|
registeredKeys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var request = {
|
||||||
|
'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
||||||
|
'appId': appId,
|
||||||
|
'registerRequests': registerRequests,
|
||||||
|
'registeredKeys': registeredKeys,
|
||||||
|
'requestId': reqId,
|
||||||
|
'timeoutSeconds': timeoutSeconds
|
||||||
|
};
|
||||||
|
var intentUrl =
|
||||||
|
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
|
||||||
|
';S.request=' + encodeURIComponent(JSON.stringify(request)) +
|
||||||
|
';end';
|
||||||
|
/* TODO(fixme): stash away requestId, this is is not necessary for
|
||||||
|
* register requests, but here to keep parity with sign.
|
||||||
|
*/
|
||||||
|
this.requestId_ = reqId;
|
||||||
|
return intentUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up an embedded trampoline iframe, sourced from the extension.
|
||||||
|
* @param {function(MessagePort)} callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.getIframePort_ = function(callback) {
|
||||||
|
// Create the iframe
|
||||||
|
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
|
||||||
|
var iframe = document.createElement('iframe');
|
||||||
|
iframe.src = iframeOrigin + '/u2f-comms.html';
|
||||||
|
iframe.setAttribute('style', 'display:none');
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
var channel = new MessageChannel();
|
||||||
|
var ready = function(message) {
|
||||||
|
if (message.data == 'ready') {
|
||||||
|
channel.port1.removeEventListener('message', ready);
|
||||||
|
callback(channel.port1);
|
||||||
|
} else {
|
||||||
|
console.error('First event on iframe port was not "ready"');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
channel.port1.addEventListener('message', ready);
|
||||||
|
channel.port1.start();
|
||||||
|
|
||||||
|
iframe.addEventListener('load', function() {
|
||||||
|
// Deliver the port to the iframe and initialize
|
||||||
|
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// High-level JS API
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default extension response timeout in seconds.
|
||||||
|
* @const
|
||||||
|
*/
|
||||||
|
u2f.EXTENSION_TIMEOUT_SEC = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton instance for a MessagePort to the extension.
|
||||||
|
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.port_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callbacks waiting for a port
|
||||||
|
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.waitingForPort_ = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A counter for requestIds.
|
||||||
|
* @type {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.reqCounter_ = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map from requestIds to client callbacks
|
||||||
|
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
|
||||||
|
* |function((u2f.Error|u2f.SignResponse)))>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.callbackMap_ = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or retrieves the MessagePort singleton to use.
|
||||||
|
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.getPortSingleton_ = function(callback) {
|
||||||
|
if (u2f.port_) {
|
||||||
|
callback(u2f.port_);
|
||||||
|
} else {
|
||||||
|
if (u2f.waitingForPort_.length == 0) {
|
||||||
|
u2f.getMessagePort(function(port) {
|
||||||
|
u2f.port_ = port;
|
||||||
|
u2f.port_.addEventListener('message',
|
||||||
|
/** @type {function(Event)} */ (u2f.responseHandler_));
|
||||||
|
|
||||||
|
// Careful, here be async callbacks. Maybe.
|
||||||
|
while (u2f.waitingForPort_.length)
|
||||||
|
u2f.waitingForPort_.shift()(u2f.port_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
u2f.waitingForPort_.push(callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles response messages from the extension.
|
||||||
|
* @param {MessageEvent.<u2f.Response>} message
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
u2f.responseHandler_ = function(message) {
|
||||||
|
var response = message.data;
|
||||||
|
var reqId = response['requestId'];
|
||||||
|
if (!reqId || !u2f.callbackMap_[reqId]) {
|
||||||
|
console.error('Unknown or missing requestId in response.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cb = u2f.callbackMap_[reqId];
|
||||||
|
delete u2f.callbackMap_[reqId];
|
||||||
|
cb(response['responseData']);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an array of sign requests to available U2F tokens.
|
||||||
|
* @param {Array<u2f.SignRequest>} signRequests
|
||||||
|
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
||||||
|
* @param {number=} opt_timeoutSeconds
|
||||||
|
*/
|
||||||
|
u2f.sign = function(signRequests, callback, opt_timeoutSeconds) {
|
||||||
|
u2f.getPortSingleton_(function(port) {
|
||||||
|
var reqId = ++u2f.reqCounter_;
|
||||||
|
u2f.callbackMap_[reqId] = callback;
|
||||||
|
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||||
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
||||||
|
var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
|
||||||
|
port.postMessage(req);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches register requests to available U2F tokens. An array of sign
|
||||||
|
* requests identifies already registered tokens.
|
||||||
|
* @param {Array<u2f.RegisterRequest>} registerRequests
|
||||||
|
* @param {Array<u2f.SignRequest>} signRequests
|
||||||
|
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
||||||
|
* @param {number=} opt_timeoutSeconds
|
||||||
|
*/
|
||||||
|
u2f.register = function(registerRequests, signRequests,
|
||||||
|
callback, opt_timeoutSeconds) {
|
||||||
|
u2f.getPortSingleton_(function(port) {
|
||||||
|
var reqId = ++u2f.reqCounter_;
|
||||||
|
u2f.callbackMap_[reqId] = callback;
|
||||||
|
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||||
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
||||||
|
var req = port.formatRegisterRequest_(
|
||||||
|
signRequests, registerRequests, timeoutSeconds, reqId);
|
||||||
|
port.postMessage(req);
|
||||||
|
});
|
||||||
|
};
|
|
@ -15,9 +15,9 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Show generate button after time selection
|
// Show generate button after time selection
|
||||||
$('#trigger_set_time_limited_aliases').hide();
|
$('#generate_tla').hide();
|
||||||
$('#validity').change(function(){
|
$('#validity').change(function(){
|
||||||
$('#trigger_set_time_limited_aliases').show();
|
$('#generate_tla').show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Init Bootstrap Switch
|
// Init Bootstrap Switch
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
require_once 'inc/prerequisites.inc.php';
|
require_once 'inc/prerequisites.inc.php';
|
||||||
error_reporting(0);
|
error_reporting(E_ALL);
|
||||||
if (isset($_SESSION['mailcow_cc_role'])) {
|
if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) {
|
||||||
if ($_GET['action'] && $_GET['object']) {
|
if ($_GET['action'] && $_GET['object']) {
|
||||||
$action = $_GET['action'];
|
$action = $_GET['action'];
|
||||||
$object = $_GET['object'];
|
$object = $_GET['object'];
|
||||||
|
@ -24,6 +24,31 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||||
echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "get_u2f_reg_challenge":
|
||||||
|
if (
|
||||||
|
($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin")
|
||||||
|
&&
|
||||||
|
($_SESSION["mailcow_cc_username"] == $object)
|
||||||
|
) {
|
||||||
|
$data = $u2f->getRegisterData(get_u2f_registrations($object));
|
||||||
|
list($req, $sigs) = $data;
|
||||||
|
$_SESSION['regReq'] = json_encode($req);
|
||||||
|
echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo '{}';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "get_u2f_auth_challenge":
|
||||||
|
if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) {
|
||||||
|
$reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object)));
|
||||||
|
$_SESSION['authReq'] = $reqs;
|
||||||
|
echo 'var req = ' . $reqs . ';';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
echo '{}';
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
echo '{}';
|
echo '{}';
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -94,10 +94,10 @@ $lang['user']['user_settings'] = 'Benutzereinstellungen';
|
||||||
$lang['user']['mailbox_settings'] = 'Mailbox-Einstellungen';
|
$lang['user']['mailbox_settings'] = 'Mailbox-Einstellungen';
|
||||||
$lang['user']['mailbox_details'] = 'Mailbox-Details';
|
$lang['user']['mailbox_details'] = 'Mailbox-Details';
|
||||||
$lang['user']['change_password'] = 'Passwort ändern';
|
$lang['user']['change_password'] = 'Passwort ändern';
|
||||||
$lang['user']['new_password'] = 'Neues Passwort:';
|
$lang['user']['new_password'] = 'Neues Passwort';
|
||||||
$lang['user']['save_changes'] = 'Änderungen speichern';
|
$lang['user']['save_changes'] = 'Änderungen speichern';
|
||||||
$lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen):';
|
$lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen)';
|
||||||
$lang['user']['new_password_repeat'] = 'Neues Passwort (Wiederholung):';
|
$lang['user']['new_password_repeat'] = 'Neues Passwort (Wiederholung)';
|
||||||
$lang['user']['new_password_description'] = 'Mindestanforderung: 6 Zeichen lang, Buchstaben und Zahlen.';
|
$lang['user']['new_password_description'] = 'Mindestanforderung: 6 Zeichen lang, Buchstaben und Zahlen.';
|
||||||
$lang['user']['did_you_know'] = '<b>Wussten Sie schon?</b> Sie können Ihre E-Mail-Adresse mit Tags versehen, etwa "ich+<b>Privat</b>@example.com", um Nachrichten automatisch in einem Unterordner (Beispiel: "Privat") abzulegen.';
|
$lang['user']['did_you_know'] = '<b>Wussten Sie schon?</b> Sie können Ihre E-Mail-Adresse mit Tags versehen, etwa "ich+<b>Privat</b>@example.com", um Nachrichten automatisch in einem Unterordner (Beispiel: "Privat") abzulegen.';
|
||||||
$lang['user']['spam_aliases'] = 'Temporäre E-Mail Aliasse';
|
$lang['user']['spam_aliases'] = 'Temporäre E-Mail Aliasse';
|
||||||
|
@ -351,11 +351,19 @@ $lang['login']['login'] = 'Anmelden';
|
||||||
$lang['login']['previous'] = 'Vorherige Seite';
|
$lang['login']['previous'] = 'Vorherige Seite';
|
||||||
$lang['login']['delayed'] = 'Login wurde zur Sicherheit um %s Sekunde/n verzögert.';
|
$lang['login']['delayed'] = 'Login wurde zur Sicherheit um %s Sekunde/n verzögert.';
|
||||||
|
|
||||||
$lang['login']['tfa'] = 'Zwei-Faktor-Authentifizierung';
|
$lang['tfa']['tfa'] = "Two-Factor Authentication";
|
||||||
$lang['login']['tfa_details'] = 'Bitte bestätigen Sie Ihr Einmalpasswort im folgenden Feld';
|
$lang['tfa']['set_tfa'] = "Konfiguriere Two-Factor Authentication Methode";
|
||||||
$lang['login']['confirm'] = 'Bestätigen';
|
$lang['tfa']['yubi_otp'] = "Yubico OTP Authentifizierung";
|
||||||
$lang['login']['otp'] = 'Einmalpasswort';
|
$lang['tfa']['u2f'] = "U2F Authentifizierung";
|
||||||
$lang['login']['trash_login'] = 'Login verwerfen';
|
$lang['tfa']['hotp'] = "HOTP Authentifizierung";
|
||||||
|
$lang['tfa']['totp'] = "TOTP Authentifizierung";
|
||||||
|
$lang['tfa']['none'] = "Deaktiviert";
|
||||||
|
$lang['tfa']['delete_tfa'] = "Deaktiviere TFA";
|
||||||
|
$lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field";
|
||||||
|
$lang['tfa']['confirm'] = "Bestätigen";
|
||||||
|
$lang['tfa']['otp'] = "Einmalpasswort";
|
||||||
|
$lang['tfa']['trash_login'] = "Login verwerfen";
|
||||||
|
$lang['tfa']['select'] = "Bitte auswählen";
|
||||||
|
|
||||||
$lang['admin']['search_domain_da'] = 'Domains durchsuchen';
|
$lang['admin']['search_domain_da'] = 'Domains durchsuchen';
|
||||||
$lang['admin']['restrictions'] = 'Postifx Restriktionen';
|
$lang['admin']['restrictions'] = 'Postifx Restriktionen';
|
||||||
|
|
|
@ -89,6 +89,7 @@ $lang['warning']['spam_alias_temp_error'] = "Temporary error: Cannot add spam al
|
||||||
$lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses exceeded";
|
$lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses exceeded";
|
||||||
$lang['danger']['fetchmail_active'] = "A process is already running, please wait for it to finish.";
|
$lang['danger']['fetchmail_active'] = "A process is already running, please wait for it to finish.";
|
||||||
$lang['danger']['validity_missing'] = 'Please assign a period of validity';
|
$lang['danger']['validity_missing'] = 'Please assign a period of validity';
|
||||||
|
$lang['danger']['tfa_token_invalid'] = 'TFA token is invalid';
|
||||||
$lang['user']['on'] = "On";
|
$lang['user']['on'] = "On";
|
||||||
$lang['user']['off'] = "Off";
|
$lang['user']['off'] = "Off";
|
||||||
$lang['user']['user_change_fn'] = "";
|
$lang['user']['user_change_fn'] = "";
|
||||||
|
@ -96,10 +97,10 @@ $lang['user']['user_settings'] = 'User settings';
|
||||||
$lang['user']['mailbox_settings'] = 'Mailbox settings';
|
$lang['user']['mailbox_settings'] = 'Mailbox settings';
|
||||||
$lang['user']['mailbox_details'] = 'Mailbox details';
|
$lang['user']['mailbox_details'] = 'Mailbox details';
|
||||||
$lang['user']['change_password'] = 'Change password';
|
$lang['user']['change_password'] = 'Change password';
|
||||||
$lang['user']['new_password'] = 'New password:';
|
$lang['user']['new_password'] = 'New password';
|
||||||
$lang['user']['save_changes'] = 'Save changes';
|
$lang['user']['save_changes'] = 'Save changes';
|
||||||
$lang['user']['password_now'] = 'Current password (confirm changes):';
|
$lang['user']['password_now'] = 'Current password (confirm changes)';
|
||||||
$lang['user']['new_password_repeat'] = 'Confirmation password (repeat):';
|
$lang['user']['new_password_repeat'] = 'Confirmation password (repeat)';
|
||||||
$lang['user']['new_password_description'] = 'Requirement: 6 characters long, letters and numbers.';
|
$lang['user']['new_password_description'] = 'Requirement: 6 characters long, letters and numbers.';
|
||||||
$lang['user']['did_you_know'] = '<b>Did you know?</b> You can use tags in your email address ("me+<b>privat</b>@example.com") to move messages to a folder automatically (example: "privat").';
|
$lang['user']['did_you_know'] = '<b>Did you know?</b> You can use tags in your email address ("me+<b>privat</b>@example.com") to move messages to a folder automatically (example: "privat").';
|
||||||
$lang['user']['spam_aliases'] = 'Temporary email aliases';
|
$lang['user']['spam_aliases'] = 'Temporary email aliases';
|
||||||
|
@ -252,7 +253,7 @@ $lang['delete']['previous'] = 'Previous page';
|
||||||
|
|
||||||
$lang['edit']['syncjob'] = 'Edit sync job';
|
$lang['edit']['syncjob'] = 'Edit sync job';
|
||||||
$lang['edit']['save'] = 'Save changes';
|
$lang['edit']['save'] = 'Save changes';
|
||||||
$lang['edit']['username'] = 'Save changes';
|
$lang['edit']['username'] = 'Username';
|
||||||
$lang['edit']['hostname'] = 'Hostname';
|
$lang['edit']['hostname'] = 'Hostname';
|
||||||
$lang['edit']['encryption'] = 'Encryption';
|
$lang['edit']['encryption'] = 'Encryption';
|
||||||
$lang['edit']['maxage'] = 'Maximum age of messages in days that will be polled from remote<br /><small>(0 = ignore age)</small>';
|
$lang['edit']['maxage'] = 'Maximum age of messages in days that will be polled from remote<br /><small>(0 = ignore age)</small>';
|
||||||
|
@ -354,11 +355,19 @@ $lang['login']['login'] = 'Login';
|
||||||
$lang['login']['previous'] = "Previous page";
|
$lang['login']['previous'] = "Previous page";
|
||||||
$lang['login']['delayed'] = 'Login was delayed by %s seconds.';
|
$lang['login']['delayed'] = 'Login was delayed by %s seconds.';
|
||||||
|
|
||||||
$lang['login']['tfa'] = "Two-factor authentication";
|
$lang['tfa']['tfa'] = "Two-factor authentication";
|
||||||
$lang['login']['tfa_details'] = "Please confirm your one-time password in the below field";
|
$lang['tfa']['set_tfa'] = "Set two-factor authentication method";
|
||||||
$lang['login']['confirm'] = "Confirm";
|
$lang['tfa']['yubi_otp'] = "Yubico OTP authentication";
|
||||||
$lang['login']['otp'] = "One-time password";
|
$lang['tfa']['u2f'] = "U2F authentication";
|
||||||
$lang['login']['trash_login'] = "Trash login";
|
$lang['tfa']['hotp'] = "HOTP authentication";
|
||||||
|
$lang['tfa']['totp'] = "TOTP authentication";
|
||||||
|
$lang['tfa']['none'] = "Deaktiviert";
|
||||||
|
$lang['tfa']['delete_tfa'] = "Disable TFA";
|
||||||
|
$lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field";
|
||||||
|
$lang['tfa']['confirm'] = "Confirm";
|
||||||
|
$lang['tfa']['otp'] = "One-time password";
|
||||||
|
$lang['tfa']['trash_login'] = "Trash login";
|
||||||
|
$lang['tfa']['select'] = "Please select";
|
||||||
|
|
||||||
$lang['admin']['search_domain_da'] = 'Search domains';
|
$lang['admin']['search_domain_da'] = 'Search domains';
|
||||||
$lang['admin']['restrictions'] = 'Postifx Restrictions';
|
$lang['admin']['restrictions'] = 'Postifx Restrictions';
|
||||||
|
|
|
@ -93,10 +93,10 @@ $lang['user']['user_settings'] = 'Gebruikersinstellingen';
|
||||||
$lang['user']['mailbox_settings'] = 'Postvakinstellingen';
|
$lang['user']['mailbox_settings'] = 'Postvakinstellingen';
|
||||||
$lang['user']['mailbox_details'] = 'Postvakdetails';
|
$lang['user']['mailbox_details'] = 'Postvakdetails';
|
||||||
$lang['user']['change_password'] = 'Verander wachtwoord';
|
$lang['user']['change_password'] = 'Verander wachtwoord';
|
||||||
$lang['user']['new_password'] = 'Nieuw wachtwoord:';
|
$lang['user']['new_password'] = 'Nieuw wachtwoord';
|
||||||
$lang['user']['save_changes'] = 'Wijzigingen opslaan';
|
$lang['user']['save_changes'] = 'Wijzigingen opslaan';
|
||||||
$lang['user']['password_now'] = 'Huidig wachtwoord (bevestig wijzigingen):';
|
$lang['user']['password_now'] = 'Huidig wachtwoord (bevestig wijzigingen)';
|
||||||
$lang['user']['new_password_repeat'] = 'Bevestig wachtwoord (herhalen):';
|
$lang['user']['new_password_repeat'] = 'Bevestig wachtwoord (herhalen)';
|
||||||
$lang['user']['new_password_description'] = 'Vereisten: 6 karakters lang, letters en nummers.';
|
$lang['user']['new_password_description'] = 'Vereisten: 6 karakters lang, letters en nummers.';
|
||||||
$lang['user']['did_you_know'] = '<b>Wist u dat?</b> U kunt tags in het e-mailadres gebruiken ("me+<b>prive</b>@voorbeeld.nl") om berichten automatisch naar een bijbehorende map te sturen (voorbeeld: "prive").';
|
$lang['user']['did_you_know'] = '<b>Wist u dat?</b> U kunt tags in het e-mailadres gebruiken ("me+<b>prive</b>@voorbeeld.nl") om berichten automatisch naar een bijbehorende map te sturen (voorbeeld: "prive").';
|
||||||
$lang['user']['spam_aliases'] = 'Tijdelijk e-mailadres';
|
$lang['user']['spam_aliases'] = 'Tijdelijk e-mailadres';
|
||||||
|
|
|
@ -91,10 +91,10 @@ $lang['user']['user_settings'] = 'Configurações do usuário';
|
||||||
$lang['user']['mailbox_settings'] = 'Configrações da conta';
|
$lang['user']['mailbox_settings'] = 'Configrações da conta';
|
||||||
$lang['user']['mailbox_details'] = 'Detalhes da conta';
|
$lang['user']['mailbox_details'] = 'Detalhes da conta';
|
||||||
$lang['user']['change_password'] = 'Alterar senha';
|
$lang['user']['change_password'] = 'Alterar senha';
|
||||||
$lang['user']['new_password'] = 'Nova senha:';
|
$lang['user']['new_password'] = 'Nova senha';
|
||||||
$lang['user']['save_changes'] = 'Salvar';
|
$lang['user']['save_changes'] = 'Salvar';
|
||||||
$lang['user']['password_now'] = 'Senha atual (confirme a alteração):';
|
$lang['user']['password_now'] = 'Senha atual (confirme a alteração)';
|
||||||
$lang['user']['new_password_repeat'] = 'Confirmar senha (repetir):';
|
$lang['user']['new_password_repeat'] = 'Confirmar senha (repetir)';
|
||||||
$lang['user']['new_password_description'] = 'Requerido: mínimo de 6 characteres com letras e números.';
|
$lang['user']['new_password_description'] = 'Requerido: mínimo de 6 characteres com letras e números.';
|
||||||
$lang['user']['did_you_know'] = '<b>Você sabia?</b> Você pode usar tags no endereço de email ("conta+<b>privado</b>@example.com") para classificar as mensagens automaticamente para uma determinada pasta (exemplo: "privado").';
|
$lang['user']['did_you_know'] = '<b>Você sabia?</b> Você pode usar tags no endereço de email ("conta+<b>privado</b>@example.com") para classificar as mensagens automaticamente para uma determinada pasta (exemplo: "privado").';
|
||||||
$lang['user']['spam_aliases'] = 'Apelidos temporários';
|
$lang['user']['spam_aliases'] = 'Apelidos temporários';
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
<?php
|
||||||
|
require_once('inc/prerequisites.inc.php');
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
|
||||||
|
|
||||||
|
$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://";
|
||||||
|
$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']);
|
||||||
|
|
||||||
|
function getRegs($username) {
|
||||||
|
global $pdo;
|
||||||
|
$sel = $pdo->prepare("select * from tfa where username = ?");
|
||||||
|
$sel->execute(array($username));
|
||||||
|
return $sel->fetchAll();
|
||||||
|
}
|
||||||
|
function addReg($username, $reg) {
|
||||||
|
global $pdo;
|
||||||
|
$ins = $pdo->prepare("INSERT INTO `tfa` (`username`, `keyHandle`, `publicKey`, `certificate`, `counter`) values (?, ?, ?, ?, ?)");
|
||||||
|
$ins->execute(array($username, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
|
||||||
|
}
|
||||||
|
function updateReg($reg) {
|
||||||
|
global $pdo;
|
||||||
|
$upd = $pdo->prepare("update tfa set counter = ? where id = ?");
|
||||||
|
$upd->execute(array($reg->counter, $reg->id));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="js/u2f-api.js"></script>
|
||||||
|
<?php
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if ((empty($_POST['u2f_username'])) || (!isset($_POST['action']) && !isset($_POST['u2f_register_data']) && !isset($_POST['u2f_auth_data']))) {
|
||||||
|
print_r($_POST);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$username = $_POST['u2f_username'];
|
||||||
|
if (isset($_POST['action'])) {
|
||||||
|
switch($_POST['action']) {
|
||||||
|
case 'register':
|
||||||
|
try {
|
||||||
|
$data = $u2f->getRegisterData(getRegs($username));
|
||||||
|
list($req, $sigs) = $data;
|
||||||
|
$_SESSION['regReq'] = json_encode($req);
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
var req = <?=json_encode($req);?>;
|
||||||
|
var sigs = <?=json_encode($sigs);?>;
|
||||||
|
var username = "<?=$username;?>";
|
||||||
|
setTimeout(function() {
|
||||||
|
console.log("Register: ", req);
|
||||||
|
u2f.register([req], sigs, function(data) {
|
||||||
|
var form = document.getElementById('u2f_form');
|
||||||
|
var reg = document.getElementById('u2f_register_data');
|
||||||
|
var user = document.getElementById('u2f_username');
|
||||||
|
var status = document.getElementById('u2f_status');
|
||||||
|
console.log("Register callback", data);
|
||||||
|
if (data.errorCode && data.errorCode != 0) {
|
||||||
|
var div = document.getElementById('u2f_return_code');
|
||||||
|
div.innerHTML = 'Error code: ' + data.errorCode;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reg.value = JSON.stringify(data);
|
||||||
|
user.value = username;
|
||||||
|
status.value = "1";
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
catch( Exception $e ) {
|
||||||
|
echo "U2F error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'authenticate':
|
||||||
|
try {
|
||||||
|
$reqs = json_encode($u2f->getAuthenticateData(getRegs($username)));
|
||||||
|
$_SESSION['authReq'] = $reqs;
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
var req = <?=$reqs;?>;
|
||||||
|
var username = "<?=$username;?>";
|
||||||
|
setTimeout(function() {
|
||||||
|
console.log("sign: ", req);
|
||||||
|
u2f.sign(req, function(data) {
|
||||||
|
var form = document.getElementById('u2f_form');
|
||||||
|
var auth = document.getElementById('u2f_auth_data');
|
||||||
|
var user = document.getElementById('u2f_username');
|
||||||
|
console.log("Authenticate callback", data);
|
||||||
|
auth.value = JSON.stringify(data);
|
||||||
|
user.value = username;
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
echo "U2F error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($_POST['u2f_register_data'])) {
|
||||||
|
try {
|
||||||
|
$reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_POST['u2f_register_data']));
|
||||||
|
addReg($username, $reg);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
echo "U2F error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
echo "Success";
|
||||||
|
$_SESSION['regReq'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($_POST['u2f_auth_data'])) {
|
||||||
|
try {
|
||||||
|
$reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), getRegs($username), json_decode($_POST['u2f_auth_data']));
|
||||||
|
updateReg($reg);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
echo "U2F error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
echo "Success";
|
||||||
|
$_SESSION['authReq'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="u2f_return_code"></div>
|
||||||
|
<form method="POST" id="u2f_form">
|
||||||
|
<input type="hidden" name="u2f_register_data" id="u2f_register_data"/>
|
||||||
|
<input type="hidden" name="u2f_auth_data" id="u2f_auth_data"/>
|
||||||
|
<input type="hidden" name="u2f_username" id="u2f_username"/><br/>
|
||||||
|
<input type="hidden" name="u2f_status" id="u2f_status"/><br/>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
?>
|
||||||
|
<form method="POST" id="post_form">
|
||||||
|
Username: <input name="u2f_username" id="u2f_username"/><br/><hr>
|
||||||
|
Action: <br />
|
||||||
|
<input value="register" name="action" type="radio"/> Register<br/>
|
||||||
|
<input value="authenticate" name="action" type="radio"/> Authenticate<br/>
|
||||||
|
<button type="submit">Submit!</button>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,53 +1,67 @@
|
||||||
<?php
|
<?php
|
||||||
require_once("inc/prerequisites.inc.php");
|
require_once("inc/prerequisites.inc.php");
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||||
|
|
||||||
|
/*
|
||||||
|
/ DOMAIN ADMIN
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once("inc/header.inc.php");
|
||||||
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
?>
|
||||||
|
<div class="container">
|
||||||
|
<h3><?=$lang['user']['user_settings'];?></h3>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><?=$lang['user']['user_settings'];?></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-offset-3 col-sm-9">
|
||||||
|
<p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?></div>
|
||||||
|
<div class="col-md-9 col-xs-7">
|
||||||
|
<p><?=get_tfa()['pretty'];?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?></div>
|
||||||
|
<div class="col-md-9 col-xs-7">
|
||||||
|
<select id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
|
||||||
|
<option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
|
||||||
|
<option value="none"><?=$lang['tfa']['none'];?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||||
|
|
||||||
|
/*
|
||||||
|
/ USER
|
||||||
|
*/
|
||||||
|
|
||||||
require_once("inc/header.inc.php");
|
require_once("inc/header.inc.php");
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
$get_tls_policy = get_tls_policy($_SESSION['mailcow_cc_username']);
|
$get_tls_policy = get_tls_policy($_SESSION['mailcow_cc_username']);
|
||||||
?>
|
?>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h3><?=$lang['user']['mailbox_settings'];?></h3>
|
<h3><?=$lang['user']['user_settings'];?></h3>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><?=$lang['user']['mailbox_details'];?></div>
|
<div class="panel-heading"><?=$lang['user']['mailbox_details'];?></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form class="form-horizontal" role="form" method="post" autocomplete="off">
|
<div class="row">
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-3 col-sm-10">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label><input type="checkbox" name="togglePwNew" id="togglePwNew"> <?=$lang['user']['change_password'];?></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="passFields">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label>
|
|
||||||
<div class="col-sm-5">
|
|
||||||
<input type="password" class="form-control" name="user_new_pass" id="user_new_pass" autocomplete="off" disabled="disabled" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label col-sm-3" for="user_new_pass2"><?=$lang['user']['new_password_repeat'];?></label>
|
|
||||||
<div class="col-sm-5">
|
|
||||||
<input type="password" class="form-control" name="user_new_pass2" id="user_new_pass2" disabled="disabled" autocomplete="off" required>
|
|
||||||
<p class="help-block"><?=$lang['user']['new_password_description'];?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="control-label col-sm-3" for="user_old_pass"><?=$lang['user']['password_now'];?></label>
|
|
||||||
<div class="col-sm-5">
|
|
||||||
<input type="password" class="form-control" name="user_old_pass" id="user_old_pass" autocomplete="off" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-3 col-sm-9">
|
<div class="col-sm-offset-3 col-sm-9">
|
||||||
<button type="submit" name="edit_user_account" class="btn btn-success btn-default"><?=$lang['user']['save_changes'];?></button>
|
<p><a href="#pwChangeModal" data-toggle="modal">[<?=$lang['user']['change_password'];?>]</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<hr>
|
<hr>
|
||||||
<?php // Get user information about aliases
|
<?php // Get user information about aliases
|
||||||
$user_get_alias_details = user_get_alias_details($username);?>
|
$user_get_alias_details = user_get_alias_details($username);?>
|
||||||
|
@ -170,7 +184,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
|
||||||
<option value="168">1 <?=$lang['user']['week'];?></option>
|
<option value="168">1 <?=$lang['user']['week'];?></option>
|
||||||
<option value="672">4 <?=$lang['user']['weeks'];?></option>
|
<option value="672">4 <?=$lang['user']['weeks'];?></option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" name="set_time_limited_aliases" value="generate" class="btn btn-success"><?=$lang['user']['alias_create_random'];?></button>
|
<button type="submit" name="set_time_limited_aliases" id="generate_tla" value="generate" class="btn btn-success"><?=$lang['user']['alias_create_random'];?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -423,7 +437,16 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom:200px;"></div>
|
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user" || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
/ USER OR DOMAIN ADMIN
|
||||||
|
*/
|
||||||
|
|
||||||
|
?>
|
||||||
<div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
|
<div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
|
||||||
<div class="modal-dialog" style="width:90%" role="document">
|
<div class="modal-dialog" style="width:90%" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
@ -433,12 +456,50 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:200px;"></div>
|
||||||
|
<div class="modal fade" id="pwChangeModal" tabindex="-1" role="dialog" aria-labelledby="pwChangeModalLabel">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="form-horizontal" role="form" method="post" autocomplete="off">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-3" for="user_new_pass"><?=$lang['user']['new_password'];?></label>
|
||||||
|
<div class="col-sm-5">
|
||||||
|
<input type="password" class="form-control" name="user_new_pass" id="user_new_pass" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-3" for="user_new_pass2"><?=$lang['user']['new_password_repeat'];?></label>
|
||||||
|
<div class="col-sm-5">
|
||||||
|
<input type="password" class="form-control" name="user_new_pass2" id="user_new_pass2" autocomplete="off" required>
|
||||||
|
<p class="help-block"><?=$lang['user']['new_password_description'];?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-sm-3" for="user_old_pass"><?=$lang['user']['password_now'];?></label>
|
||||||
|
<div class="col-sm-5">
|
||||||
|
<input type="password" class="form-control" name="user_old_pass" id="user_old_pass" autocomplete="off" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-3 col-sm-9">
|
||||||
|
<button type="submit" name="edit_<?=($_SESSION['mailcow_cc_role'] == "domainadmin") ? "domain_admin" : "user_account";?>" class="btn btn-sm btn-success"><?=$lang['user']['change_password'];?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div> <!-- /container -->
|
</div> <!-- /container -->
|
||||||
<script src="js/sorttable.js"></script>
|
<script src="js/sorttable.js"></script>
|
||||||
<script src="js/user.js"></script>
|
<script src="js/user.js"></script>
|
||||||
<?php
|
<?php
|
||||||
require_once("inc/footer.inc.php");
|
require_once("inc/footer.inc.php");
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
header('Location: /');
|
header('Location: /');
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue