// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors // // SPDX-License-Identifier: AGPL-3.0-or-later define([ 'jquery', '/customize/messages.js', '/common/hyperscript.js', '/common/common-interface.js', '/components/nthen/index.js', '/customize.dist/login.js', '/common/common-util.js', '/common/common-icons.js', ], function ($, Messages, h, UI, nThen, Login, Util, Icons) { 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'), [ (enabled ? Icons.get('check') : Icons.get('close')), 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; var ssoSeed; 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) { 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', [Icons.get('lock'), 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 = Util.encodeBase64(Nacl.randomBytes(24)); var button = h('button.btn.btn-primary', [ Icons.get('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/${encodeURI(label)}:${encodeURI(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', [ Icons.get('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); } if (lock) { return; } 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; });