TOTP: UI fixes and improvements
This commit is contained in:
parent
9aac9d1c2f
commit
318202531f
6 changed files with 308 additions and 226 deletions
|
@ -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'),
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue