mirror of https://github.com/xwiki-labs/cryptpad
TOTP setup and revocation in settings
This commit is contained in:
parent
bf548c1022
commit
d789627920
|
@ -48,6 +48,11 @@ var decode32 = S => {
|
||||||
return decoded;
|
return decoded;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// XXX Decide expire time
|
||||||
|
// Allow user settings?
|
||||||
|
var EXPIRATION = 7 * 24 * 3600 * 1000; // Sessions are valid 7 days
|
||||||
|
|
||||||
var createJWT = function (Env, sessionId, publicKey, cb) {
|
var createJWT = function (Env, sessionId, publicKey, cb) {
|
||||||
JWT.sign({
|
JWT.sign({
|
||||||
// this is a custom JWT field (not a standard) - we include a reference to the session
|
// this is a custom JWT field (not a standard) - we include a reference to the session
|
||||||
|
@ -55,6 +60,7 @@ var createJWT = function (Env, sessionId, publicKey, cb) {
|
||||||
ref: sessionId,
|
ref: sessionId,
|
||||||
// we specify in the token for what resource the token should be valid (their block's public key)
|
// we specify in the token for what resource the token should be valid (their block's public key)
|
||||||
sub: Util.escapeKeyCharacters(publicKey),
|
sub: Util.escapeKeyCharacters(publicKey),
|
||||||
|
exp: (+new Date()) + EXPIRATION
|
||||||
}, Env.bearerSecret, {
|
}, Env.bearerSecret, {
|
||||||
// token integrity is ensured with HMAC SHA512 with the server's bearerSecret
|
// token integrity is ensured with HMAC SHA512 with the server's bearerSecret
|
||||||
// clients can inspect token parameters, but cannot modify them
|
// clients can inspect token parameters, but cannot modify them
|
||||||
|
@ -336,3 +342,112 @@ So, we should:
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This command is somewhat simpler than TOTP_SETUP
|
||||||
|
// Revoke a client TOTP secret which will allow them to disable TOTP for a login block IFF:
|
||||||
|
// 1. That login block exists
|
||||||
|
// 2. That login block is protected by TOTP 2FA
|
||||||
|
// 3. They can produce a valid OTP for that block's TOTP secret
|
||||||
|
// 4. They can sign for the block's public key
|
||||||
|
const revoke = Commands.TOTP_REVOKE = function (Env, body, cb) {
|
||||||
|
var { publicKey, code } = body;
|
||||||
|
|
||||||
|
// they must provide a valid OTP code
|
||||||
|
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
|
||||||
|
|
||||||
|
// they must provide a valid block public key
|
||||||
|
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
|
||||||
|
|
||||||
|
var secret;
|
||||||
|
nThen(function (w) {
|
||||||
|
// check that there is an MFA configuration for the given account
|
||||||
|
MFA.read(Env, publicKey, w(function (err, content) {
|
||||||
|
if (err) {
|
||||||
|
w.abort();
|
||||||
|
Env.Log.error('TOTP_VALIDATE_MFA_READ', {
|
||||||
|
error: err,
|
||||||
|
publicKey: publicKey,
|
||||||
|
});
|
||||||
|
return void cb('NO_MFA_CONFIGURED');
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed = Util.tryParse(content);
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
w.abort();
|
||||||
|
return void cb("INVALID_CONFIGURATION");
|
||||||
|
}
|
||||||
|
|
||||||
|
secret = parsed.secret;
|
||||||
|
}));
|
||||||
|
}).nThen(function () {
|
||||||
|
let decoded = decode32(secret);
|
||||||
|
if (!decoded) {
|
||||||
|
Env.Log.error("TOTP_VALIDATE_INVALID_SECRET", {
|
||||||
|
publicKey, // log the public key so the admin can investigate further
|
||||||
|
// don't log the problematic secret directly as
|
||||||
|
// logs are likely to be pasted in random places
|
||||||
|
});
|
||||||
|
return void cb("E_INVALID_SECRET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the code
|
||||||
|
var validated = OTP.totp.verify(code, decoded, {
|
||||||
|
window: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validated) {
|
||||||
|
// I won't worry about logging these OTPs as they shouldn't leak any useful information
|
||||||
|
Env.Log.error("TOTP_VALIDATE_BAD_OTP", {
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
return void cb("INVALID_OTP");
|
||||||
|
}
|
||||||
|
|
||||||
|
// call back to indicate that their request was well-formed and valid
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
revoke.complete = function (Env, body, cb) {
|
||||||
|
/*
|
||||||
|
if they are here then they:
|
||||||
|
|
||||||
|
1. have a valid block configured with TOTP-based 2FA
|
||||||
|
2. were able to provide a valid TOTP for that block's secret
|
||||||
|
3. were able to sign their messages for the block's public key
|
||||||
|
|
||||||
|
So, we should:
|
||||||
|
|
||||||
|
1. Revoke the TOTP authentication for their block
|
||||||
|
2. Remove all existing sessions
|
||||||
|
|
||||||
|
*/
|
||||||
|
var { publicKey } = body;
|
||||||
|
|
||||||
|
nThen(function (w) {
|
||||||
|
MFA.delete(Env, publicKey, w(function (err) {
|
||||||
|
if (!err) { return; }
|
||||||
|
w.abort();
|
||||||
|
Env.Log.error('TOTP_REVOKE_MFA_DELETE', {
|
||||||
|
error: err,
|
||||||
|
publicKey: publicKey,
|
||||||
|
});
|
||||||
|
cb('MFA_ERROR');
|
||||||
|
}));
|
||||||
|
}).nThen(function () {
|
||||||
|
Sessions.deleteUser(Env, publicKey, function (err) {
|
||||||
|
if (!err) { return; }
|
||||||
|
// If we can't delete the sessions, don't send an erorr, just log to the server.
|
||||||
|
// The MFA will still be correctly disabled as long as the first step is done.
|
||||||
|
Env.Log.error('TOTP_REVOKE_SESSIONS__DELETE', {
|
||||||
|
error: err,
|
||||||
|
publicKey: publicKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).nThen(function () {
|
||||||
|
cb(void 0, {
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,7 @@ var COMMANDS = {};
|
||||||
const TOTP = require("./challenge-commands/totp.js");
|
const TOTP = require("./challenge-commands/totp.js");
|
||||||
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
|
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
|
||||||
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
|
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
|
||||||
|
COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE;
|
||||||
|
|
||||||
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
|
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');
|
||||||
|
|
||||||
|
|
|
@ -380,6 +380,13 @@ app.use('/block/', function (req, res, next) {
|
||||||
// reject if it's too old
|
// reject if it's too old
|
||||||
if (payload.exp && ((+new Date()) > payload.exp)) {
|
if (payload.exp && ((+new Date()) > payload.exp)) {
|
||||||
Log.error("JWT_EXPIRED", payload);
|
Log.error("JWT_EXPIRED", payload);
|
||||||
|
Sessions.delete(Env, name, payload.ref, function (err) {
|
||||||
|
if (err) {
|
||||||
|
Log.error('JWT_SESSION_DELETE_EXPIRED_ERROR', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.info('JWT_SESSION_DELETE_EXPIRED', err);
|
||||||
|
});
|
||||||
return void no();
|
return void no();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,10 @@ Basic.read = function (Env, path, cb) {
|
||||||
cb(void 0, content);
|
cb(void 0, content);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Basic.readDir = function (Env, path, cb) {
|
||||||
|
if (!path) { return void pathError(cb); }
|
||||||
|
Fs.readdir(path, cb);
|
||||||
|
};
|
||||||
|
|
||||||
Basic.write = function (Env, path, data, cb) {
|
Basic.write = function (Env, path, data, cb) {
|
||||||
if (!path) { return void pathError(cb); }
|
if (!path) { return void pathError(cb); }
|
||||||
|
@ -57,4 +61,9 @@ Basic.delete = function (Env, path, cb) {
|
||||||
if (!path) { return void pathError(cb); }
|
if (!path) { return void pathError(cb); }
|
||||||
Fs.rm(path, cb);
|
Fs.rm(path, cb);
|
||||||
};
|
};
|
||||||
|
Basic.deleteDir = function (Env, path, cb) {
|
||||||
|
if (!path) { return void pathError(cb); }
|
||||||
|
Fs.rm(path, { recursive: true, force: true }, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,20 @@ Sessions.delete = function (Env, id, ref, cb) {
|
||||||
Basic.delete(Env, path, cb);
|
Basic.delete(Env, path, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Sessions.deleteUser = function (Env, id, cb) {
|
||||||
|
if (!id || typeof(id) !== 'string') { return; }
|
||||||
|
id = Util.escapeKeyCharacters(id);
|
||||||
|
var dirPath = Path.join(Env.paths.base, "sessions", id.slice(0, 2), id);
|
||||||
|
|
||||||
|
Basic.readDir(Env, dirPath, (err, files) => {
|
||||||
|
var checkContent = !files || (Array.isArray(files) && files.every((file) => {
|
||||||
|
return file && file.length === 32;
|
||||||
|
}));
|
||||||
|
if (!checkContent) { return void cb('INVALID_SESSIONS_DIR'); }
|
||||||
|
Basic.deleteDir(Env, dirPath, cb);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// XXX All of a user's sessions should be removed When a user deletes their account
|
// XXX All of a user's sessions should be removed When a user deletes their account
|
||||||
// The fact that each user is given their own publicKey-scoped directory makes them easy
|
// The fact that each user is given their own publicKey-scoped directory makes them easy
|
||||||
// to remove all at once. Nodejs provides an easy way to `rm -rf` since 14.14.0:
|
// to remove all at once. Nodejs provides an easy way to `rm -rf` since 14.14.0:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* jshint esversion: 7 */
|
||||||
define([], function () {
|
define([], function () {
|
||||||
// Based on https://gist.github.com/bellbind/871b145110c458e83077a718aef9fa0e
|
// Based on https://gist.github.com/bellbind/871b145110c458e83077a718aef9fa0e
|
||||||
|
|
||||||
|
@ -42,7 +43,7 @@ define([], function () {
|
||||||
}
|
}
|
||||||
function b32d(bs) {
|
function b32d(bs) {
|
||||||
const len = bs.length;
|
const len = bs.length;
|
||||||
if (len === 0) return new Uint8Array([]);
|
if (len === 0) { return new Uint8Array([]); }
|
||||||
//console.assert(len % 8 === 0, len);
|
//console.assert(len % 8 === 0, len);
|
||||||
const pad = len - bs.indexOf("="), rem = b32pad.indexOf(pad);
|
const pad = len - bs.indexOf("="), rem = b32pad.indexOf(pad);
|
||||||
//console.assert(rem >= 0, pad);
|
//console.assert(rem >= 0, pad);
|
||||||
|
|
|
@ -175,7 +175,7 @@ Note: This must currently be reversed manually (by deleting the mfa config file)
|
||||||
$deriveKeys.click(function () {
|
$deriveKeys.click(function () {
|
||||||
if (BUSY) { return; }
|
if (BUSY) { return; }
|
||||||
|
|
||||||
var name = $username.val().trim()
|
var name = $username.val().trim();
|
||||||
var password = $password.val();
|
var password = $password.val();
|
||||||
|
|
||||||
if (!name) { return void window.alert("Invalid name"); }
|
if (!name) { return void window.alert("Invalid name"); }
|
||||||
|
@ -212,7 +212,7 @@ Note: This must currently be reversed manually (by deleting the mfa config file)
|
||||||
|
|
||||||
// TOTP app configuration
|
// TOTP app configuration
|
||||||
|
|
||||||
var $generateSecret = $('#generate-secret')
|
var $generateSecret = $('#generate-secret');
|
||||||
var $b32Secret = $('#base32-secret');
|
var $b32Secret = $('#base32-secret');
|
||||||
|
|
||||||
var randomSecret = () => {
|
var randomSecret = () => {
|
||||||
|
|
|
@ -2513,6 +2513,10 @@ define([
|
||||||
// when it was expected. Log them out and redirect them to
|
// when it was expected. Log them out and redirect them to
|
||||||
// the login page, where they will be able to authenticate
|
// the login page, where they will be able to authenticate
|
||||||
// and request a new JWT
|
// and request a new JWT
|
||||||
|
|
||||||
|
// XXX We may only require them to provid a new TOTP code here
|
||||||
|
// instead of redirecting them to the login page
|
||||||
|
|
||||||
waitFor.abort();
|
waitFor.abort();
|
||||||
return void LocalStore.logout(function () {
|
return void LocalStore.logout(function () {
|
||||||
requestLogin();
|
requestLogin();
|
||||||
|
|
|
@ -15,6 +15,7 @@ define([
|
||||||
'/common/make-backup.js',
|
'/common/make-backup.js',
|
||||||
'/common/common-feedback.js',
|
'/common/common-feedback.js',
|
||||||
'/common/common-constants.js',
|
'/common/common-constants.js',
|
||||||
|
'/customize.dist/login.js',
|
||||||
|
|
||||||
'/common/jscolor.js',
|
'/common/jscolor.js',
|
||||||
'/bower_components/file-saver/FileSaver.min.js',
|
'/bower_components/file-saver/FileSaver.min.js',
|
||||||
|
@ -37,7 +38,8 @@ define([
|
||||||
ApiConfig,
|
ApiConfig,
|
||||||
Backup,
|
Backup,
|
||||||
Feedback,
|
Feedback,
|
||||||
Constants
|
Constants,
|
||||||
|
Login
|
||||||
) {
|
) {
|
||||||
var saveAs = window.saveAs;
|
var saveAs = window.saveAs;
|
||||||
var APP = window.APP = {};
|
var APP = window.APP = {};
|
||||||
|
@ -47,6 +49,17 @@ define([
|
||||||
var privateData;
|
var privateData;
|
||||||
var sframeChan;
|
var sframeChan;
|
||||||
|
|
||||||
|
Messages.settings_totpTitle = "TOTP"; // 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_totp_code = "OTP code"; // XXX
|
||||||
|
Messages.settings_totp_code_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
|
||||||
|
|
||||||
var categories = {
|
var categories = {
|
||||||
'account': [ // Msg.settings_cat_account
|
'account': [ // Msg.settings_cat_account
|
||||||
'cp-settings-own-drive',
|
'cp-settings-own-drive',
|
||||||
|
@ -57,6 +70,10 @@ define([
|
||||||
'cp-settings-change-password',
|
'cp-settings-change-password',
|
||||||
'cp-settings-delete'
|
'cp-settings-delete'
|
||||||
],
|
],
|
||||||
|
'access': [ // Msg.settings_cat_access // XXX
|
||||||
|
// XXX add password change and account deletion here?
|
||||||
|
'cp-settings-totp'
|
||||||
|
],
|
||||||
'security': [ // Msg.settings_cat_security
|
'security': [ // Msg.settings_cat_security
|
||||||
'cp-settings-logout-everywhere',
|
'cp-settings-logout-everywhere',
|
||||||
'cp-settings-autostore',
|
'cp-settings-autostore',
|
||||||
|
@ -777,6 +794,238 @@ define([
|
||||||
cb($inputBlock);
|
cb($inputBlock);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
|
||||||
|
// Account access
|
||||||
|
|
||||||
|
var drawTotp = function (content, enabled) {
|
||||||
|
var $content = $(content).empty();
|
||||||
|
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', {
|
||||||
|
type: 'password',
|
||||||
|
placeholder: Messages.login_password,
|
||||||
|
})),
|
||||||
|
OTPEntry = h('input', {
|
||||||
|
placeholder: Messages.settings_totp_code
|
||||||
|
}),
|
||||||
|
disable
|
||||||
|
]));
|
||||||
|
var $b = $(disable);
|
||||||
|
var $OTPEntry = $(OTPEntry);
|
||||||
|
UI.confirmButton(disable, {
|
||||||
|
classes: 'btn-danger',
|
||||||
|
multiple: true
|
||||||
|
}, function () {
|
||||||
|
$b.attr('disabled', 'disabled');
|
||||||
|
var name = privateData.accountName;
|
||||||
|
var password = $(pwInput).val();
|
||||||
|
|
||||||
|
// 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_BLOCK", {
|
||||||
|
blockHash: result.blockHash,
|
||||||
|
}, function (err, obj) {
|
||||||
|
if (!obj || !obj.correct) {
|
||||||
|
UI.warn(Messages.login_noSuchUser);
|
||||||
|
$b.removeAttr('disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var blockKeys = result.blockKeys;
|
||||||
|
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) {
|
||||||
|
$b.removeAttr('disabled');
|
||||||
|
return void UI.warn(Messages.settings_totp_code_invalid);
|
||||||
|
}
|
||||||
|
drawTotp(content, false);
|
||||||
|
}, {raw: true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var button = h('button.btn.btn-primary', Messages.settings_totp_enable);
|
||||||
|
$(button).click(function () {
|
||||||
|
$content.empty();
|
||||||
|
var Base32, Block, QRCode, Nacl;
|
||||||
|
var blockKeys;
|
||||||
|
nThen(function (waitFor) {
|
||||||
|
require([
|
||||||
|
'/auth/base32.js',
|
||||||
|
'/common/outer/login-block.js',
|
||||||
|
'/lib/qrcode.min.js',
|
||||||
|
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||||
|
], waitFor(function (_Base32, _Login, _Block) {
|
||||||
|
Base32 = _Base32;
|
||||||
|
Block = _Block;
|
||||||
|
QRCode = window.QRCode;
|
||||||
|
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_BLOCK", {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}).nThen(function () {
|
||||||
|
var randomSecret = function () {
|
||||||
|
var U8 = Nacl.randomBytes(20);
|
||||||
|
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) {
|
||||||
|
new QRCode(target, uri);
|
||||||
|
}, 400);
|
||||||
|
var updateURI = function (secret) {
|
||||||
|
$container.empty();
|
||||||
|
|
||||||
|
var username = privateData.accountName;
|
||||||
|
var hostname = new URL(privateData.origin).hostname;
|
||||||
|
var label = "CryptPad";
|
||||||
|
|
||||||
|
var uri = `otpauth://totp/${label}:${username}@${hostname}?secret=${secret}`;
|
||||||
|
|
||||||
|
var qr = h('div');
|
||||||
|
var uriInput = UI.dialog.selectable(uri);
|
||||||
|
updateQR(uri, qr);
|
||||||
|
|
||||||
|
var OTPEntry = h('input', {
|
||||||
|
placeholder: Messages.settings_totp_code
|
||||||
|
});
|
||||||
|
var $OTPEntry = $(OTPEntry);
|
||||||
|
|
||||||
|
var description = h('p', Messages.settings_totp_tuto);
|
||||||
|
var confirmOTP = h('button.btn.btn-primary', Messages.settings_totp_confirm);
|
||||||
|
var $confirmBtn = $(confirmOTP);
|
||||||
|
var lock = false;
|
||||||
|
UI.confirmButton(confirmOTP, {
|
||||||
|
multiple: true
|
||||||
|
}, function () {
|
||||||
|
var code = $OTPEntry.val();
|
||||||
|
if (code.length !== 6 || /\D/.test(code)) {
|
||||||
|
return void UI.warn(Messages.error); // XXX
|
||||||
|
}
|
||||||
|
$confirmBtn.attr('disabled', 'disabled');
|
||||||
|
lock = true;
|
||||||
|
|
||||||
|
sframeChan.query("Q_SETTINGS_TOTP_SETUP", {
|
||||||
|
key: blockKeys.sign,
|
||||||
|
data: {
|
||||||
|
command: 'TOTP_SETUP',
|
||||||
|
secret: secret,
|
||||||
|
code: code,
|
||||||
|
}
|
||||||
|
}, function (err, obj) {
|
||||||
|
lock = false;
|
||||||
|
$OTPEntry.val("");
|
||||||
|
if (err || !obj || !obj.success) {
|
||||||
|
$confirmBtn.removeAttr('disabled');
|
||||||
|
console.error(err);
|
||||||
|
return void UI.warn(Messages.error);
|
||||||
|
}
|
||||||
|
drawTotp(content, true);
|
||||||
|
}, {raw: true});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.append([
|
||||||
|
uriInput,
|
||||||
|
qr,
|
||||||
|
h('br'),
|
||||||
|
description,
|
||||||
|
OTPEntry,
|
||||||
|
confirmOTP
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var $g = $(generate).click(function () {
|
||||||
|
$g.remove();
|
||||||
|
var secret = randomSecret();
|
||||||
|
updateURI(secret);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}).appendTo(content);
|
||||||
|
};
|
||||||
|
makeBlock('totp', function (cb) { // Msg.settings_totpTitle
|
||||||
|
if (!common.isLoggedIn()) { return void cb(false); }
|
||||||
|
|
||||||
|
var content = h('div');
|
||||||
|
sframeChan.query('Q_SETTINGS_TOTP_CHECK', {}, function (err, obj) {
|
||||||
|
if (err || !obj || (obj && obj.err === 'NOBLOCK')) { return void cb(false); }
|
||||||
|
var enabled = obj && obj.totp;
|
||||||
|
drawTotp(content, Boolean(enabled));
|
||||||
|
cb(content);
|
||||||
|
});
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Security
|
// Security
|
||||||
|
|
||||||
makeBlock('safe-links', function(cb) { // Msg.settings_safeLinksTitle
|
makeBlock('safe-links', function(cb) { // Msg.settings_safeLinksTitle
|
||||||
|
|
|
@ -70,6 +70,47 @@ 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_BLOCK', function (data, cb) {
|
||||||
|
cb({correct: data.blockHash === Utils.LocalStore.getBlockHash() });
|
||||||
|
});
|
||||||
|
sframeChan.on('Q_SETTINGS_TOTP_SETUP', 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.bearer) });
|
||||||
|
console.log(response);
|
||||||
|
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) });
|
||||||
|
console.error(response);
|
||||||
|
if (response && response.success) {
|
||||||
|
Utils.LocalStore.setSessionToken('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
sframeChan.on('Q_SETTINGS_TOTP_CHECK', function (obj, cb) {
|
||||||
|
require([
|
||||||
|
'/common/outer/login-block.js',
|
||||||
|
], function (Block) {
|
||||||
|
var blockHash = Utils.LocalStore.getBlockHash();
|
||||||
|
if (!blockHash) { return void cb({ err: 'NOBLOCK' }); }
|
||||||
|
var parsed = Block.parseBlockHash(blockHash);
|
||||||
|
Utils.Util.getBlock(parsed.href, {}, function (err) {
|
||||||
|
cb({totp: err === 401});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
sframeChan.on('Q_SETTINGS_DELETE_ACCOUNT', function (data, cb) {
|
sframeChan.on('Q_SETTINGS_DELETE_ACCOUNT', function (data, cb) {
|
||||||
Cryptpad.deleteAccount(data, cb);
|
Cryptpad.deleteAccount(data, cb);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue