mirror of https://github.com/xwiki-labs/cryptpad
Merge branch 'forcemfa' into staging
This commit is contained in:
commit
7ba29238b2
|
@ -131,6 +131,14 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
//otpSessionExpiration: 7*24, // hours
|
//otpSessionExpiration: 7*24, // hours
|
||||||
|
|
||||||
|
/* Registered users can be forced to protect their account
|
||||||
|
* with a Multi-factor Authentication (MFA) tool like a TOTP
|
||||||
|
* authenticator application.
|
||||||
|
*
|
||||||
|
* defaults to false
|
||||||
|
*/
|
||||||
|
//enforceMFA: false,
|
||||||
|
|
||||||
/* =====================
|
/* =====================
|
||||||
* Admin
|
* Admin
|
||||||
* ===================== */
|
* ===================== */
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
.cp-loading-container {
|
.cp-loading-container {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
height: 236px;
|
min-height: 236px;
|
||||||
max-height: calc(100vh - 20px);
|
max-height: calc(100vh - 20px);
|
||||||
margin: 50px;
|
margin: 50px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -106,6 +106,7 @@
|
||||||
color: @cp_loading-fg;
|
color: @cp_loading-fg;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
display: none;
|
display: none;
|
||||||
|
overflow-y: auto;
|
||||||
a {
|
a {
|
||||||
color: @cp_loading-link;
|
color: @cp_loading-link;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cp-loading-missing-mfa {
|
||||||
|
.cp-settings-qr-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
.cp-settings-qr-code {
|
||||||
|
input {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cp-settings-qr {
|
||||||
|
img {
|
||||||
|
border: 10px solid white;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
margin: 10px 10px 10px 0;
|
||||||
|
}
|
||||||
|
.cp-password-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap:0.5rem;
|
||||||
|
|
||||||
|
justify-content:flex-start;
|
||||||
|
input {
|
||||||
|
flex-shrink: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
width: 100%;
|
||||||
|
font-weight: unset;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Properties modal
|
// Properties modal
|
||||||
.cp-app-prop {
|
.cp-app-prop {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
|
@ -495,6 +495,7 @@ var instanceStatus = function (Env, Server, cb) {
|
||||||
instanceJurisdiction: Env.instanceJurisdiction,
|
instanceJurisdiction: Env.instanceJurisdiction,
|
||||||
instanceName: Env.instanceName,
|
instanceName: Env.instanceName,
|
||||||
instanceNotice: Env.instanceNotice,
|
instanceNotice: Env.instanceNotice,
|
||||||
|
enforceMFA: Env.enforceMFA,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,9 @@ var makeBooleanSetter = function (attr) {
|
||||||
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log)
|
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_EMBEDDING', [true]]], console.log)
|
||||||
commands.ENABLE_EMBEDDING = makeBooleanSetter('enableEmbedding');
|
commands.ENABLE_EMBEDDING = makeBooleanSetter('enableEmbedding');
|
||||||
|
|
||||||
|
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['ENFORCE_MFA', [true]]], console.log)
|
||||||
|
commands.ENFORCE_MFA = makeBooleanSetter('enforceMFA');
|
||||||
|
|
||||||
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
|
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
|
||||||
commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration');
|
commands.RESTRICT_REGISTRATION = makeBooleanSetter('restrictRegistration');
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,7 @@ module.exports.create = function (config) {
|
||||||
commandTimers: {},
|
commandTimers: {},
|
||||||
|
|
||||||
sso: config.sso,
|
sso: config.sso,
|
||||||
|
enforceMFA: config.enforceMFA,
|
||||||
|
|
||||||
// initialized as undefined
|
// initialized as undefined
|
||||||
bearerSecret: void 0,
|
bearerSecret: void 0,
|
||||||
|
|
|
@ -584,7 +584,8 @@ var serveConfig = makeRouteCache(function () {
|
||||||
shouldUpdateNode: Env.shouldUpdateNode || undefined,
|
shouldUpdateNode: Env.shouldUpdateNode || undefined,
|
||||||
listMyInstance: Env.listMyInstance,
|
listMyInstance: Env.listMyInstance,
|
||||||
accounts_api: Env.accounts_api,
|
accounts_api: Env.accounts_api,
|
||||||
sso: ssoCfg
|
sso: ssoCfg,
|
||||||
|
enforceMFA: Env.enforceMFA
|
||||||
}, null, '\t'),
|
}, null, '\t'),
|
||||||
'});'
|
'});'
|
||||||
].join(';\n');
|
].join(';\n');
|
||||||
|
|
|
@ -63,6 +63,7 @@ define([
|
||||||
'cp-admin-update-limit',
|
'cp-admin-update-limit',
|
||||||
'cp-admin-registration',
|
'cp-admin-registration',
|
||||||
'cp-admin-enableembeds',
|
'cp-admin-enableembeds',
|
||||||
|
'cp-admin-forcemfa',
|
||||||
'cp-admin-email',
|
'cp-admin-email',
|
||||||
|
|
||||||
'cp-admin-instance-info-notice',
|
'cp-admin-instance-info-notice',
|
||||||
|
@ -1527,6 +1528,31 @@ Example
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Msg.admin_forcemfaHint, .admin_forcemfaTitle
|
||||||
|
Messages.admin_forcemfaTitle = "Enforce MFA on this instance"; // XXX
|
||||||
|
Messages.admin_forcemfaHint = "All CryptPad users will be asked to set up a multi-factor authenticator (TOTP) to log in to their account."; // XXX
|
||||||
|
create['forcemfa'] = makeAdminCheckbox({
|
||||||
|
key: 'forcemfa',
|
||||||
|
getState: function () {
|
||||||
|
return APP.instanceStatus.enforceMFA;
|
||||||
|
},
|
||||||
|
query: function (val, setState) {
|
||||||
|
sFrameChan.query('Q_ADMIN_RPC', {
|
||||||
|
cmd: 'ADMIN_DECREE',
|
||||||
|
data: ['ENFORCE_MFA', [val]]
|
||||||
|
}, function (e, response) {
|
||||||
|
if (e || response.error) {
|
||||||
|
UI.warn(Messages.error);
|
||||||
|
console.error(e, response);
|
||||||
|
}
|
||||||
|
APP.updateStatus(function () {
|
||||||
|
setState(APP.instanceStatus.enforceMFA);
|
||||||
|
flushCacheNotice();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
create['email'] = function () {
|
create['email'] = function () {
|
||||||
var key = 'email';
|
var key = 'email';
|
||||||
var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle
|
var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle
|
||||||
|
|
|
@ -4176,5 +4176,21 @@ define([
|
||||||
modal = UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
|
modal = UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Messages.loading_mfa_required = "Multi-factor Authentication is required on this instance. Please update your account using an anthenticator app and the form below."; // XXX
|
||||||
|
UIElements.onMissingMFA = (common, config, cb) => {
|
||||||
|
let content = h('div');
|
||||||
|
let msg = h('div.cp-loading-missing-mfa', [
|
||||||
|
h('div.alert.alert-warning', Messages.loading_mfa_required),
|
||||||
|
content
|
||||||
|
]);
|
||||||
|
common.totpSetup(config, content, false, (newState) => {
|
||||||
|
if (!newState) {
|
||||||
|
return void UI.errorLoadingScreen(Messages.error);
|
||||||
|
}
|
||||||
|
cb({state: true});
|
||||||
|
});
|
||||||
|
return UI.errorLoadingScreen(msg, false, false);
|
||||||
|
};
|
||||||
|
|
||||||
return UIElements;
|
return UIElements;
|
||||||
});
|
});
|
||||||
|
|
|
@ -2171,6 +2171,7 @@ define([
|
||||||
// Loading events
|
// Loading events
|
||||||
common.loading = {};
|
common.loading = {};
|
||||||
common.loading.onDriveEvent = Util.mkEvent();
|
common.loading.onDriveEvent = Util.mkEvent();
|
||||||
|
common.loading.onMissingMFAEvent = Util.mkEvent();
|
||||||
|
|
||||||
// (Auto)store pads
|
// (Auto)store pads
|
||||||
common.autoStore = {};
|
common.autoStore = {};
|
||||||
|
@ -2495,6 +2496,22 @@ define([
|
||||||
if (AppConfig.beforeLogin) {
|
if (AppConfig.beforeLogin) {
|
||||||
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());
|
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());
|
||||||
}
|
}
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
var blockHash = LocalStore.getBlockHash();
|
||||||
|
if (!blockHash || !Config.enforceMFA) { return; }
|
||||||
|
|
||||||
|
// If this instance is configured to enforce MFA for all registered users,
|
||||||
|
// request the login block with no credential to check if it is protected.
|
||||||
|
var parsed = Block.parseBlockHash(blockHash);
|
||||||
|
Util.getBlock(parsed.href, { }, waitFor((err, response) => {
|
||||||
|
// If this account is already protected, nothing to do
|
||||||
|
if (err === 401 && response.method) { return; }
|
||||||
|
|
||||||
|
// Missing MFA protection, show set up screen
|
||||||
|
common.loading.onMissingMFAEvent.fire({
|
||||||
|
cb: waitFor()
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
}).nThen(function (waitFor) {
|
}).nThen(function (waitFor) {
|
||||||
// if a block URL is present then the user is probably logged in with a modern account
|
// if a block URL is present then the user is probably logged in with a modern account
|
||||||
|
|
|
@ -0,0 +1,321 @@
|
||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'/customize/messages.js',
|
||||||
|
'/common/hyperscript.js',
|
||||||
|
'/common/common-interface.js',
|
||||||
|
'/components/nthen/index.js',
|
||||||
|
'/customize.dist/login.js',
|
||||||
|
'/common/common-util.js',
|
||||||
|
|
||||||
|
], function ($, Messages, h, UI, nThen, Login, Util) {
|
||||||
|
const MFA = {};
|
||||||
|
|
||||||
|
MFA.totpSetup = function (common, config, content, enabled, cb) {
|
||||||
|
|
||||||
|
var sframeChan = common.getSframeChannel();
|
||||||
|
// NOTE privateData may not be defined yet
|
||||||
|
var accountName = config.accountName;
|
||||||
|
var origin = config.origin;
|
||||||
|
|
||||||
|
var $content = $(content).empty();
|
||||||
|
$content.append(h('div.cp-settings-mfa-hint.cp-settings-mfa-status' + (enabled ? '.mfa-enabled' : '.mfa-disabled'), [
|
||||||
|
h('i.fa' + (enabled ? '.fa-check' : '.fa-times')),
|
||||||
|
h('span', enabled ? Messages.mfa_status_on : Messages.mfa_status_off)
|
||||||
|
]));
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
(function () {
|
||||||
|
var button = h('button.btn', Messages.mfa_disable);
|
||||||
|
button.classList.add('disable-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,
|
||||||
|
}),
|
||||||
|
button
|
||||||
|
]);
|
||||||
|
$content.append(pwContainer);
|
||||||
|
|
||||||
|
// submit password on enter keyup
|
||||||
|
$(pwInput).on('keyup', e => {
|
||||||
|
if (e.which === 13) { $mfaRevokeBtn.click(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
var spinner = UI.makeSpinner($mfaRevokeBtn);
|
||||||
|
$mfaRevokeBtn.click(function () {
|
||||||
|
var name = accountName;
|
||||||
|
var password = $(pwInput).val();
|
||||||
|
if (!password) { return void UI.warn(Messages.login_noSuchUser); }
|
||||||
|
|
||||||
|
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.disable-button', Messages.mfa_revoke_button);
|
||||||
|
$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,
|
||||||
|
data: {
|
||||||
|
command: 'TOTP_REVOKE',
|
||||||
|
code: code,
|
||||||
|
}
|
||||||
|
}, function (err, obj) {
|
||||||
|
$OTPEntry.val("");
|
||||||
|
if (err || !obj || !obj.success) {
|
||||||
|
$d.removeAttr('disabled');
|
||||||
|
return void UI.warn(Messages.settings_otp_invalid);
|
||||||
|
}
|
||||||
|
cb(false);
|
||||||
|
}, {raw: true});
|
||||||
|
|
||||||
|
});
|
||||||
|
OTPEntry.focus();
|
||||||
|
// submit OTP on enter keyup
|
||||||
|
$OTPEntry.on('keyup', e => {
|
||||||
|
if (e.which === 13) { $d.click(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// submit password on enter keyup
|
||||||
|
$(pwInput).on('keyup', e => {
|
||||||
|
if (e.which === 13) { $(button).click(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
$(button).click(function () {
|
||||||
|
var name = 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;
|
||||||
|
var ssoSeed;
|
||||||
|
nThen(function (waitFor) {
|
||||||
|
require([
|
||||||
|
'/auth/base32.js',
|
||||||
|
'/lib/qrcode.min.js',
|
||||||
|
'/components/tweetnacl/nacl-fast.min.js',
|
||||||
|
], waitFor(function (_Base32) {
|
||||||
|
Base32 = _Base32;
|
||||||
|
QRCode = window.QRCode;
|
||||||
|
Nacl = window.nacl;
|
||||||
|
}));
|
||||||
|
}).nThen(function (waitFor) {
|
||||||
|
sframeChan.query("Q_SETTINGS_GET_SSO_SEED", {
|
||||||
|
}, waitFor(function (err, obj) {
|
||||||
|
if (!obj || !obj.seed) { return; } // Not an sso account?
|
||||||
|
ssoSeed = obj.seed;
|
||||||
|
}));
|
||||||
|
}).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 () {
|
||||||
|
var salt = ssoSeed || name;
|
||||||
|
Login.Cred.deriveFromPassphrase(salt, password, Login.requiredBytes, function (bytes) {
|
||||||
|
console.error(bytes);
|
||||||
|
var result = Login.allocateBytes(bytes);
|
||||||
|
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
|
||||||
|
blockHash: result.blockHash,
|
||||||
|
}, function (err, obj) {
|
||||||
|
console.error(obj);
|
||||||
|
if (!obj || !obj.correct) {
|
||||||
|
spinner.hide();
|
||||||
|
UI.warn(Messages.login_noSuchUser);
|
||||||
|
$mfaSetupBtn.removeAttr('disabled');
|
||||||
|
$(pwInput).removeAttr('disabled');
|
||||||
|
waitFor.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn(obj);
|
||||||
|
spinner.done();
|
||||||
|
blockKeys = result.blockKeys;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 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 () {
|
||||||
|
var U8 = Nacl.randomBytes(20);
|
||||||
|
return Base32.encode(U8);
|
||||||
|
};
|
||||||
|
$content.empty();
|
||||||
|
|
||||||
|
var updateQR = Util.mkAsync(function (uri, target) {
|
||||||
|
new QRCode(target, uri);
|
||||||
|
});
|
||||||
|
var updateURI = function (secret) {
|
||||||
|
var username = accountName;
|
||||||
|
var hostname = new URL(origin).hostname;
|
||||||
|
var label = "CryptPad";
|
||||||
|
|
||||||
|
var uri = `otpauth://totp/${label}:${username}@${hostname}?secret=${secret}`;
|
||||||
|
|
||||||
|
var qr = h('div.cp-settings-qr');
|
||||||
|
var uriInput = UI.dialog.selectable(uri);
|
||||||
|
|
||||||
|
updateQR(uri, qr);
|
||||||
|
|
||||||
|
var OTPEntry = h('input', {
|
||||||
|
placeholder: Messages.settings_otp_code
|
||||||
|
});
|
||||||
|
var $OTPEntry = $(OTPEntry);
|
||||||
|
|
||||||
|
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 lock = false;
|
||||||
|
|
||||||
|
confirmOTP.addEventListener('click', function () {
|
||||||
|
var code = $OTPEntry.val();
|
||||||
|
if (code.length !== 6 || /\D/.test(code)) {
|
||||||
|
return void UI.warn(Messages.settings_otp_invalid);
|
||||||
|
}
|
||||||
|
confirmOTP.disabled = true;
|
||||||
|
lock = true;
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
secret: secret,
|
||||||
|
contact: "secret:" + recoverySecret, // TODO other recovery options
|
||||||
|
code: code,
|
||||||
|
};
|
||||||
|
|
||||||
|
sframeChan.query("Q_SETTINGS_TOTP_SETUP", {
|
||||||
|
key: blockKeys.sign,
|
||||||
|
data: data
|
||||||
|
}, function (err, obj) {
|
||||||
|
lock = false;
|
||||||
|
$OTPEntry.val("");
|
||||||
|
if (err || !obj || !obj.success) {
|
||||||
|
confirmOTP.disabled = false;
|
||||||
|
console.error(err);
|
||||||
|
return void UI.warn(Messages.error);
|
||||||
|
}
|
||||||
|
cb(true);
|
||||||
|
}, { raw: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
$content.append([
|
||||||
|
description,
|
||||||
|
uriInput,
|
||||||
|
h('div.cp-settings-qr-container', [
|
||||||
|
qr,
|
||||||
|
h('div.cp-settings-qr-code', [
|
||||||
|
OTPEntry,
|
||||||
|
h('br'),
|
||||||
|
confirmOTP
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
OTPEntry.focus();
|
||||||
|
// submit OTP on enter keyup
|
||||||
|
$OTPEntry.on('keyup', e => {
|
||||||
|
if (e.which === 13) { $(confirmOTP).click(); }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var secret = randomSecret();
|
||||||
|
updateURI(secret);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return MFA;
|
||||||
|
});
|
|
@ -231,6 +231,61 @@ define([
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var addFirstHandlers = () => {
|
||||||
|
sframeChan.on('Q_SETTINGS_CHECK_PASSWORD', function (data, cb) {
|
||||||
|
var blockHash = Utils.LocalStore.getBlockHash();
|
||||||
|
var userHash = Utils.LocalStore.getUserHash();
|
||||||
|
var correct = (blockHash && blockHash === data.blockHash) ||
|
||||||
|
(!blockHash && userHash === data.userHash);
|
||||||
|
cb({correct: correct});
|
||||||
|
});
|
||||||
|
sframeChan.on('Q_SETTINGS_TOTP_SETUP', function (obj, cb) {
|
||||||
|
require([
|
||||||
|
'/common/outer/http-command.js',
|
||||||
|
], function (ServerCommand) {
|
||||||
|
var data = obj.data;
|
||||||
|
data.command = 'TOTP_SETUP';
|
||||||
|
data.session = Utils.LocalStore.getSessionToken();
|
||||||
|
ServerCommand(obj.key, data, function (err, response) {
|
||||||
|
cb({ success: Boolean(!err && response && response.bearer) });
|
||||||
|
if (response && response.bearer) {
|
||||||
|
Utils.LocalStore.setSessionToken(response.bearer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
sframeChan.on('Q_SETTINGS_TOTP_REVOKE', function (obj, cb) {
|
||||||
|
require([
|
||||||
|
'/common/outer/http-command.js',
|
||||||
|
], function (ServerCommand) {
|
||||||
|
ServerCommand(obj.key, obj.data, function (err, response) {
|
||||||
|
cb({ success: Boolean(!err && response && response.success) });
|
||||||
|
if (response && response.success) {
|
||||||
|
Utils.LocalStore.setSessionToken('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
sframeChan.on('Q_SETTINGS_GET_SSO_SEED', function (obj, _cb) {
|
||||||
|
var cb = Utils.Util.mkAsync(_cb);
|
||||||
|
cb({
|
||||||
|
seed: Utils.LocalStore.getSSOSeed()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Cryptpad.loading.onMissingMFAEvent.reg((data) => {
|
||||||
|
var cb = data.cb;
|
||||||
|
if (!sframeChan) { return void cb('EINVAL'); }
|
||||||
|
sframeChan.query('Q_LOADING_MISSING_AUTH', {
|
||||||
|
accountName: Utils.LocalStore.getAccountName(),
|
||||||
|
origin: window.location.origin,
|
||||||
|
}, (err, obj) => {
|
||||||
|
if (obj && obj.state) { return void cb(true); }
|
||||||
|
console.error(err || obj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var whenReady = waitFor(function (msg) {
|
var whenReady = waitFor(function (msg) {
|
||||||
if (msg.source !== iframe) { return; }
|
if (msg.source !== iframe) { return; }
|
||||||
var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data;
|
var data = typeof(msg.data) === "string" ? JSON.parse(msg.data) : msg.data;
|
||||||
|
@ -247,6 +302,7 @@ define([
|
||||||
});
|
});
|
||||||
SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
|
SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
|
||||||
Utils.sframeChan = sframeChan = sfc;
|
Utils.sframeChan = sframeChan = sfc;
|
||||||
|
addFirstHandlers();
|
||||||
window.CryptPad_loadingError = function (e) {
|
window.CryptPad_loadingError = function (e) {
|
||||||
sfc.event('EV_LOADING_ERROR', e);
|
sfc.event('EV_LOADING_ERROR', e);
|
||||||
};
|
};
|
||||||
|
@ -272,6 +328,7 @@ define([
|
||||||
}
|
}
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
|
|
||||||
|
|
||||||
// NOTE: Driveless mode should only work for existing pads, but we can't check that
|
// NOTE: Driveless mode should only work for existing pads, but we can't check that
|
||||||
// before creating the worker because we need the anon RPC to do so.
|
// before creating the worker because we need the anon RPC to do so.
|
||||||
// We're only going to check if a hash exists in the URL or not.
|
// We're only going to check if a hash exists in the URL or not.
|
||||||
|
|
|
@ -19,6 +19,7 @@ define([
|
||||||
'/common/sframe-common-mailbox.js',
|
'/common/sframe-common-mailbox.js',
|
||||||
'/common/inner/cache.js',
|
'/common/inner/cache.js',
|
||||||
'/common/inner/common-mediatag.js',
|
'/common/inner/common-mediatag.js',
|
||||||
|
'/common/inner/mfa.js',
|
||||||
'/common/metadata-manager.js',
|
'/common/metadata-manager.js',
|
||||||
|
|
||||||
'/customize/application_config.js',
|
'/customize/application_config.js',
|
||||||
|
@ -50,6 +51,7 @@ define([
|
||||||
Mailbox,
|
Mailbox,
|
||||||
Cache,
|
Cache,
|
||||||
MT,
|
MT,
|
||||||
|
MFA,
|
||||||
MetadataMgr,
|
MetadataMgr,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
Pages,
|
Pages,
|
||||||
|
@ -123,6 +125,7 @@ define([
|
||||||
funcs.importMediaTagMenu = callWithCommon(MT.importMediaTagMenu);
|
funcs.importMediaTagMenu = callWithCommon(MT.importMediaTagMenu);
|
||||||
funcs.getMediaTagPreview = callWithCommon(MT.getMediaTagPreview);
|
funcs.getMediaTagPreview = callWithCommon(MT.getMediaTagPreview);
|
||||||
funcs.getMediaTag = callWithCommon(MT.getMediaTag);
|
funcs.getMediaTag = callWithCommon(MT.getMediaTag);
|
||||||
|
funcs.totpSetup = callWithCommon(MFA.totpSetup);
|
||||||
|
|
||||||
// Thumb
|
// Thumb
|
||||||
funcs.displayThumbnail = callWithCommon(Thumb.displayThumbnail);
|
funcs.displayThumbnail = callWithCommon(Thumb.displayThumbnail);
|
||||||
|
@ -893,6 +896,10 @@ define([
|
||||||
UI.updateLoadingProgress(data);
|
UI.updateLoadingProgress(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.sframeChan.on('Q_LOADING_MISSING_AUTH', function (data, cb) {
|
||||||
|
UIElements.onMissingMFA(funcs, data, cb);
|
||||||
|
});
|
||||||
|
|
||||||
ctx.sframeChan.on('EV_NEW_VERSION', function () {
|
ctx.sframeChan.on('EV_NEW_VERSION', function () {
|
||||||
// TODO lock the UI and do the same in non-framework apps
|
// TODO lock the UI and do the same in non-framework apps
|
||||||
var $err = $('<div>').append(Messages.newVersionError);
|
var $err = $('<div>').append(Messages.newVersionError);
|
||||||
|
|
|
@ -1244,7 +1244,16 @@ define([
|
||||||
sframeChan.query('Q_SETTINGS_MFA_CHECK', {}, function (err, obj) {
|
sframeChan.query('Q_SETTINGS_MFA_CHECK', {}, function (err, obj) {
|
||||||
if (err || !obj || (obj && obj.err === 'NOBLOCK')) { return void cb(false); }
|
if (err || !obj || (obj && obj.err === 'NOBLOCK')) { return void cb(false); }
|
||||||
var enabled = obj && obj.mfa && obj.type === 'TOTP';
|
var enabled = obj && obj.mfa && obj.type === 'TOTP';
|
||||||
drawMfa(content, Boolean(enabled));
|
var config = {
|
||||||
|
accountName: privateData.accountName,
|
||||||
|
origin: privateData.origin
|
||||||
|
};
|
||||||
|
var draw = (state) => {
|
||||||
|
common.totpSetup(config, content, state, (newState) => {
|
||||||
|
draw(newState);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
draw(Boolean(enabled));
|
||||||
cb(content);
|
cb(content);
|
||||||
});
|
});
|
||||||
}, true);
|
}, true);
|
||||||
|
|
|
@ -74,40 +74,6 @@ define([
|
||||||
sframeChan.on('Q_SETTINGS_IMPORT_LOCAL', function (data, cb) {
|
sframeChan.on('Q_SETTINGS_IMPORT_LOCAL', function (data, cb) {
|
||||||
Cryptpad.mergeAnonDrive(cb);
|
Cryptpad.mergeAnonDrive(cb);
|
||||||
});
|
});
|
||||||
sframeChan.on('Q_SETTINGS_CHECK_PASSWORD', function (data, cb) {
|
|
||||||
var blockHash = Utils.LocalStore.getBlockHash();
|
|
||||||
var userHash = Utils.LocalStore.getUserHash();
|
|
||||||
var correct = (blockHash && blockHash === data.blockHash) ||
|
|
||||||
(!blockHash && userHash === data.userHash);
|
|
||||||
cb({correct: correct});
|
|
||||||
});
|
|
||||||
sframeChan.on('Q_SETTINGS_TOTP_SETUP', function (obj, cb) {
|
|
||||||
require([
|
|
||||||
'/common/outer/http-command.js',
|
|
||||||
], function (ServerCommand) {
|
|
||||||
var data = obj.data;
|
|
||||||
data.command = 'TOTP_SETUP';
|
|
||||||
data.session = Utils.LocalStore.getSessionToken();
|
|
||||||
ServerCommand(obj.key, data, function (err, response) {
|
|
||||||
cb({ success: Boolean(!err && response && response.bearer) });
|
|
||||||
if (response && response.bearer) {
|
|
||||||
Utils.LocalStore.setSessionToken(response.bearer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
sframeChan.on('Q_SETTINGS_TOTP_REVOKE', function (obj, cb) {
|
|
||||||
require([
|
|
||||||
'/common/outer/http-command.js',
|
|
||||||
], function (ServerCommand) {
|
|
||||||
ServerCommand(obj.key, obj.data, function (err, response) {
|
|
||||||
cb({ success: Boolean(!err && response && response.success) });
|
|
||||||
if (response && response.success) {
|
|
||||||
Utils.LocalStore.setSessionToken('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
sframeChan.on('Q_SETTINGS_MFA_CHECK', function (obj, cb) {
|
sframeChan.on('Q_SETTINGS_MFA_CHECK', function (obj, cb) {
|
||||||
require([
|
require([
|
||||||
'/common/outer/login-block.js',
|
'/common/outer/login-block.js',
|
||||||
|
@ -124,12 +90,6 @@ define([
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
sframeChan.on('Q_SETTINGS_GET_SSO_SEED', function (obj, _cb) {
|
|
||||||
var cb = Utils.Util.mkAsync(_cb);
|
|
||||||
cb({
|
|
||||||
seed: Utils.LocalStore.getSSOSeed()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
sframeChan.on('Q_SETTINGS_REMOVE_OWNED_PADS', function (data, cb) {
|
sframeChan.on('Q_SETTINGS_REMOVE_OWNED_PADS', function (data, cb) {
|
||||||
Cryptpad.removeOwnedPads(data, cb);
|
Cryptpad.removeOwnedPads(data, cb);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue