This commit is contained in:
djmaze 2021-04-13 11:42:06 +02:00
parent 84f3a7dd77
commit d9bab28bfe
16 changed files with 1206 additions and 0 deletions

View file

@ -1,5 +1,7 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings'; import { settingsAddViewModel } from 'Screen/AbstractSettings';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
import { showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
const USER_VIEW_MODELS_HOOKS = [], const USER_VIEW_MODELS_HOOKS = [],
ADMIN_VIEW_MODELS_HOOKS = []; ADMIN_VIEW_MODELS_HOOKS = [];
@ -53,3 +55,6 @@ rl.pluginSettingsGet = (pluginSection, name) => {
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null; plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
return plugins ? (null == plugins[name] ? null : plugins[name]) : null; return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
}; };
rl.showPluginPopup = showScreenPopup;
rl.pluginPopupView = AbstractViewPopup;

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2021 SnappyMail Team
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,381 @@
<?php
use \RainLoop\Exceptions\ClientException;
class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Two Factor Authentication',
VERSION = '2.0',
RELEASE = '2021-04-13',
REQUIRED = '2.5.0',
CATEGORY = 'Security',
DESCRIPTION = 'This plugin allows you to to have TOTP';
// \RainLoop\Notifications\
const
AccountTwoFactorAuthRequired = 120,
AccountTwoFactorAuthError = 121,
Capa_TWO_FACTOR = 'TWO_FACTOR',
Capa_TWO_FACTOR_FORCE = 'TWO_FACTOR_FORCE';
/**
* @var \RainLoop\Providers\TwoFactorAuth
*/
private $oTwoFactorAuthProvider;
public function Init() : void
{
$this->UseLangs(true);
$this->addJs('js/TwoFactorAuthSettings.js');
$this->addJsonHook('GetTwoFactorInfo', 'DoGetTwoFactorInfo');
$this->addJsonHook('CreateTwoFactorSecret', 'DoCreateTwoFactorSecret');
$this->addJsonHook('ShowTwoFactorSecret', 'DoShowTwoFactorSecret');
$this->addJsonHook('EnableTwoFactor', 'DoEnableTwoFactor');
$this->addJsonHook('TestTwoFactorInfo', 'DoTestTwoFactorInfo');
$this->addJsonHook('ClearTwoFactorInfo', 'DoClearTwoFactorInfo');
$this->addTemplate('templates/TwoFactorAuthSettings.html');
$this->addTemplate('templates/PopupsTwoFactorAuthTest.html');
}
public function configMapping() : array
{
return [
\RainLoop\Plugins\Property::NewInstance('allow_two_factor_auth')
->SetLabel('TAB_SECURITY/LABEL_ALLOW_TWO_STEP')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL),
\RainLoop\Plugins\Property::NewInstance('force_two_factor_auth')
->SetLabel('TAB_SECURITY/LABEL_FORCE_TWO_STEP')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
];
}
public function DoGetTwoFactorInfo() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->TwoFactorAuthProvider() ||
!$this->GetCapa(false, static::Capa_TWO_FACTOR, $oAccount))
{
return $this->FalseResponse(__FUNCTION__);
}
return $this->DefaultResponse(__FUNCTION__,
$this->getTwoFactorInfo($oAccount, true));
}
public function DoCreateTwoFactorSecret() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->TwoFactorAuthProvider() ||
!$this->GetCapa(false, static::Capa_TWO_FACTOR, $oAccount))
{
return $this->FalseResponse(__FUNCTION__);
}
$sEmail = $oAccount->ParentEmailHelper();
$sSecret = $this->TwoFactorAuthProvider()->CreateSecret();
$aCodes = array();
for ($iIndex = 9; $iIndex > 0; $iIndex--)
{
$aCodes[] = \rand(100000000, 900000000);
}
$this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\RainLoop\Utils::EncodeKeyValues(array(
'User' => $sEmail,
'Enable' => false,
'Secret' => $sSecret,
'BackupCodes' => \implode(' ', $aCodes)
))
);
$this->requestSleep();
return $this->DefaultResponse(__FUNCTION__,
$this->getTwoFactorInfo($oAccount));
}
public function DoShowTwoFactorSecret() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->TwoFactorAuthProvider() ||
!$this->GetCapa(false, static::Capa_TWO_FACTOR, $oAccount))
{
return $this->FalseResponse(__FUNCTION__);
}
$aResult = $this->getTwoFactorInfo($oAccount);
unset($aResult['BackupCodes']);
return $this->DefaultResponse(__FUNCTION__, $aResult);
}
public function DoEnableTwoFactor() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->TwoFactorAuthProvider() ||
!$this->GetCapa(false, static::Capa_TWO_FACTOR, $oAccount))
{
return $this->FalseResponse(__FUNCTION__);
}
// $this->setSettingsFromParams($oSettings, 'EnableTwoFactor', 'bool');
$sEmail = $oAccount->ParentEmailHelper();
$bResult = false;
$mData = $this->getTwoFactorInfo($oAccount);
if (isset($mData['Secret'], $mData['BackupCodes']))
{
$bResult = $this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\RainLoop\Utils::EncodeKeyValues(array(
'User' => $sEmail,
'Enable' => '1' === \trim($this->jsonParam('Enable', '0')),
'Secret' => $mData['Secret'],
'BackupCodes' => $mData['BackupCodes']
))
);
}
return $this->DefaultResponse(__FUNCTION__, $bResult);
}
public function DoTestTwoFactorInfo() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->TwoFactorAuthProvider() ||
!$this->GetCapa(false, static::Capa_TWO_FACTOR, $oAccount))
{
return $this->FalseResponse(__FUNCTION__);
}
$sCode = \trim($this->jsonParam('Code', ''));
$aData = $this->getTwoFactorInfo($oAccount);
$sSecret = !empty($aData['Secret']) ? $aData['Secret'] : '';
// $this->Logger()->WriteDump(array(
// $sCode, $sSecret, $aData,
// $this->TwoFactorAuthProvider()->VerifyCode($sSecret, $sCode)
// ));
$this->requestSleep();
return $this->DefaultResponse(__FUNCTION__,
$this->TwoFactorAuthProvider()->VerifyCode($sSecret, $sCode));
}
public function DoClearTwoFactorInfo() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->TwoFactorAuthProvider() ||
!$this->GetCapa(false, static::Capa_TWO_FACTOR, $oAccount))
{
return $this->FalseResponse(__FUNCTION__);
}
$this->StorageProvider()->Clear($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
return $this->DefaultResponse(__FUNCTION__,
$this->getTwoFactorInfo($oAccount, true));
}
protected function TwoFactorAuthProvider() : \RainLoop\Providers\TwoFactorAuth
{
// if ($this->Config()->Get('plugin', 'allow_two_factor_auth', 0))
// if ($this->Config()->Get('plugin', 'force_two_factor_auth', 0))
if (!$this->oTwoFactorAuthProvider) {
require __DIR__ . '/providers/interface.php';
require __DIR__ . '/providers/totp.php';
$this->oTwoFactorAuthProvider = new TwoFactorAuthTotp();
}
return $this->oTwoFactorAuthProvider;
}
protected function getTwoFactorInfo(\RainLoop\Model\Account $oAccount, bool $bRemoveSecret = false) : array
{
$sEmail = $oAccount->ParentEmailHelper();
$mData = null;
$aResult = array(
'User' => '',
'IsSet' => false,
'Enable' => false,
'Secret' => '',
'UrlTitle' => '',
'BackupCodes' => ''
);
if (!empty($sEmail))
{
$aResult['User'] = $sEmail;
$sData = $this->StorageProvider()->Get($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
if ($sData)
{
$mData = \RainLoop\Utils::DecodeKeyValues($sData);
}
}
if (!empty($aResult['User']) &&
!empty($mData['User']) && !empty($mData['Secret']) &&
!empty($mData['BackupCodes']) && $sEmail === $mData['User'])
{
$aResult['IsSet'] = true;
$aResult['Enable'] = isset($mData['Enable']) ? !!$mData['Enable'] : false;
$aResult['Secret'] = $mData['Secret'];
$aResult['BackupCodes'] = $mData['BackupCodes'];
$aResult['UrlTitle'] = $this->Config()->Get('webmail', 'title', '');
}
if ($bRemoveSecret)
{
if (isset($aResult['Secret']))
{
unset($aResult['Secret']);
}
if (isset($aResult['UrlTitle']))
{
unset($aResult['UrlTitle']);
}
if (isset($aResult['BackupCodes']))
{
unset($aResult['BackupCodes']);
}
}
return $aResult;
}
protected function removeBackupCodeFromTwoFactorInfo(\RainLoop\Model\Account $oAccount, string $sCode) : bool
{
if (!$oAccount || empty($sCode))
{
return false;
}
$sData = $this->StorageProvider()->Get($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
if ($sData)
{
$mData = \RainLoop\Utils::DecodeKeyValues($sData);
if (!empty($mData['BackupCodes']))
{
$sBackupCodes = \preg_replace('/[^\d]+/', ' ', ' '.$mData['BackupCodes'].' ');
$sBackupCodes = \str_replace(' '.$sCode.' ', '', $sBackupCodes);
$mData['BackupCodes'] = \trim(\preg_replace('/[^\d]+/', ' ', $sBackupCodes));
return $this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\RainLoop\Utils::EncodeKeyValues($mData)
);
}
}
return false;
}
/*
public function ChangePassword()
{
$oActions = $this->Manager()->Actions();
$oAccount = $oActions->GetAccount();
if (!$oAccount->Email()) {
\trigger_error('ChangePassword failed: empty email address');
throw new ClientException(static::CouldNotSaveNewPassword);
}
$sPrevPassword = $this->jsonParam('PrevPassword');
if ($sPrevPassword !== $oAccount->Password()) {
throw new ClientException(static::CurrentPasswordIncorrect, null, $oActions->StaticI18N('NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT'));
}
$sNewPassword = $this->jsonParam('NewPassword');
if ($this->Config()->Get('plugin', 'pass_min_length', 10) > \strlen($sNewPassword)) {
throw new ClientException(static::NewPasswordShort, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_SHORT'));
}
if ($this->Config()->Get('plugin', 'pass_min_strength', 70) > static::PasswordStrength($sNewPassword)) {
throw new ClientException(static::NewPasswordWeak, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_WEAK'));
}
$bResult = false;
$oConfig = $this->Config();
foreach ($this->getSupportedDrivers() as $name => $class) {
$sFoundedValue = '';
if (\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $oConfig->Get('plugin', "driver_{$name}_allowed_emails"), $sFoundedValue)) {
$name = $class::NAME;
$oLogger = $oActions->Logger();
try
{
$oDriver = new $class(
$oConfig,
$oLogger
);
if (!$oDriver->ChangePassword($oAccount, $sPrevPassword, $sNewPassword)) {
throw new ClientException(static::CouldNotSaveNewPassword);
}
$bResult = true;
if ($oLogger) {
$oLogger->Write("{$name} password changed for {$oAccount->Email()}");
}
}
catch (\Throwable $oException)
{
\trigger_error("{$class} failed: {$oException->getMessage()}");
if ($oLogger) {
$oLogger->Write("ERROR: {$name} password change for {$oAccount->Email()} failed");
$oLogger->WriteException($oException);
// $oLogger->WriteException($oException, \MailSo\Log\Enumerations\Type::WARNING, $name);
}
}
}
}
if (!$bResult) {
\trigger_error("ChangePassword failed");
throw new ClientException(static::CouldNotSaveNewPassword);
}
$oAccount->SetPassword($sNewPassword);
$oActions->SetAuthToken($oAccount);
return $this->jsonResponse(__FUNCTION__, $oActions->GetSpecAuthToken());
}
*/
}

View file

@ -0,0 +1,296 @@
/*
import { trigger as translatorTrigger } from 'Common/Translator';
*/
(rl => { if (rl) {
const
Capa = {
TwoFactor: 'TWO_FACTOR',
TwoFactorForce: 'TWO_FACTOR_FORCE',
},
pString = value => null != value ? '' + value : '',
Remote = new class {
/**
* @param {?Function} fCallback
*/
getTwoFactor(fCallback) {
rl.pluginRemoteRequest(fCallback, 'GetTwoFactorInfo');
}
/**
* @param {?Function} fCallback
*/
createTwoFactor(fCallback) {
rl.pluginRemoteRequest(fCallback, 'CreateTwoFactorSecret');
}
/**
* @param {?Function} fCallback
*/
clearTwoFactor(fCallback) {
rl.pluginRemoteRequest(fCallback, 'ClearTwoFactorInfo');
}
/**
* @param {?Function} fCallback
*/
showTwoFactorSecret(fCallback) {
rl.pluginRemoteRequest(fCallback, 'ShowTwoFactorSecret');
}
/**
* @param {?Function} fCallback
* @param {string} sCode
*/
testTwoFactor(fCallback, sCode) {
rl.pluginRemoteRequest(fCallback, 'TestTwoFactorInfo', {
Code: sCode
});
}
/**
* @param {?Function} fCallback
* @param {boolean} bEnable
*/
enableTwoFactor(fCallback, bEnable) {
rl.pluginRemoteRequest(fCallback, 'EnableTwoFactor', {
Enable: bEnable ? 1 : 0
});
}
/**
* @param {?Function} fCallback
*/
clearTwoFactorInfo(fCallback) {
rl.pluginRemoteRequest(fCallback, 'ClearTwoFactorInfo');
}
};
class TwoFactorAuthSettings
{
constructor() {
this.lock = ko.observable(false);
this.processing = ko.observable(false);
this.clearing = ko.observable(false);
this.secreting = ko.observable(false);
this.viewUser = ko.observable('');
this.twoFactorStatus = ko.observable(false);
this.twoFactorTested = ko.observable(false);
this.viewSecret = ko.observable('');
this.viewBackupCodes = ko.observable('');
this.viewUrlTitle = ko.observable('');
this.viewUrl = ko.observable('');
this.viewEnable_ = ko.observable(false);
this.capaTwoFactor = rl.settings.capa(Capa.TwoFactor);
const fn = iError => iError && this.viewEnable_(false);
this.addComputables({
viewEnable: {
read: this.viewEnable_,
write: (value) => {
value = !!value;
if (value && this.twoFactorTested()) {
this.viewEnable_(value);
Remote.enableTwoFactor(fn, value);
} else {
if (!value) {
this.viewEnable_(value);
}
Remote.enableTwoFactor(fn, false);
}
}
},
viewTwoFactorEnableTooltip: () => {
// translatorTrigger();
return this.twoFactorTested() || this.viewEnable_()
? ''
: rl.i18n('POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_TEST_BEFORE_DESC');
},
viewTwoFactorStatus: () => {
// translatorTrigger();
return rl.i18n('POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_'
+ (this.twoFactorStatus() ? '' : 'NOT_')
+ 'CONFIGURED_DESC'
);
},
twoFactorAllowedEnable: () => this.viewEnable() || this.twoFactorTested()
});
this.onResult = this.onResult.bind(this);
this.onShowSecretResult = this.onShowSecretResult.bind(this);
}
showSecret() {
this.secreting(true);
Remote.showTwoFactorSecret(this.onShowSecretResult);
}
hideSecret() {
this.viewSecret('');
this.viewBackupCodes('');
this.viewUrlTitle('');
this.viewUrl('');
}
createTwoFactor() {
this.processing(true);
Remote.createTwoFactor(this.onResult);
}
logout() {
rl.app.logout();
}
testTwoFactor() {
rl.showPluginPopup(TwoFactorAuthTestPopupView, [this.twoFactorTested]);
}
clearTwoFactor() {
this.viewSecret('');
this.viewBackupCodes('');
this.viewUrlTitle('');
this.viewUrl('');
this.twoFactorTested(false);
this.clearing(true);
Remote.clearTwoFactor(this.onResult);
}
onShow(bLock) {
this.lock(!!bLock);
this.viewSecret('');
this.viewBackupCodes('');
this.viewUrlTitle('');
this.viewUrl('');
}
onHide() {
if (this.lock()) {
location.reload();
}
}
getQr() {
return 'otpauth://totp/' + encodeURIComponent(this.viewUser())
+ '?secret=' + encodeURIComponent(this.viewSecret())
+ '&issuer=' + encodeURIComponent('');
}
onResult(iError, oData) {
this.processing(false);
this.clearing(false);
if (iError) {
this.viewUser('');
this.viewEnable_(false);
this.twoFactorStatus(false);
this.twoFactorTested(false);
this.viewSecret('');
this.viewBackupCodes('');
this.viewUrlTitle('');
this.viewUrl('');
} else {
this.viewUser(pString(oData.Result.User));
this.viewEnable_(!!oData.Result.Enable);
this.twoFactorStatus(!!oData.Result.IsSet);
this.twoFactorTested(!!oData.Result.Tested);
this.viewSecret(pString(oData.Result.Secret));
this.viewBackupCodes(pString(oData.Result.BackupCodes).replace(/[\s]+/g, ' '));
this.viewUrlTitle(pString(oData.Result.UrlTitle));
this.viewUrl(qr.toDataURL({ level: 'M', size: 8, value: this.getQr() }));
}
}
onShowSecretResult(iError, data) {
this.secreting(false);
if (iError) {
this.viewSecret('');
this.viewUrlTitle('');
this.viewUrl('');
} else {
this.viewSecret(pString(data.Result.Secret));
this.viewUrlTitle(pString(data.Result.UrlTitle));
this.viewUrl(qr.toDataURL({ level: 'M', size: 6, value: this.getQr() }));
}
}
onBuild() {
if (this.capaTwoFactor) {
this.processing(true);
Remote.getTwoFactor(this.onResult);
}
}
}
class TwoFactorAuthTestPopupView extends rl.pluginPopupView {
constructor() {
super('TwoFactorAuthTest');
this.addObservables({
code: '',
codeStatus: null,
testing: false
});
this.koTestedTrigger = null;
ko.decorateCommands(this, {
testCodeCommand: self => self.code() && !self.testing()
});
}
testCodeCommand() {
this.testing(true);
Remote.testTwoFactor(iError => {
this.testing(false);
this.codeStatus(!iError);
if (this.koTestedTrigger && this.codeStatus()) {
this.koTestedTrigger(true);
}
}, this.code());
}
clearPopup() {
this.code('');
this.codeStatus(null);
this.testing(false);
this.koTestedTrigger = null;
}
onShow(koTestedTrigger) {
this.clearPopup();
this.koTestedTrigger = koTestedTrigger;
}
}
rl.addSettingsViewModel(
TwoFactorAuthSettings,
'TwoFactorAuthSettings',
'POPUPS_TWO_FACTOR_CFG/LEGEND_TWO_FACTOR_AUTH',
'two-factor-auth'
);
}})(window.rl);

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "Zwei-Faktor-Authentifizierung erlauben"
LABEL_FORCE_TWO_STEP = "Zwei-Faktor-Authentifizierung erzwingen"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Zwei-Faktor-Authentifizierung"
LABEL_ENABLE_TWO_FACTOR = "Zwei-Faktor-Authentifizierung aktivieren"
LABEL_TWO_FACTOR_USER = "Benutzer"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Geheimnis"
LABEL_TWO_FACTOR_BACKUP_CODES = "Sicherungscodes"
BUTTON_CREATE = "Neues Geheimnis erstellen"
BUTTON_ACTIVATE = "Aktivieren"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Geheimnis einblenden"
BUTTON_HIDE_SECRET = "Geheminis ausblenden"
TWO_FACTOR_REQUIRE_DESC = "Ihr Benutzerkonto erfordert die Einrichtung der Zwei-Faktor-Authentifizierung."
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Konfiguriert"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nicht konfiguriert"
TWO_FACTOR_SECRET_DESC = "Importieren Sie diese Information in Ihre Google-Authenticator-Anwendung (oder andere TOTP-Anwendung), indem Sie den unten bereitgestellten QR-Code verwenden oder den Code manuell eingeben."
TWO_FACTOR_BACKUP_CODES_DESC = "Sollten Sie keine Codes über den Google Authenticator erhalten, können Sie einen Sicherungscode zur Anmeldung verwenden. Der Sicherungscode wird inaktiv, sobald Sie ihn zur Anmeldung verwendet haben."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Sie können diese Einstellung nicht ohne vorherigen Test verändern."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Zwei-Faktor-Authentifizierung"
LABEL_CODE = "Code"
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "Zwei-Faktor-Authentifizierung konfigurieren"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "Zwei-Faktor-Authentifizierung erforderlich"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "Fehler bei Zwei-Faktor-Authentifizierung"
[LOGIN]
LABEL_VERIFICATION_CODE = "Verifizierungscode"
LABEL_DONT_ASK_VERIFICATION_CODE = "Für zwei Wochen nicht nach dem Code fragen"

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "Allow 2-Step Verification"
LABEL_FORCE_TWO_STEP = "Enforce 2-Step Verification"
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "2-Step verification test"
LABEL_CODE = "Code"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "2-Step Verification (TOTP)"
LABEL_ENABLE_TWO_FACTOR = "Enable 2-Step verification"
LABEL_TWO_FACTOR_USER = "User"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Secret"
LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes"
BUTTON_CREATE = "Create a secret"
BUTTON_ACTIVATE = "Activate"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Show Secret"
BUTTON_HIDE_SECRET = "Hide Secret"
TWO_FACTOR_REQUIRE_DESC = "Your account requires 2-Step verification configuration."
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured"
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After youve used a backup code to sign in, it will become inactive.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "Configure 2-Step verification"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "Two factor verification required"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "Two factor verification error"
[LOGIN]
LABEL_VERIFICATION_CODE = "Verification Code"
LABEL_DONT_ASK_VERIFICATION_CODE = "Don't ask for the code for 2 weeks"

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "Activar la verificación de 2 pasos"
LABEL_FORCE_TWO_STEP = "Forzar la Autenticación en 2 pasos"
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Prueba de verificación de 2 pasos"
LABEL_CODE = "Código"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Verificación de 2 Pasos"
LABEL_ENABLE_TWO_FACTOR = "Activar la verificación de 2 pasos"
LABEL_TWO_FACTOR_USER = "Usuario"
LABEL_TWO_FACTOR_STATUS = "Estado"
LABEL_TWO_FACTOR_SECRET = "Clave secreta"
LABEL_TWO_FACTOR_BACKUP_CODES = "Códigos de copia de seguridad"
BUTTON_CREATE = "Crear nueva clave secreta"
BUTTON_ACTIVATE = "Activate"
LINK_TEST = "probar"
BUTTON_SHOW_SECRET = "Mostrar clave secreta"
BUTTON_HIDE_SECRET = "Ocultar clave secreta"
TWO_FACTOR_REQUIRE_DESC = "Se requiere que configure la verificación de 2-pasos."
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurado"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "No configurado"
TWO_FACTOR_SECRET_DESC = "Importar esta información en su cliente Google Authenticator (u otro cliente TOTP) utilizando el código QR se indica debajo o introduciendo el código manualmente."
TWO_FACTOR_BACKUP_CODES_DESC = "Si usted no puede recibir los códigos a través de Google Authenticator, puede utilizar códigos de copia de seguridad para firmar pulg Después de que usted ha utilizado un código de copia de seguridad para iniciar sesión, se convertirá en inactiva."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "Configurar verificación de 2-Pasos"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "Factor de verificación en dos pasos requerido"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "Error de verificación en dos pasos"
[LOGIN]
LABEL_VERIFICATION_CODE = "Código de verificación"
LABEL_DONT_ASK_VERIFICATION_CODE = "No solicitar el código de verificación durante 2 semanas"

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "Autoriser l'authentification en deux étapes"
LABEL_FORCE_TWO_STEP = "Forcer l'authentification en deux étapes"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Authentification en deux étapes"
LABEL_ENABLE_TWO_FACTOR = "Activer l'authentification en deux étapes"
LABEL_TWO_FACTOR_USER = "Utilisateur"
LABEL_TWO_FACTOR_STATUS = "Statut"
LABEL_TWO_FACTOR_SECRET = "Secret"
LABEL_TWO_FACTOR_BACKUP_CODES = "Code de sauvegarde"
BUTTON_CREATE = "Créer une nouvelle clé secrète"
BUTTON_ACTIVATE = "Activer"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Montrer la clé secrète"
BUTTON_HIDE_SECRET = "Masquer la clé secrète"
TWO_FACTOR_REQUIRE_DESC = "Votre compte requiert la configuration d'une vérification en deux étapes."
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configuré"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configuré"
TWO_FACTOR_SECRET_DESC = "Importez ces informations dans votre client Google Authenticator (ou un autre client TOTP) en utilisant le code QR fourni ci-dessous ou en entrant les valeurs manuellement."
TWO_FACTOR_BACKUP_CODES_DESC = "Si vous ne pouvez pas recevoir les codes par Google Authenticator, vous pouvez utiliser les codes de sauvegarde pour vous connecter. Après avoir fait cela, il deviendra inactif."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Vous ne pouvez pas modifier ce paramètre avant de l'avoir essayé."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Test d'authentification en deux étapes"
LABEL_CODE = "Code"
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "Configurer l'authentification en deux étapes"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "Double authentification requise"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "Erreur lors de la double authentification"
[LOGIN]
LABEL_VERIFICATION_CODE = "Code de vérification"
LABEL_DONT_ASK_VERIFICATION_CODE = "Ne plus demander le code pendant 2 semaines"

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "2 lépcsős hitelesítés engedélyezése"
LABEL_FORCE_TWO_STEP = "2 lépcsős hitelesítés kényszerítése"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "2 lépcsős hitelesítés"
LABEL_ENABLE_TWO_FACTOR = "2 lépcsős hitelesítés engedélyezése"
LABEL_TWO_FACTOR_USER = "Felhasználó"
LABEL_TWO_FACTOR_STATUS = "Állapot"
LABEL_TWO_FACTOR_SECRET = "Titok"
LABEL_TWO_FACTOR_BACKUP_CODES = "Biztonsági kódok"
BUTTON_CREATE = "Új titok létrehozás"
BUTTON_ACTIVATE = "Aktivál"
LINK_TEST = "teszt"
BUTTON_SHOW_SECRET = "Titok megjelenítése"
BUTTON_HIDE_SECRET = "Titok elrejtése"
TWO_FACTOR_REQUIRE_DESC = "A fiókhoz 2 lépcsős hitelesítés szükséges."
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Beállítva"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nincs beállítva"
TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Tesztelés nélkül nem lehet megváltoztatni ezt a beállítást."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "2-lépéses hitelesítés teszt"
LABEL_CODE = "Kód"
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "2 lépcsős hitelesítés beállítása"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "Kétlépcsős azonosítás kötelező"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "Kétlépcsős azonosítás hiba"
[LOGIN]
LABEL_VERIFICATION_CODE = "Megerősítő kód"
LABEL_DONT_ASK_VERIFICATION_CODE = "Két hétig ne kérje a kódot"

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "2-Stap verificatie toestaan"
LABEL_FORCE_TWO_STEP = "2-Stap verificatie afdwingen"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "2-Stap verificatie"
LABEL_ENABLE_TWO_FACTOR = "Gebruik 2-Stap verificatie"
LABEL_TWO_FACTOR_USER = "Gebruikersnaam"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Geheime sleutel"
LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes"
BUTTON_CREATE = "Nieuwe geheime sleutel aanmaken"
BUTTON_ACTIVATE = "Activate"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Bekijk geheime sleutel"
BUTTON_HIDE_SECRET = "Verberg geheime sleutel"
TWO_FACTOR_REQUIRE_DESC = "Uw account vereist 2-Stap verificatie configuratie."
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Geconfigureerd"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Niet geconfigureerd"
TWO_FACTOR_SECRET_DESC = "Importeer deze informatie in uw Google Authenticator-client (of andere TOTP-client) door gebruik te maken van de QR code hier beneden of door de code handmatig in te voeren."
TWO_FACTOR_BACKUP_CODES_DESC = "Als u geen codes ontvangt via de Google Authenticator kunt u de backup codes gebruiken om in te loggen. Na gebruik van de backup code wordt deze inactief."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "U kunt 2-stap verificatie niet activeren voordat u het succesvol getest heeft."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "2-Stap verificatie test"
LABEL_CODE = "Code"
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "Configureer 2-stap verificatie"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "2-Stap verificatie vereist"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "2-Stap verificatie fout"
[LOGIN]
LABEL_VERIFICATION_CODE = "Verificatie Code"
LABEL_DONT_ASK_VERIFICATION_CODE = "Vraag 2 weken niet naar code"

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "Låt 2-tvåstegsverifiering"
LABEL_FORCE_TWO_STEP = "Driva 2-tvåstegsverifiering"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Tvåstegsverifiering (TOTP)"
LABEL_ENABLE_TWO_FACTOR = "Aktivera tvåstegsverifiering"
LABEL_TWO_FACTOR_USER = "Användare"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Hemlig kod"
LABEL_TWO_FACTOR_BACKUP_CODES = "Backupkoder"
BUTTON_CREATE = "Skapa ny hemlig kod"
BUTTON_ACTIVATE = "Aktivera"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Visa hemlig kod"
BUTTON_HIDE_SECRET = "Göm hemlig kod"
TWO_FACTOR_REQUIRE_DESC = "Ditt konto kräver tvåstegsverifiering."
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Konfigurerad"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Inte konfigurarad"
TWO_FACTOR_SECRET_DESC = "Importera denna information till din Google Authenticator klient (eller annan TOTP klient) med denna QR kod eller manuellt med koden här nedan."
TWO_FACTOR_BACKUP_CODES_DESC = "Om du inte kan ta emot koderna med Google Authenticator, så använd backupkoderna för att logga in. När du använt backupkoderna för att logga in så blir dom inaktiva."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Du kan inte ändra denna inställning innan test."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Tvåstegsverifieringstest"
LABEL_CODE = "Kod"
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "Konfigurera tvåstegsverifiering"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "Tvåstegsverifiering krävs"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "Tvåstegsverifieringsfel"
[LOGIN]
LABEL_VERIFICATION_CODE = "Verifikationskod"
LABEL_DONT_ASK_VERIFICATION_CODE = "Fråga inte efter koden på 2 veckor"

