TOTP: UI fixes and improvements

This commit is contained in:
yflory 2023-06-20 12:52:04 +02:00
parent 9aac9d1c2f
commit 318202531f
6 changed files with 308 additions and 226 deletions

View file

@ -6,35 +6,24 @@ define([
'/customize/messages.js',
'/customize/pages.js'
], function (Config, $, h, UI, Msg, Pages) {
Msg.recovery_header = "Account recovery"; // XXX
Msg.recovery_mfa_description = "If you have lost access to your Two-Factor Authentication method you can disable 2FA for your account using your recovery code. Please start by entering your login and password:";
Msg.recovery_mfa_secret = "Please enter your recovery code to disable 2FA for your account:";
Msg.recovery_mfa_secret_ph = "Recovery code";
Msg.mfa_disable = "Disable 2FA"; // XXX also in settings
Msg.continue = "Continue"; // XXX also in settings
Msg.recovery_forgot = 'Forgot recovery code';
Msg.recovery_forgot_text = 'Please copy the following information and <a href="mailto:{0}">email it</a> toyour instance administrators';
Msg.recovery_mfa_wrong = "Invalid username or password";
Msg.recovery_mfa_error = "Unknown error. Please reload and try again.";
Msg.recovery_mfa_disabled = "Multi-factor authentication is already disabled for this account.";
return function () {
document.title = Msg.register_header;
var tos = $(UI.createCheckbox('accept-terms')).find('.cp-checkmark-label').append(Msg.register_acceptTerms).parent()[0];
var termsLink = Pages.customURLs.terms;
$(tos).find('a').attr({
href: termsLink,
target: '_blank',
tabindex: '-1',
});
Msg.recovery_header = "Recover account"; // XXX
Msg.recovery_totp = "Disable TOTP on your account"; // XXX
Msg.recovery_totp_description = "If you've locked yourselves out of your CryptPad account because of a TOTP (Time based One Time Password) multi-factor authentication, you can use this form to disable this protection.";
Msg.recovery_totp_beta = 'The TOTP multi-factor authentication has just been released. To avoid accidentaly locking accounts, the support team will temporarily agree to disable the protection even if you forgot your secret recovery key. <strong>If you forgot your recovery key, please copy the proof of ownership and send it to <a href="mailto:{0}">{0}</a></strong>';
Msg.recovery_totp_login = "Please enter your login credentials";
Msg.recovery_totp_secret = "Please enter your secret recovery key";
Msg.recovery_totp_secret_ph = "Secret recovery key";
Msg.recovery_totp_proof = "Proof of ownership";
Msg.recovery_totp_continue = "Continue";
Msg.recovery_totp_disable = "Disable TOTP";
Msg.recovery_totp_method_email = "Manual recovery by email";
Msg.recovery_totp_method_secret = "Automatic recovery by secret key";
Msg.recovery_totp_wrong = "Invalid username or password";
Msg.recovery_totp_error = "Unknown error. Please reload and try again.";
Msg.recovery_totp_disabled = "Multi-factor authentication is already disabled for this account.";
document.title = Msg.recovery_header;
var frame = function (content) {
return [
@ -48,29 +37,13 @@ Msg.recovery_totp_disabled = "Multi-factor authentication is already disabled fo
];
};
if (Config.restrictRegistration) {
return frame([
h('div.cp-restricted-registration', [
h('p', Msg.register_registrationIsClosed),
])
]);
}
var termsCheck;
if (termsLink) {
termsCheck = h('div.checkbox-container', tos);
}
return frame([
h('div.row.cp-recovery-det', [
h('div#userForm.form-group.hidden.col-md-12', [
h('h2', Msg.recovery_totp),
h('div.cp-recovery-desc', [
Msg._getKey('recovery_totp_description', [ Pages.Instance.name ]),
]),
h('div.hidden.col-md-3'),
h('div#userForm.form-group.hidden.col-md-6', [
h('div.cp-recovery-step.step1', [
h('div.alert.alert-danger.wrong-cred.cp-hidden', Msg.recovery_totp_wrong),
h('label', Msg.recovery_totp_login),
h('p', Msg.recovery_mfa_description),
h('div.alert.alert-danger.wrong-cred.cp-hidden', Msg.recovery_mfa_wrong),
h('input.form-control#username', {
type: 'text',
autocomplete: 'off',
@ -85,41 +58,40 @@ Msg.recovery_totp_disabled = "Multi-factor authentication is already disabled fo
placeholder: Msg.login_password,
}),
h('div.cp-recover-button',
h('button.btn.btn-primary#cp-recover-login', Msg.recovery_totp_continue)
h('button.btn.btn-primary#cp-recover-login', Msg.continue)
)
]),
h('div.cp-recovery-step.step2', { style: 'display: none;' }, [
h('div.cp-recovery-method', [
h('h3', Msg.recovery_totp_method_secret),
h('label', Msg.recovery_totp_secret),
h('input.form-control#totprecovery', {
type: 'text',
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'off',
spellcheck: false,
placeholder: Msg.recovery_totp_secret_ph,
autofocus: true,
}),
h('div.cp-recover-button',
h('button.btn.btn-primary#cp-recover', Msg.recovery_totp_disable)
)
h('label', Msg.recovery_mfa_secret),
h('input.form-control#mfarecovery', {
type: 'text',
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'off',
spellcheck: false,
placeholder: Msg.recovery_mfa_secret_ph,
autofocus: true,
}),
h('div.cp-recovery-forgot', [
h('i.fa.fa-caret-right'),
h('span', Msg.recovery_forgot)
]),
h('div.cp-recovery-desc', Msg.settings_kanbanTagsOr),
h('div.cp-recovery-method', [
h('h3', Msg.recovery_totp_method_email),
Config.adminEmail ? UI.setHTML(h('div.alert.alert-warning'),
Msg._getKey('recovery_totp_beta', [Config.adminEmail])) : undefined,
h('label', Msg.recovery_totp_proof),
h('div.cp-recovery-alt', { style: 'display: none;' }, [
UI.setHTML(h('div'),
Msg._getKey('recovery_forgot_text', [Config.adminEmail || ''])),
h('textarea.cp-recover-email', {readonly: 'readonly'}),
h('button.btn.btn-secondary#totpcopyproof', Msg.copyToClipboard),
h('button.btn.btn-secondary#mfacopyproof', Msg.copyToClipboard),
]),
h('div.cp-recover-button',
h('button.btn.btn-primary#cp-recover', Msg.mfa_disable)
)
]),
h('div.cp-recovery-step.step-info', { style: 'display: none;' }, [
h('div.alert.alert-info.cp-hidden.disabled', Msg.recovery_totp_disabled),
h('div.alert.alert-danger.cp-hidden.unknown-error', Msg.recovery_totp_error),
h('div.alert.alert-info.cp-hidden.disabled', Msg.recovery_mfa_disabled),
h('div.alert.alert-danger.cp-hidden.unknown-error', Msg.recovery_mfa_error),
]),
])
]),
h('div.hidden.col-md-3'),
])
]);
};

View file

@ -59,7 +59,14 @@
.cp-recovery-det {
.cp-recover-button {
text-align: center;
text-align: right;
}
.cp-recovery-forgot {
cursor: pointer;
i {
margin-right: 5px;
width: 10px;
}
}
.cp-recovery-method {
padding: 5px;

View file

@ -1507,8 +1507,10 @@ define([
};
Messages.settings_otp_code = "OTP code"; // XXX KEY ALREADY ADDED IN www/settings/inner.js
Messages.loading_enter_otp = "This account is protected with MFA. Please enter your OTP code."; // XXX
Messages.settings_otp_invalid = "Invalid OTP code";
Messages.settings_otp_invalid = "Invalid OTP code"; // Same
Messages.loading_enter_otp = "This account is protected with Two-Factor Authentication. Please enter your verification code."; // XXX
Messages.loading_recover = 'Unable to get a code? <a href="/recovery/">Recover your account</a>';
UI.getOTPScreen = function (cb, exitable, err) {
@ -1529,7 +1531,8 @@ define([
spellcheck: false,
}),
btn = h('button.btn.btn-primary', Messages.ui_confirm)
])
]),
UI.setHTML(h('p.cp-password-recovery'), Messages.loading_recover)
]);
var $input = $(input);
var $btn = $(btn).click(function () {

View file

@ -35,28 +35,36 @@ define([
// text and password input fields
var $uname = $('#username');
var $passwd = $('#password');
var $recoveryKey = $('#totprecovery');
var $copyProof = $('#totpcopyproof');
var $recoveryKey = $('#mfarecovery');
var $copyProof = $('#mfacopyproof');
var $step1 = $('.cp-recovery-step.step1');
var $step2 = $('.cp-recovery-step.step2');
var $stepInfo = $('.cp-recovery-step.step-info');
var $totpProof = $('textarea.cp-recover-email');
var $mfaProof = $('textarea.cp-recover-email');
var $forgot = $('.cp-recovery-forgot');
var $alt = $('.cp-recovery-alt');
[ $uname, $passwd]
.some(function ($el) { if (!$el.val()) { $el.focus(); return true; } });
var totpStep2 = function () {
var mfaStep2 = function () {
$step1.hide();
$step2.show();
};
var totpStepInfo = function (cls) {
var mfaStepInfo = function (cls) {
$step1.hide();
$stepInfo.find('.alert').toggleClass('cp-hidden', true);
$stepInfo.find(cls).toggleClass('cp-hidden', false);
$stepInfo.show();
};
$forgot.click(function () {
$alt.toggle();
if ($alt.is(':visible')) { $forgot.find('i').attr('class', 'fa fa-caret-down'); }
else { $forgot.find('i').attr('class', 'fa fa-caret-right'); }
});
var proofStr;
var addProof = function (blockKeys) {
var pub = blockKeys.sign.publicKey;
@ -69,7 +77,7 @@ define([
var proof = Nacl.sign.detached(Nacl.util.decodeUTF8(Sortify(toSign)), sec);
toSign.proof = Nacl.util.encodeBase64(proof);
proofStr = JSON.stringify(toSign, 0, 2);
$totpProof.html(proofStr);
$mfaProof.html(proofStr);
};
$copyProof.click(function () {
@ -81,7 +89,9 @@ define([
}
});
var revokeTOTP = function (blockKeys) {
var blockKeys, blockHash, uname;
var revokeTOTP = function () {
var recoveryKey = $recoveryKey.val().trim();
if (!recoveryKey || recoveryKey.length !== 32) {
return void UI.warn(Messages.error); // XXX error message?
@ -96,38 +106,40 @@ define([
return void UI.warn(Messages.error);
}
// XXX redirect to login?
return void UI.log(Messages.ui_success);
UI.log(Messages.ui_success);
LocalStore.login(undefined, blockHash, uname, function () {
Login.redirect();
});
});
};
var $recoverLogin = $('button#cp-recover-login');
var $recoverConfirm = $('button#cp-recover');
var blockKeys;
$recoverLogin.click(function () {
UI.addLoadingScreen({
loadingText: Messages.login_hashing
});
var name = $uname.val();
uname = $uname.val();
var pw = $passwd.val();
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, pw, Login.requiredBytes, function (bytes) {
Login.Cred.deriveFromPassphrase(uname, pw, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
var blockHash = result.blockHash;
blockHash = result.blockHash;
var parsed = Block.parseBlockHash(blockHash);
addProof(result.blockKeys);
blockKeys = result.blockKeys;
Util.getBlock(parsed.href, {}, function (err, v) {
UI.removeLoadingScreen();
if (v && !err) {
return totpStepInfo('.disabled');
return mfaStepInfo('.disabled');
}
if (err === 401) {
return totpStep2(result.blockKeys);
return mfaStep2(result.blockKeys);
}
if (err === 404) {
return $step1.find('.wrong-cred').toggleClass('cp-hidden', false);
}
totpStepInfo('.unknown-error');
mfaStepInfo('.unknown-error');
});
});
}, 100);
@ -137,8 +149,7 @@ define([
multiple: true
}, function () {
if (!blockKeys) { return; }
// XXX disable TOTP automatically
revokeTOTP(blockKeys);
revokeTOTP();
});
});

View file

@ -86,13 +86,13 @@
}
}
.cp-settings-change-password, .cp-settings-own-drive, .cp-settings-delete {
.cp-password-container {
[type="password"], [type="text"] {
width: @sidebar_button-width;
flex: unset;
}
button {
margin-top: 5px;
margin-left: 10px;
}
}
.cp-settings-drive-backup {
@ -103,10 +103,47 @@
margin-right: 5px;
}
}
.cp-settings-totp {
.cp-settings-mfa {
.cp-settings-qr {
img {
border: 10px solid white;
border-radius: 10px;
}
margin: 10px 20px 20px 0;
}
}
.cp-settings-mfa-hint {
color: @cp_sidebar-hint;
}
.cp-settings-mfa-status {
& > i {
margin-right: 5px;
}
margin: 1em 0;
}
.cp-settings-mfa {
.cp-password-container {
flex-wrap: wrap;
input {
flex-shrink: 1;
max-width: 400px;
}
label {
width: 100%;
font-weight: unset;
margin-bottom: 5px;
}
}
.cp-settings-qr-container {
display: flex;
align-items: center;
.cp-settings-qr-code {
input {
max-width: 250px;
}
button {
margin-top: 10px
}
}
}
}

View file

@ -49,19 +49,27 @@ define([
var privateData;
var sframeChan;
Messages.settings_totpTitle = "TOTP"; // XXX
Messages.settings_mfaTitle = "Two-Factor Authentication (2FA)"; // XXX
Messages.settings_mfaHint = "Protect your account..."; // XXX
Messages.settings_cat_access = "Security"; // XXX
Messages.settings_totp_enable = "Enable TOTP"; // XXX
Messages.settings_totp_disable = "Disable TOTP"; // XXX
Messages.settings_totp_generate = "Generate secret"; // XXX
Messages.settings_otp_code = "OTP code"; // XXX
Messages.done = "Done";
Messages.continue = "Continue";
Messages.mfa_setup_label = "To enable 2FA, please begin by entering your account password"; // XXX
Messages.mfa_setup_button = "Begin 2FA setup"; // XXX
Messages.mfa_revoke_label = "To disable 2FA, please begin by entering your account password"; // XXX
Messages.mfa_revoke_button = "Start disable 2FA"; // XXX
Messages.mfa_revoke_code = "Please enter your verification code";
Messages.mfa_status_on = "2FA is active on this account";
Messages.mfa_status_off = "2FA is not active on this account";
Messages.mfa_recovery_title = "Save this recovery code now";
Messages.mfa_recovery_hint = "If you loose access to your authenticator...........";
Messages.mfa_recovery_warning = "This code will not be shown again...........";
Messages.mfa_enable = "Enable 2FA";
Messages.mfa_disable = "Disable 2FA";
Messages.settings_otp_code = "Verification code"; // XXX
Messages.settings_otp_invalid = "Invalid OTP code"; // XXX
Messages.settings_totp_tuto = "Scan this QR code with a authenticator application. Obtain a valid authentication code and confirm before it expires."; // XXX
Messages.settings_totp_confirm = "Enable TOTP with this secret"; // XXX
Messages.settings_totp_recovery_header = "Recovery code";
Messages.settings_totp_recovery = "If you lose access to your authenticator app, you may lock yourselves out of your CryptPad account. <strong>To prevent this, please store the following recovery secret key.</strong> You'll be able to use it to disable the multi-factor authentication. Do not share this key.";
Messages.settings_otp_tuto = "Please scan this QR code with your authenticator app and paste the verification code to confirm.";
Messages.settings_removeOwnedButton = "Destroy documents";
Messages.settings_removeOwnedText = "Please wait while your document are being destroyed...";
@ -75,7 +83,7 @@ define([
'cp-settings-mediatag-size',
],
'access': [ // Msg.settings_cat_access // XXX
'cp-settings-totp',
'cp-settings-mfa',
'cp-settings-remove-owned',
'cp-settings-change-password',
'cp-settings-delete'
@ -930,46 +938,76 @@ define([
// Account access
var drawTotp = function (content, enabled) {
var drawMfa = function (content, enabled) {
var $content = $(content).empty();
$content.append(h('div.cp-settings-mfa-hint.cp-settings-mfa-status', [
h('i.fa' + (enabled ? '.fa-check' : '.fa-times')),
h('span', enabled ? Messages.mfa_status_on : Messages.mfa_status_off)
]));
if (enabled) {
(function () {
var disable = h('button.btn.btn-danger', Messages.settings_totp_disable);
var OTPEntry, pwInput;
$content.append(h('div', [
h('p', pwInput = h('input', {
var button = h('button.btn.btn-danger', Messages.mfa_revoke_button);
var $mfaRevokeBtn = $(button);
var pwInput;
var pwContainer = h('div.cp-password-container', [
h('label.cp-settings-mfa-hint', { for: 'cp-mfa-password' }, Messages.mfa_revoke_label),
pwInput = h('input#cp-mfa-password', {
type: 'password',
placeholder: Messages.login_password,
})),
OTPEntry = h('input', {
placeholder: Messages.settings_otp_code
}),
disable
]));
var $b = $(disable);
var $OTPEntry = $(OTPEntry);
UI.confirmButton(disable, {
classes: 'btn-danger',
multiple: true
}, function () {
$b.attr('disabled', 'disabled');
button
]);
$content.append(pwContainer);
var spinner = UI.makeSpinner($mfaRevokeBtn);
$mfaRevokeBtn.click(function () {
var name = privateData.accountName;
var password = $(pwInput).val();
if (!password) { return void UI.warn(Messages.login_noSuchUser); }
// scrypt locks up the UI before the DOM has a chance
// to update (displaying logs, etc.), so do a set timeout
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, password, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
}, function (err, obj) {
if (!obj || !obj.correct) {
UI.warn(Messages.login_noSuchUser);
$b.removeAttr('disabled');
return;
}
var blockKeys = result.blockKeys;
spinner.spin();
$(pwInput).prop('disabled', 'disabled');
$mfaRevokeBtn.prop('disabled', 'disabled');
var blockKeys;
nThen(function (waitFor) {
var next = waitFor();
// scrypt locks up the UI before the DOM has a chance
// to update (displaying logs, etc.), so do a set timeout
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, password, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
}, function (err, obj) {
if (!obj || !obj.correct) {
spinner.hide();
UI.warn(Messages.login_noSuchUser);
$mfaRevokeBtn.removeAttr('disabled');
$(pwInput).removeAttr('disabled');
waitFor.abort();
return;
}
spinner.done();
blockKeys = result.blockKeys;
next();
});
});
}, 100);
}).nThen(function () {
$(pwContainer).remove();
var OTPEntry;
var disable = h('button.btn.btn-danger', Messages.mfa_disable);
$content.append(h('div.cp-password-container', [
h('label.cp-settings-mfa-hint', { for: 'cp-mfa-password' }, Messages.mfa_revoke_code),
OTPEntry = h('input', {
placeholder: Messages.settings_otp_code
}),
disable
]));
var $OTPEntry = $(OTPEntry);
var $d = $(disable).click(function () {
$d.prop('disabled', 'disabled');
var code = $OTPEntry.val();
sframeChan.query("Q_SETTINGS_TOTP_REVOKE", {
key: blockKeys.sign,
@ -980,25 +1018,44 @@ define([
}, function (err, obj) {
$OTPEntry.val("");
if (err || !obj || !obj.success) {
$b.removeAttr('disabled');
$d.removeAttr('disabled');
return void UI.warn(Messages.settings_otp_invalid);
}
drawTotp(content, false);
drawMfa(content, false);
}, {raw: true});
});
});
}, 1000);
});
})();
return;
}
var button = h('button.btn.btn-primary', Messages.settings_totp_enable);
var button = h('button.btn.btn-primary', Messages.mfa_setup_button);
var $mfaSetupBtn = $(button);
var pwInput;
$content.append(h('div.cp-password-container', [
h('label.cp-settings-mfa-hint', { for: 'cp-mfa-password' }, Messages.mfa_setup_label),
pwInput = h('input#cp-mfa-password', {
type: 'password',
placeholder: Messages.login_password,
}),
button
]));
var spinner = UI.makeSpinner($mfaSetupBtn);
$(button).click(function () {
$content.empty();
var name = privateData.accountName;
var password = $(pwInput).val();
if (!password) { return void UI.warn(Messages.login_noSuchUser); }
spinner.spin();
$(pwInput).prop('disabled', 'disabled');
$mfaSetupBtn.prop('disabled', 'disabled');
var Base32, QRCode, Nacl;
var blockKeys;
var recoverySecret;
nThen(function (waitFor) {
require([
'/auth/base32.js',
@ -1010,52 +1067,58 @@ define([
Nacl = window.nacl;
}));
}).nThen(function (waitFor) {
var pwInput;
var button = h('button.btn.btn-secondary', Messages.ui_confirm);
$content.append(h('div', [
h('p', pwInput = h('input', {
type: 'password',
placeholder: Messages.login_password,
})),
button
]));
var next = waitFor();
var BUSY = false;
var spinner = UI.makeSpinner($content);
var $b = $(button).click(function () {
if (BUSY) { return; }
var name = privateData.accountName;
var password = $(pwInput).val();
if (!password) { return void UI.warn(Messages.login_noSuchUser); }
spinner.spin();
$b.attr('disabled', 'disabled');
BUSY = true;
// scrypt locks up the UI before the DOM has a chance
// to update (displaying logs, etc.), so do a set timeout
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, password, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
}, function (err, obj) {
BUSY = false;
if (!obj || !obj.correct) {
spinner.hide();
UI.warn(Messages.login_noSuchUser);
$b.removeAttr('disabled');
return;
}
spinner.done();
blockKeys = result.blockKeys;
next();
});
// scrypt locks up the UI before the DOM has a chance
// to update (displaying logs, etc.), so do a set timeout
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, password, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
}, function (err, obj) {
if (!obj || !obj.correct) {
spinner.hide();
UI.warn(Messages.login_noSuchUser);
$mfaSetupBtn.removeAttr('disabled');
$(pwInput).removeAttr('disabled');
waitFor.abort();
return;
}
spinner.done();
blockKeys = result.blockKeys;
next();
});
}, 1000);
});
}, 100);
}).nThen(function (waitFor) {
$content.empty();
var next = waitFor();
recoverySecret = Nacl.util.encodeBase64(Nacl.randomBytes(24));
var button = h('button.btn.btn-primary', [
h('i.fa.fa-check'),
h('span', Messages.done)
]);
$content.append(h('div.alert.alert-danger', [
h('h2', Messages.mfa_recovery_title),
h('p', Messages.mfa_recovery_hint),
h('p', Messages.mfa_recovery_warning),
h('div.cp-password-container', [
UI.dialog.selectable(recoverySecret),
button
])
]));
var nextButton = h('button.btn.btn-primary', {
'disabled': 'disabled'
}, Messages.continue);
$(nextButton).click(function () {
next();
}).appendTo($content);
$(button).click(function () {
$content.find('.alert-danger').removeClass('alert-danger').addClass('alert-success');
$(button).prop('disabled', 'disabled');
$(nextButton).removeAttr('disabled');
});
}).nThen(function () {
var randomSecret = function () {
@ -1063,18 +1126,11 @@ define([
return Base32.encode(U8);
};
$content.empty();
var generate = h('button.btn.btn-primary', Messages.settings_totp_generate);
var secretContainer = h('div');
var $container = $(secretContainer);
$content.append(secretContainer, h('p', generate));
var updateQR = Util.throttle(function (uri, target) {
var updateQR = Util.mkAsync(function (uri, target) {
new QRCode(target, uri);
}, 400);
});
var updateURI = function (secret) {
$container.empty();
var recoverySecret = Nacl.util.encodeBase64(Nacl.randomBytes(24));
var username = privateData.accountName;
var hostname = new URL(privateData.origin).hostname;
var label = "CryptPad";
@ -1090,8 +1146,11 @@ define([
});
var $OTPEntry = $(OTPEntry);
var description = h('p', Messages.settings_totp_tuto);
var confirmOTP = h('button.btn.btn-primary', Messages.settings_totp_confirm);
var description = h('p.cp-settings-mfa-hint', Messages.settings_otp_tuto);
var confirmOTP = h('button.btn.btn-primary', [
h('i.fa.fa-check'),
h('span', Messages.mfa_enable)
]);
var $confirmBtn = $(confirmOTP);
var lock = false;
UI.confirmButton(confirmOTP, {
@ -1099,7 +1158,7 @@ define([
}, function () {
var code = $OTPEntry.val();
if (code.length !== 6 || /\D/.test(code)) {
return void UI.warn(Messages.error); // XXX
return void UI.warn(Messages.settings_otp_invalid);
}
$confirmBtn.attr('disabled', 'disabled');
lock = true;
@ -1107,7 +1166,7 @@ define([
var data = {
command: 'TOTP_SETUP',
secret: secret,
contact: "secret:" + recoverySecret, // XXX add other recovery options
contact: "secret:" + recoverySecret, // TODO other recovery options
code: code,
};
@ -1122,46 +1181,39 @@ define([
console.error(err);
return void UI.warn(Messages.error);
}
drawTotp(content, true);
drawMfa(content, true);
}, {raw: true});
});
var recoveryBlock = h('div.alert.alert-danger', [
h('h3', Messages.settings_totp_recovery_header),
UI.setHTML(h('p'), Messages.settings_totp_recovery),
UI.dialog.selectable(recoverySecret)
]);
$container.append([
uriInput,
qr,
h('br'),
$content.append([
description,
recoveryBlock,
OTPEntry,
confirmOTP
h('div.cp-settings-qr-container', [
qr,
h('div.cp-settings-qr-code', [
OTPEntry,
h('br'),
confirmOTP
])
]),
uriInput
]);
};
var $g = $(generate).click(function () {
$g.remove();
var secret = randomSecret();
updateURI(secret);
});
var secret = randomSecret();
updateURI(secret);
});
}).appendTo(content);
});
};
makeBlock('totp', function (cb) { // Msg.settings_totpTitle
makeBlock('mfa', function (cb) { // Msg.settings_mfaTitle, Msg.settings_mfaHint
if (!common.isLoggedIn()) { return void cb(false); }
var content = h('div');
sframeChan.query('Q_SETTINGS_MFA_CHECK', {}, function (err, obj) {
if (err || !obj || (obj && obj.err === 'NOBLOCK')) { return void cb(false); }
var enabled = obj && obj.mfa && obj.type === 'TOTP';
drawTotp(content, Boolean(enabled));
drawMfa(content, Boolean(enabled));
cb(content);
});
}, true);