View file

@ -0,0 +1,32 @@
[TAB_SECURITY]
LABEL_ALLOW_TWO_STEP = "允许两步验证"
LABEL_FORCE_TWO_STEP = "强制使用两步验证"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "两步验证 (TOTP)"
LABEL_ENABLE_TWO_FACTOR = "启用两步验证"
LABEL_TWO_FACTOR_USER = "用户"
LABEL_TWO_FACTOR_STATUS = "状态"
LABEL_TWO_FACTOR_SECRET = "密钥"
LABEL_TWO_FACTOR_BACKUP_CODES = "备用代码"
BUTTON_CREATE = "创建新密钥"
BUTTON_ACTIVATE = "启用"
LINK_TEST = "测试"
BUTTON_SHOW_SECRET = "显示密钥"
BUTTON_HIDE_SECRET = "隐藏密钥"
TWO_FACTOR_REQUIRE_DESC = "您的账户需要设置两步验证。"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "已设置"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "未设置"
TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。\n"
TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "完成测试前你无法更改此设置。"
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "两步验证测试"
LABEL_CODE = "代码"
[SETTINGS_SECURITY]
LABEL_CONFIGURE_TWO_FACTOR = "配置两步验证"
[NOTIFICATIONS]
ACCOUNT_TWO_FACTOR_AUTH_REQUIRED = "需要进行两步验证"
ACCOUNT_TWO_FACTOR_AUTH_ERROR = "两步验证错误"
[LOGIN]
LABEL_VERIFICATION_CODE = "验证码"
LABEL_DONT_ASK_VERIFICATION_CODE = "在两周内不再询问验证码"

View file

@ -0,0 +1,8 @@
<?php
interface TwoFactorAuthInterface
{
public function Label() : string;
public function VerifyCode(string $sSecret, string $sCode) : bool;
public function CreateSecret() : string;
}

View file

@ -0,0 +1,102 @@
<?php
class TwoFactorAuthTotp implements TwoFactorAuthInterface
{
public function Label() : string
{
return 'Two Factor Authenticator Code';
}
public function VerifyCode(string $sSecret, string $sCode) : bool
{
$key = static::Base32Decode($sSecret);
$algo = 'SHA1'; // Google Authenticator doesn't support SHA256
$digits = 6; // Google Authenticator doesn't support 8
$modulo = \pow(10, $digits);
$timeSlice = \floor(\time() / 30);
$discrepancy = 1;
for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
// Pack time into binary string
$counter = \str_pad(\pack('N*', $timeSlice + $i), 8, "\x00", STR_PAD_LEFT);
// Hash it with users secret key
$hm = \hash_hmac($algo, $counter, $key, true);
// Unpak 4 bytes of the result, use last nipple of result as index/offset
$value = \unpack('N', \substr($hm, (\ord(\substr($hm, -1)) & 0x0F), 4));
// Only 32 bits
$value = $value[1] & 0x7FFFFFFF;
$value = \str_pad($value % $modulo, $digits, '0', STR_PAD_LEFT);
if (\hash_equals($value, $sCode)) {
return true;
}
}
return false;
}
public function CreateSecret() : string
{
$CHARS = \array_keys(static::$map);
$length = 16;
$secret = '';
while (0 < $length--) {
$secret .= $CHARS[\random_int(0,31)];
}
return $secret;
}
protected static $map = array(
'A' => 0, // ord 65
'B' => 1,
'C' => 2,
'D' => 3,
'E' => 4,
'F' => 5,
'G' => 6,
'H' => 7,
'I' => 8,
'J' => 9,
'K' => 10,
'L' => 11,
'M' => 12,
'N' => 13,
'O' => 14,
'P' => 15,
'Q' => 16,
'R' => 17,
'S' => 18,
'T' => 19,
'U' => 20,
'V' => 21,
'W' => 22,
'X' => 23,
'Y' => 24,
'Z' => 25, // ord 90
'2' => 26, // ord 50
'3' => 27,
'4' => 28,
'5' => 29,
'6' => 30,
'7' => 31 // ord 55
);
protected static function Base32Decode(string $data)
{
$data = \strtoupper(\rtrim($data, "=\x20\t\n\r\0\x0B"));
$dataSize = \strlen($data);
$buf = 0;
$bufSize = 0;
$res = '';
for ($i = 0; $i < $dataSize; ++$i) {
$c = $data[$i];
if (isset(static::$map[$c])) {
$buf = ($buf << 5) | static::$map[$c];
$bufSize += 5;
if ($bufSize > 7) {
$bufSize -= 8;
$res .= \chr(($buf & (0xff << $bufSize)) >> $bufSize);
}
}
}
return $res;
}
}

View file

@ -0,0 +1,29 @@
<div class="modal fade b-two-factor-test-content g-ui-user-select-none" data-bind="modal: modalVisibility">
<div>
<div class="modal-header">
<button type="button" class="close" data-bind="command: cancelCommand">×</button>
<h3 data-i18n="POPUPS_TWO_FACTOR_TEST/TITLE_TEST_CODE"></h3>
</div>
<div class="modal-body">
<div class="form-horizontal">
<br />
<div class="control-group">
<label class="control-label">
<span data-i18n="POPUPS_TWO_FACTOR_TEST/LABEL_CODE"></span>
</label>
<div class="controls">
<input type="text" class="uiInput inputName"
autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: code, onEnter: testCodeCommand" />
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn" data-bind="command: testCodeCommand, css: { 'btn-success': true === codeStatus(), 'btn-danger': false === codeStatus() }">
<i data-bind="css: {'icon-ok': !testing(), 'icon-spinner': testing()}"></i>
<span data-i18n="GLOBAL/TEST"></span>
</a>
</div>
</div>
</div>

View file

@ -0,0 +1,109 @@
<div class="b-settings-two-factor">
<div class="form-horizontal" data-bind="visible: capaAutoLogout">
<div class="legend" data-i18n="POPUPS_TWO_FACTOR_CFG/LEGEND_TWO_FACTOR_AUTH"></div>
<div class="control-group" data-bind="visible: capaTwoFactor">
<label class="control-label"></label>
<div class="controls">
<i class="fontastic">🔒</i>
&nbsp;
<span class="g-ui-link" tabindex="0" data-i18n="SETTINGS_SECURITY/LABEL_CONFIGURE_TWO_FACTOR" data-bind="click: configureTwoFactor, onSpace: configureTwoFactor, onEnter: configureTwoFactor"></span>
</div>
</div>
</div>
<div>
<div class="modal-header">
<button type="button" class="close" data-bind="visible: viewEnable() || !lock(), command: cancelCommand">×</button>
<h3 data-i18n="POPUPS_TWO_FACTOR_CFG/LEGEND_TWO_FACTOR_AUTH"></h3>
</div>
<div class="modal-body">
<div class="form-horizontal" data-bind="visible: capaTwoFactor" style="margin-top: 10px;">
<div class="control-group" data-bind="visible: twoFactorStatus">
<div class="controls">
<div style="display: inline-block" data-bind="attr:{title: viewTwoFactorEnableTooltip}">
<div data-bind="component: {
name: 'Checkbox',
params: {
label: 'POPUPS_TWO_FACTOR_CFG/LABEL_ENABLE_TWO_FACTOR',
enable: twoFactorAllowedEnable,
value: viewEnable,
inline: true
}
}"></div>
</div>
&nbsp;&nbsp;&nbsp;
<span class="g-ui-link" data-bind="click: testTwoFactor, visible: twoFactorStatus"
data-i18n="POPUPS_TWO_FACTOR_CFG/LINK_TEST"></span>
</div>
</div>
<div class="control-group">
<label class="control-label">
<span data-i18n="POPUPS_TWO_FACTOR_CFG/LABEL_TWO_FACTOR_USER"></span>
</label>
<div class="controls" style="padding-top: 5px;">
<strong><span data-bind="text: viewUser"></span></strong>
<div style="padding-top: 15px;" data-bind="visible: lock">
<blockquote>
<p class="muted width100-on-mobile" style="width: 550px" data-i18n="POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_REQUIRE_DESC"></p>
</blockquote>
</div>
</div>
</div>
<div class="control-group" data-bind="visible: '' === viewSecret() && twoFactorStatus() && !clearing()">
<div class="controls" style="padding-top: 5px;">
<strong data-bind="visible: secreting">...</strong>
<span class="g-ui-link" data-bind="click: showSecret, visible: !secreting()"
data-i18n="POPUPS_TWO_FACTOR_CFG/BUTTON_SHOW_SECRET"></span>
</div>
</div>
<div class="control-group" data-bind="visible: '' !== viewSecret()">
<label class="control-label">
<span data-i18n="POPUPS_TWO_FACTOR_CFG/LABEL_TWO_FACTOR_SECRET"></span>
</label>
<div class="controls" style="padding-top: 5px;">
<strong data-bind="text: viewSecret"></strong>
&nbsp;&nbsp;
<span class="g-ui-link" data-bind="click: hideSecret" data-i18n="POPUPS_TWO_FACTOR_CFG/BUTTON_HIDE_SECRET"></span>
<br />
<br />
<blockquote>
<p class="muted width100-on-mobile" style="width: 550px" data-i18n="POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_DESC"></p>
</blockquote>
<!-- ko if: '' !== viewUrl() -->
<img style="margin-left: -7px;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2P8DwQACgAD/il4QJ8AAAAASUVORK5CYII=" data-bind="attr: {'src': viewUrl}" />
<!-- /ko -->
</div>
</div>
<div class="control-group" data-bind="visible: '' !== viewBackupCodes()">
<label class="control-label">
<span data-i18n="POPUPS_TWO_FACTOR_CFG/LABEL_TWO_FACTOR_BACKUP_CODES"></span>
</label>
<div class="controls" style="padding-top: 5px;">
<pre data-bind="text: viewBackupCodes" style="width: 230px; word-break: break-word;"></pre>
<br />
<blockquote>
<p class="muted width100-on-mobile" style="width: 550px" data-i18n="POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_BACKUP_CODES_DESC"></p>
</blockquote>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn pull-left" data-bind="visible: lock, click: logout">
<i class="fontastic"></i>
<span data-i18n="GLOBAL/LOGOUT"></span>
</a>
<a class="btn btn-danger" data-bind="click: clearTwoFactor, visible: twoFactorStatus">
<i class="fontastic" data-bind="css: {'icon-spinner': clearing()}"></i>
<span data-i18n="GLOBAL/CLEAR"></span>
</a>
<a class="btn" data-bind="click: createTwoFactor, visible: !twoFactorStatus()">
<i class="fontastic" data-bind="css: {'icon-spinner': processing()}"></i>
<span data-i18n="POPUPS_TWO_FACTOR_CFG/BUTTON_ACTIVATE"></span>
</a>
<a class="btn" data-bind="command: cancelCommand, visible: viewEnable() || !lock()">
<i class="icon-ok" ></i>
<span data-i18n="GLOBAL/DONE"></span>
</a>
</div>
</div>
</div>