TOTP: Use HTTP challenges to write and remove blocks

This commit is contained in:
yflory 2023-06-09 15:06:17 +02:00
parent b3a620edc0
commit 9aac9d1c2f
22 changed files with 893 additions and 644 deletions

View File

@ -174,7 +174,7 @@ define([
blockUrl = Block.getBlockUrl(res.opt.blockKeys);
var TOTP_prompt = function (err, cb) {
onOTP(err, function (code) {
onOTP(function (code) {
ServerCommand(res.opt.blockKeys.sign, {
command: 'TOTP_VALIDATE',
code: code,
@ -185,7 +185,7 @@ define([
// allow them to specify a lifetime for the session?
// "log me out after one day"?
}, cb);
});
}, false, err);
};
var done = waitFor();
@ -508,8 +508,10 @@ define([
toPublish[Constants.userHashKey] = userHash;
toPublish.edPublic = RT.proxy.edPublic;
var blockRequest = Block.serialize(JSON.stringify(toPublish), res.opt.blockKeys);
rpc.writeLoginBlock(blockRequest, waitFor(function (e) {
Block.writeLoginBlock({
blockKeys: blockKeys,
content: toPublish
}, waitFor(function (e) {
if (e) {
console.error(e);
waitFor.abort();

View File

@ -0,0 +1,76 @@
const Block = require("../commands/block");
const MFA = require("../storage/mfa");
const Util = require("../common-util");
const Commands = module.exports;
var isValidBlockId = Block.isValidBlockId;
// Read the MFA settings for the given public key
const checkMFA = (Env, publicKey, cb) => {
// Success if we can't get the MFA settings
MFA.read(Env, publicKey, function (err, content) {
if (err) {
if (err.code !== "ENOENT") {
Env.Log.error('TOTP_VALIDATE_MFA_READ', {
error: err,
publicKey: publicKey,
});
}
return void cb();
}
var parsed = Util.tryParse(content);
if (!parsed) { return void cb(); }
cb("NOT_ALLOWED");
});
};
// Make sure the block is not protected by MFA but don't do anything else
const check = Commands.MFA_CHECK = function (Env, body, cb) {
var { publicKey } = body;
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
checkMFA(Env, publicKey, cb);
};
check.complete = function (Env, body, cb) { cb(); };
// Write a login block IFF
// 1. You can sign for the block's public key
// 2. the block is not protected by MFA
// Note: the internal WRITE_LOGIN_BLOCK will check is you're allowed to create this block
const writeBlock = Commands.WRITE_BLOCK = function (Env, body, cb) {
const { publicKey, content } = body;
// they must provide a valid block public key
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
if (publicKey !== content.publicKey) { return void cb("INVALID_KEY"); }
// check MFA
checkMFA(Env, publicKey, cb);
};
writeBlock.complete = function (Env, body, cb) {
const { content } = body;
Block.writeLoginBlock(Env, content, cb);
};
// Remove a login block IFF
// 1. You can sign for the block's public key
// 2. the block is not protected by MFA
const removeBlock = Commands.REMOVE_BLOCK = function (Env, body, cb) {
const { publicKey } = body;
// they must provide a valid block public key
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
// check MFA
checkMFA(Env, publicKey, cb);
};
removeBlock.complete = function (Env, body, cb) {
const { publicKey } = body;
Block.removeLoginBlock(Env, publicKey, cb);
};

View File

@ -7,7 +7,8 @@ const Util = require("../common-util");
const MFA = require("../storage/mfa");
const Sessions = require("../storage/sessions");
const Block = require("../storage/block");
const BlockStore = require("../storage/block");
const Block = require("../commands/block");
const Commands = module.exports;
@ -38,9 +39,7 @@ var isValidRecoveryKey = otp => {
// this check doesn't confirm that their id is valid base64
// any attempt relying on this should fail when we can't decode
// the id they provided.
var isValidBlockId = id => {
return id && isString(id) && id.length === 44;
};
var isValidBlockId = Block.isValidBlockId;
// the base32 library can throw when decoding under various conditions.
// we have some basic requirements for the length of base32 as well,
@ -83,6 +82,92 @@ var createJWT = function (Env, sessionId, publicKey, cb) {
});
};
// Create a session with a token for the given public key
const makeSession = (Env, publicKey, cb) => {
const sessionId = Sessions.randomId();
var token;
nThen(function (w) {
createJWT(Env, sessionId, publicKey, w(function (err, _token) {
if (err) {
Env.Log.error("TOTP_VALIDATE_JWT_SIGN_ERROR", {
error: Util.serializeError(err),
publicKey: publicKey,
});
w.abort();
return void cb("TOKEN_ERROR");
}
token = _token;
}));
}).nThen(function (w) {
// store the token
Sessions.write(Env, publicKey, sessionId, token, w(function (err) {
if (err) {
Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", {
error: Util.serializeError(err),
publicKey: publicKey,
sessionId: sessionId,
});
w.abort();
return void cb("SESSION_WRITE_ERROR");
}
// else continue
}));
}).nThen(function () {
cb(void 0, {
bearer: token,
});
});
};
// Read the MFA settings for the given public key
const readMFA = (Env, publicKey, cb) => {
// check that there is an MFA configuration for the given account
MFA.read(Env, publicKey, function (err, content) {
if (err) {
Env.Log.error('TOTP_VALIDATE_MFA_READ', {
error: err,
publicKey: publicKey,
});
return void cb('NO_MFA_CONFIGURED');
}
var parsed = Util.tryParse(content);
if (!parsed) { return void cb("INVALID_CONFIGURATION"); }
cb(undefined, parsed);
});
};
// Check if an OTP code is valid against the provided secret
const checkCode = (Env, secret, code, publicKey, _cb) => {
const cb = Util.mkAsync(_cb);
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();
};
// This command allows clients to configure TOTP as a second factor protecting
// their login block IFF they:
// 1. provide a sufficiently strong TOTP secret
@ -154,11 +239,9 @@ TOTP_SETUP.complete = function (Env, body, cb) {
// the remainder of the setup is successfully completed.
// Otherwise they would have to reauthenticate.
// The session id is used as a reference to this particular session.
const sessionId = Sessions.randomId();
var token;
nThen(function (w) {
// confirm that the block exists
Block.check(Env, publicKey, w(function (err) {
BlockStore.check(Env, publicKey, w(function (err) {
if (err) {
Env.Log.error("TOTP_SETUP_NO_BLOCK", {
publicKey,
@ -195,43 +278,11 @@ TOTP_SETUP.complete = function (Env, body, cb) {
}
// otherwise continue
}));
}).nThen(function (w) {
// generate a bearer token and store it
createJWT(Env, sessionId, publicKey, w(function (err, _token) {
if (err) {
}).nThen(function () {
// we have already stored the MFA data, which will cause access to the resource to be restricted to the provided TOTP secret.
// we attempt to create a session as a matter of convenience - so if it fails
// that just means they'll be forced to authenticate
Env.Log.error("TOTP_SETUP_JWT_SIGN_ERROR", {
error: err,
publicKey: publicKey,
});
return void cb('TOKEN_ERROR');
}
token = _token;
}));
}).nThen(function (w) {
// store the token
Sessions.write(Env, publicKey, sessionId, token, w(function (err) {
if (err) {
// again, if there's a failure here the user should automatically
// be forced to reauthenticate because their block is protected
// but they will not have a valid JWT allowing them to access it
Env.Log.error("TOTP_SETUP_SESSION_WRITE", {
error: err,
publicKey: publicKey,
sessionId: sessionId,
});
w.abort();
return void cb("SESSION_WRITE_ERROR");
}
// else continue
}));
}).nThen(function () {
// respond with the stored token that they can now use to authenticate
cb(void 0, {
bearer: token,
});
makeSession(Env, publicKey, cb);
});
};
@ -253,51 +304,15 @@ const validate = Commands.TOTP_VALIDATE = function (Env, body, cb) {
var secret;
nThen(function (w) {
// check that there is an MFA configuration for the given account
MFA.read(Env, publicKey, w(function (err, content) {
readMFA(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');
return void cb(err);
}
var parsed = Util.tryParse(content);
if (!parsed) {
w.abort();
return void cb("INVALID_CONFIGURATION");
}
secret = parsed.secret;
secret = content.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();
checkCode(Env, secret, code, publicKey, cb);
});
};
@ -316,43 +331,31 @@ So, we should:
*/
var { publicKey } = body;
const sessionId = Sessions.randomId();
var token;
nThen(function (w) {
createJWT(Env, sessionId, publicKey, w(function (err, _token) {
if (err) {
Env.Log.error("TOTP_VALIDATE_JWT_SIGN_ERROR", {
error: Util.serializeError(err),
publicKey: publicKey,
});
return void cb("TOKEN_ERROR");
}
token = _token;
}));
}).nThen(function (w) {
// store the token
Sessions.write(Env, publicKey, sessionId, token, w(function (err) {
if (err) {
Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", {
error: Util.serializeError(err),
publicKey: publicKey,
sessionId: sessionId,
});
w.abort();
return void cb("SESSION_WRITE_ERROR");
}
// else continue
}));
}).nThen(function () {
cb(void 0, {
bearer: token,
});
});
makeSession(Env, publicKey, cb);
};
// This command is somewhat simpler than TOTP_SETUP
// Same as TOTP_VALIDATE but without making a session at the end
const check = Commands.TOTP_CHECK = function (Env, body, cb) {
var { publicKey, auth } = body;
const code = auth;
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
var secret;
nThen(function (w) {
readMFA(Env, publicKey, w(function (err, content) {
if (err) {
w.abort();
return void cb(err);
}
secret = content.secret;
}));
}).nThen(function () {
checkCode(Env, secret, code, publicKey, cb);
});
};
check.complete = function (Env, body, cb) { cb(); };
// 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
@ -370,25 +373,13 @@ const revoke = Commands.TOTP_REVOKE = function (Env, body, cb) {
var secret, recoveryStored;
nThen(function (w) {
// check that there is an MFA configuration for the given account
MFA.read(Env, publicKey, w(function (err, content) {
readMFA(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');
return void cb(err);
}
var parsed = Util.tryParse(content);
if (!parsed) {
w.abort();
return void cb("INVALID_CONFIGURATION");
}
secret = parsed.secret;
recoveryStored = parsed.contact;
secret = content.secret;
recoveryStored = content.contact;
}));
}).nThen(function (w) {
if (!recoveryKey) { return; }
@ -402,31 +393,7 @@ const revoke = Commands.TOTP_REVOKE = function (Env, body, cb) {
}
cb();
}).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();
checkCode(Env, secret, code, publicKey, cb);
});
};
@ -447,3 +414,116 @@ So, we should:
MFA.revoke(Env, publicKey, cb);
};
// Write a login block using an existing OTP block IFF
// 1. You can sign for the block's public key
// 2. You have a proof for the old block
// 3. The old block is OTP protected
// 4. The OTP code is valid
// Note: this is used when users change their password
const writeBlock = Commands.TOTP_WRITE_BLOCK = function (Env, body, cb) {
const { publicKey, content } = body;
const code = content.auth;
const registrationProof = content.registrationProof;
// they must provide a valid block public key
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
if (publicKey !== content.publicKey) { return void cb("INVALID_KEY"); }
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
if (!registrationProof) { return void cb('MISSING_ANCESTOR'); }
let secret;
let oldKey;
nThen(function (w) {
Block.validateAncestorProof(Env, registrationProof, w((err, provenKey) => {
if (err || !provenKey) {
w.abort();
return void cb('INVALID_ANCESTOR');
}
oldKey = provenKey;
}));
}).nThen(function (w) {
// check that there is an MFA configuration for the ancestor account
readMFA(Env, oldKey, w(function (err, content) {
if (err) {
w.abort();
return void cb(err);
}
secret = content.secret;
}));
}).nThen(function () {
// check that the OTP code is valid
checkCode(Env, secret, code, oldKey, cb);
});
};
writeBlock.complete = function (Env, body, cb) {
const { publicKey, content } = body;
nThen(function (w) {
// Write new block
Block.writeLoginBlock(Env, content, w((err) => {
if (err) {
w.abort();
return void cb("BLOCK_WRITE_ERROR");
}
}));
}).nThen(function (w) {
// Copy MFA settings
const proof = Util.tryParse(content.registrationProof);
const oldKey = proof && proof[0];
if (!oldKey) {
w.abort();
return void cb('INVALID_ANCESTOR');
}
MFA.copy(Env, oldKey, publicKey, w());
}).nThen(function () {
// Create a session for the current user
makeSession(Env, publicKey, cb);
});
};
// Remove a login block IFF
// 1. You can sign for the block's public key
const removeBlock = Commands.TOTP_REMOVE_BLOCK = function (Env, body, cb) {
const { publicKey, auth } = body;
const code = auth;
// they must provide a valid block public key
if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); }
if (!isValidOTP(code)) { return void cb('E_INVALID'); }
let secret;
nThen(function (w) {
// check that there is an MFA configuration for this block
readMFA(Env, publicKey, w(function (err, content) {
if (err) {
w.abort();
return void cb(err);
}
secret = content.secret;
}));
}).nThen(function () {
// check that the OTP code is valid
checkCode(Env, secret, code, publicKey, cb);
});
};
removeBlock.complete = function (Env, body, cb) {
const { publicKey } = body;
nThen(function (w) {
// Remove the block
Block.removeLoginBlock(Env, publicKey, w((err) => {
if (err) {
w.abort();
return void cb(err);
}
}));
}).nThen(() => {
// Delete the MFA settings and sessions
MFA.revoke(Env, publicKey, cb);
});
};

View File

@ -6,6 +6,11 @@ const nThen = require("nthen");
const Util = require("../common-util");
const BlockStore = require("../storage/block");
var isString = s => typeof(s) === 'string';
Block.isValidBlockId = id => {
return id && isString(id) && id.length === 44;
};
/*
We assume that the server is secured against MitM attacks
via HTTPS, and that malicious actors do not have code execution
@ -98,33 +103,24 @@ Block.validateAncestorProof = function (Env, proof, _cb) {
}
};
Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
Block.writeLoginBlock = function (Env, msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var publicKey = msg[0];
var signature = msg[1];
var block = msg[2];
var registrationProof = msg[3];
var previousKey;
const { publicKey, signature, ciphertext, registrationProof } = msg;
var previousKey;
var validatedBlock, path;
nThen(function (w) {
if (Util.escapeKeyCharacters(publicKey) !== safeKey) {
w.abort();
return void cb("INCORRECT_KEY");
}
}).nThen(function (w) {
if (!Env.restrictRegistration) { return; }
if (!registrationProof) {
// we allow users with existing blocks to create new ones
// call back with error if registration is restricted and no proof of an existing block was provided
w.abort();
Env.Log.info("BLOCK_REJECTED_REGISTRATION", {
safeKey: safeKey,
publicKey: publicKey,
});
return cb("E_RESTRICTED");
}
Env.validateAncestorProof(registrationProof, w(function (err, provenKey) {
Block.validateAncestorProof(Env, registrationProof, w(function (err, provenKey) {
if (err || !provenKey) { // double check that a key was validated
w.abort();
Env.Log.warn('BLOCK_REJECTED_INVALID_ANCESTOR', {
@ -135,7 +131,7 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
previousKey = provenKey;
}));
}).nThen(function (w) {
Env.validateLoginBlock(publicKey, signature, block, w(function (e, _validatedBlock) {
Block.validateLoginBlock(Env, publicKey, signature, ciphertext, w(function (e, _validatedBlock) {
if (e) {
w.abort();
return void cb(e);
@ -156,7 +152,6 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
}
BlockStore.write(Env, publicKey, buffer, function (err) {
Env.Log.info('BLOCK_WRITE_BY_OWNER', {
safeKey: safeKey,
blockId: publicKey,
isChange: Boolean(registrationProof),
previousKey: previousKey,
@ -167,8 +162,6 @@ Block.writeLoginBlock = function (Env, safeKey, msg, _cb) {
});
};
const DELETE_BLOCK = Nacl.util.encodeBase64(Nacl.util.decodeUTF8('DELETE_BLOCK'));
/*
When users write a block, they upload the block, and provide
a signature proving that they deserve to be able to write to
@ -179,24 +172,9 @@ const DELETE_BLOCK = Nacl.util.encodeBase64(Nacl.util.decodeUTF8('DELETE_BLOCK')
information, we can just sign some constant and use that as proof.
*/
Block.removeLoginBlock = function (Env, safeKey, msg, _cb) {
// XXX respect MFA settings if they exist
// XXX delete MFA settings if they are able to authenticate
// XXX clean up any existing sessions when deleting
// TODO This should probably be converted to run as challenge-response commands
Block.removeLoginBlock = function (Env, publicKey, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var publicKey = msg[0];
var signature = msg[1];
nThen(function (w) {
if (Util.escapeKeyCharacters(publicKey) !== safeKey) {
w.abort();
return void cb("INCORRECT_KEY");
}
}).nThen(function () {
Env.validateLoginBlock(publicKey, signature, DELETE_BLOCK, function (e) {
if (e) { return void cb(e); }
BlockStore.archive(Env, publicKey, function (err) {
Env.Log.info('ARCHIVAL_BLOCK_BY_OWNER_RPC', {
publicKey: publicKey,
@ -204,7 +182,5 @@ Block.removeLoginBlock = function (Env, safeKey, msg, _cb) {
});
cb(err);
});
});
});
};

View File

@ -60,10 +60,19 @@ var COMMANDS = {};
// Methods allowing clients to configure Time-based One-Time Passwords for their login-block,
// and to authenticate new sessions once a TOTP secret has been associated with their account,
const NOAUTH = require("./challenge-commands/base.js");
COMMANDS.MFA_CHECK = NOAUTH.MFA_CHECK;
COMMANDS.WRITE_BLOCK = NOAUTH.WRITE_BLOCK;
COMMANDS.REMOVE_BLOCK = NOAUTH.REMOVE_BLOCK;
const TOTP = require("./challenge-commands/totp.js");
COMMANDS.TOTP_SETUP = TOTP.TOTP_SETUP;
COMMANDS.TOTP_VALIDATE = TOTP.TOTP_VALIDATE;
COMMANDS.TOTP_CHECK = TOTP.TOTP_CHECK;
COMMANDS.TOTP_REVOKE = TOTP.TOTP_REVOKE;
COMMANDS.TOTP_WRITE_BLOCK = TOTP.TOTP_WRITE_BLOCK;
COMMANDS.TOTP_REMOVE_BLOCK = TOTP.TOTP_REMOVE_BLOCK;
var randomToken = () => Nacl.util.encodeBase64(Nacl.randomBytes(24)).replace(/\//g, '-');

View File

@ -51,6 +51,7 @@ Logger.levels.forEach(level => {
};
});
Env.Log = Log;
Env.incrementBytesWritten = function () {};
const EVENTS = {};
@ -58,6 +59,7 @@ EVENTS.ENV_UPDATE = function (data /*, cb */) {
try {
Env = JSON.parse(data);
Env.Log = Log;
Env.incrementBytesWritten = function () {};
} catch (err) {
Log.error('HTTP_WORKER_ENV_UPDATE', Util.serializeError(err));
}

View File

@ -5,7 +5,6 @@ const Core = require("./commands/core");
const Admin = require("./commands/admin-rpc");
const Pinning = require("./commands/pin-rpc");
const Quota = require("./commands/quota");
const Block = require("./commands/block");
const Metadata = require("./commands/metadata");
const Channel = require("./commands/channel");
const Upload = require("./commands/upload");
@ -54,8 +53,6 @@ const AUTHENTICATED_USER_TARGETED = {
UPLOAD_COMPLETE: Upload.complete,
UPLOAD_CANCEL: Upload.cancel,
OWNED_UPLOAD_COMPLETE: Upload.complete_owned,
WRITE_LOGIN_BLOCK: Block.writeLoginBlock,
REMOVE_LOGIN_BLOCK: Block.removeLoginBlock,
ADMIN: Admin.command,
SET_METADATA: Metadata.setMetadata,
};

View File

@ -57,7 +57,7 @@ MFA.revoke = function (Env, publicKey, cb) {
}).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.
// If we can't delete the sessions, don't send an error, 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,
@ -69,6 +69,20 @@ MFA.revoke = function (Env, publicKey, cb) {
success: true
});
});
};
MFA.copy = function (Env, oldKey, newKey, cb) {
let content;
nThen(function (w) {
MFA.read(Env, oldKey, w(function (err, c) {
if (err) {
// No MFA configured, nothing to copy
w.abort();
return void cb();
}
content = c;
}));
}).nThen(function () {
MFA.write(Env, newKey, content, cb);
});
};

View File

@ -12,6 +12,7 @@ define([
'/common/common-util.js',
'/common/pinpad.js',
'/common/outer/network-config.js',
'/common/outer/login-block.js',
'/customize/pages.js',
'/checkup/checkup-tools.js',
'/customize/application_config.js',
@ -21,7 +22,7 @@ define([
'less!/checkup/app-checkup.less',
], function ($, ApiConfig, Assertions, h, Messages, DomReady,
nThen, SFCommonO, Login, Hash, Util, Pinpad,
NetConfig, Pages, Tools, AppConfig) {
NetConfig, Block, Pages, Tools, AppConfig) {
window.CHECKUP_MAIN_LOADED = true;
var Assert = Assertions();
@ -306,9 +307,8 @@ define([
var opt = Login.allocateBytes(bytes);
var blockKeys = opt.blockKeys;
var blockUrl = Login.Block.getBlockUrl(opt.blockKeys);
var blockRequest = Login.Block.serialize("{}", opt.blockKeys);
var removeRequest = Login.Block.remove(opt.blockKeys);
console.warn('Testing block URL (%s). One 404 is normal.', blockUrl);
var userHash = '/2/drive/edit/000000000000000000000000';
@ -319,7 +319,7 @@ define([
var RT, rpc, exists, restricted;
nThen(function (waitFor) {
Util.fetch(blockUrl, waitFor(function (err) {
Util.getBlock(blockUrl, {}, waitFor(function (err) {
if (err) { return; } // No block found
exists = true;
}));
@ -344,20 +344,13 @@ define([
proxy.curvePrivate = opt.curvePrivate;
rt.realtime.onSettle(waitFor());
}));
}).nThen(function (waitFor) {
// Init RPC
Pinpad.create(RT.network, RT.proxy, waitFor(function (e, _rpc) {
if (e) {
waitFor.abort();
console.error("Can't initialize RPC", e); // INVALID_KEYS
return void cb(false);
}
rpc = _rpc;
}));
}).nThen(function (waitFor) {
// Write block
if (exists) { return; }
rpc.writeLoginBlock(blockRequest, waitFor(function (e) {
Block.writeLoginBlock({
blockKeys: blockKeys,
content: {}
}, waitFor(function (e) {
// we should tolerate restricted registration
// and proceed to clean up after any data we've created
if (e === 'E_RESTRICTED') {
@ -373,7 +366,7 @@ define([
}).nThen(function (waitFor) {
if (restricted) { return; }
// Read block
Util.fetch(blockUrl, waitFor(function (e) {
Util.getBlock(blockUrl, {}, waitFor(function (e) {
if (e) {
waitFor.abort();
console.error("Can't read login block", e);
@ -382,15 +375,26 @@ define([
}));
}).nThen(function (waitFor) {
// Remove block
rpc.removeLoginBlock(removeRequest, waitFor(function (e) {
Block.removeLoginBlock({
blockKeys: blockKeys,
}, waitFor(function (e) {
if (restricted) { return; } // an ENOENT is expected in the case of restricted registration, but we call this anyway to clean up any mess from previous tests.
if (e) {
waitFor.abort();
console.error("Can't remove login block", e);
console.error(blockRequest);
return void cb(false);
}
}));
}).nThen(function (waitFor) {
// Init RPC
Pinpad.create(RT.network, RT.proxy, waitFor(function (e, _rpc) {
if (e) {
waitFor.abort();
console.error("Can't initialize RPC", e); // INVALID_KEYS
return void cb(false);
}
rpc = _rpc;
}));
}).nThen(function (waitFor) {
rpc.removeOwnedChannel(secret.channel, waitFor(function (e) {
if (e) {

View File

@ -1506,5 +1506,44 @@ 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";
UI.getOTPScreen = function (cb, exitable, err) {
var btn, input;
var error;
if (err) {
error = h('p.cp-password-error', Messages.settings_otp_invalid);
}
var block = h('div#cp-loading-password-prompt', [
error,
h('p.cp-password-info', Messages.loading_enter_otp),
h('p.cp-password-form', [
input = h('input', {
placeholder: Messages.settings_otp_code,
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'off',
spellcheck: false,
}),
btn = h('button.btn.btn-primary', Messages.ui_confirm)
])
]);
var $input = $(input);
var $btn = $(btn).click(function () {
var val = $input.val();
if (!val) { return void UI.getOTPScreen(cb, exitable, 'INVALID_CODE'); }
cb(val);
});
$(input).on('keydown', function (e) {
if (e.which !== 13) { return; } // enter
$btn.click();
});
UI.errorLoadingScreen(block, false, exitable);
};
return UI;
});

View File

@ -334,7 +334,16 @@
// this is resulting in some code duplication
return void CB(void 0, response);
}
CB(response.status);
if (response.status === 401) {
response.json().then((data) => {
CB(401, data);
}).catch(() => {
CB(401);
});
return;
}
CB(response.status, response);
}).catch(error => {
CB(error);
});

View File

@ -12,12 +12,14 @@ define([
'/common/outer/local-store.js',
'/common/outer/worker-channel.js',
'/common/outer/login-block.js',
'/common/common-credential.js',
'/customize/login.js',
'/customize/application_config.js',
'/bower_components/nthen/index.js',
], function (Config, Messages, Util, Hash, Cache,
Messaging, Constants, Feedback, Visible, UserObject, LocalStore, Channel, Block,
AppConfig, Nthen) {
Cred, Login, AppConfig, Nthen) {
/* This file exposes functionality which is specific to Cryptpad, but not to
any particular pad type. This includes functions for committing metadata
@ -616,18 +618,6 @@ define([
});
};
common.writeLoginBlock = function (data, cb) {
postMessage('WRITE_LOGIN_BLOCK', data, function (obj) {
cb(obj);
});
};
common.removeLoginBlock = function (data, cb) {
postMessage('REMOVE_LOGIN_BLOCK', data, function (obj) {
cb(obj);
});
};
// ANON RPC
// SFRAME: talk to anon_rpc from the iframe
@ -1892,71 +1882,18 @@ define([
});
};
var getBlockKeys = function (data, cb) {
var accountName = LocalStore.getAccountName();
var password = data.password;
var Cred, Block, Login;
var blockKeys;
var hash = LocalStore.getUserHash();
if (!hash) { return void cb({ error: 'E_NOT_LOGGED_IN' }); }
var blockHash = LocalStore.getBlockHash();
Nthen(function (waitFor) {
require([
'/common/common-credential.js',
'/common/outer/login-block.js',
'/customize/login.js'
], waitFor(function (_Cred, _Block, _Login) {
Cred = _Cred;
Block = _Block;
Login = _Login;
}));
}).nThen(function (waitFor) {
// confirm that the provided password is correct
Cred.deriveFromPassphrase(accountName, password, Login.requiredBytes,
waitFor(function (bytes) {
var allocated = Login.allocateBytes(bytes);
blockKeys = allocated.blockKeys;
if (blockHash) {
if (blockHash !== allocated.blockHash) {
// incorrect password
console.log("provided password did not yield the correct blockHash");
waitFor.abort();
return void cb({ error: 'INVALID_PASSWORD', });
}
} else {
// otherwise they're a legacy user, and we should check against the User_hash
if (hash !== allocated.userHash) {
// incorrect password
console.log("provided password did not yield the correct userHash");
waitFor.abort();
return void cb({ error: 'INVALID_PASSWORD', });
}
}
}));
}).nThen(function () {
cb({
Cred: Cred,
Block: Block,
Login: Login,
blockKeys: blockKeys
});
});
};
common.deleteAccount = function (data, cb) {
data = data || {};
// Confirm that the provided password is corrct and get the block keys
getBlockKeys(data, function (obj) {
if (obj && obj.error) { return void cb(obj); }
var blockKeys = obj.blockKeys;
var removeData = obj.Block.remove(blockKeys);
var bytes = data.bytes; // From Scrypt
var auth = data.auth; // MFA data
var allocated = Login.allocateBytes(bytes);
var blockKeys = allocated.blockKeys;
postMessage("DELETE_ACCOUNT", {
keys: Block.keysToRPCFormat(blockKeys),
removeData: removeData
keys: blockKeys,
auth: auth
}, function (obj) {
if (obj.state) {
Feedback.send('DELETE_ACCOUNT_AUTOMATIC');
@ -1964,8 +1901,10 @@ define([
Feedback.send('DELETE_ACCOUNT_MANUAL');
}
cb(obj);
});
});
}, {raw: true});
};
common.removeOwnedPads = function (data, cb) {
postMessage("REMOVE_OWNED_PADS", data, cb);
};
common.changeUserPassword = function (Crypt, edPublic, data, cb) {
if (!edPublic) {
@ -1973,36 +1912,28 @@ define([
error: 'E_NOT_LOGGED_IN'
});
}
var accountName = LocalStore.getAccountName();
var hash = LocalStore.getUserHash();
var hash = common.userHash;
if (!hash) {
return void cb({
error: 'E_NOT_LOGGED_IN'
});
}
var password = data.password; // To remove your old block
var newPassword = data.newPassword; // To create your new block
var oldBytes = data.oldBytes; // From Scrypt
var newBytes = data.newBytes; // From Scrypt
var secret = Hash.getSecrets('drive', hash);
var newHash, newHref, newSecret, blockKeys;
var newHash, newHref, newSecret;
var oldIsOwned = false;
var blockHash = LocalStore.getBlockHash();
var oldBlockKeys;
var Cred, Block, Login;
var oldAllocated = Login.allocateBytes(oldBytes);
var newAllocated = Login.allocateBytes(newBytes);
var oldBlockKeys = oldAllocated.blockKeys;
var blockKeys = newAllocated.blockKeys;
var auth = data.auth;
Nthen(function (waitFor) {
getBlockKeys(data, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
oldBlockKeys = obj.blockKeys;
Cred = obj.Cred;
Login = obj.Login;
Block = obj.Block;
}));
}).nThen(function (waitFor) {
// Check if our drive is already owned
console.log("checking if old drive is owned");
common.anonRpcMsg('GET_METADATA', secret.channel, waitFor(function (err, obj) {
@ -2012,6 +1943,17 @@ define([
oldIsOwned = true;
}
}));
}).nThen(function (waitFor) {
Block.checkRights({
auth: auth,
blockKeys: oldBlockKeys,
}, waitFor(function (err) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({ error: 'INVALID_CODE' });
}
}));
}).nThen(function (waitFor) {
// Create a new user hash
// Get the current content, store it in the new user file
@ -2040,19 +1982,13 @@ define([
}
}), optsPut);
}));
}).nThen(function (waitFor) {
// Drive content copied: get the new block location
console.log("deriving new credentials from passphrase");
Cred.deriveFromPassphrase(accountName, newPassword, Login.requiredBytes, waitFor(function (bytes) {
var allocated = Login.allocateBytes(bytes);
blockKeys = allocated.blockKeys;
}));
}).nThen(function (waitFor) {
var blockUrl = Block.getBlockUrl(blockKeys);
// Check whether there is a block at that location
// Check whether there is a block at that new location
Util.fetch(blockUrl, waitFor(function (err, block) {
// If there is no block or the block is invalid, continue.
if (err) {
// error 401 means protected block
if (err && err !== 401) {
console.log("no block found");
return;
}
@ -2069,30 +2005,31 @@ define([
}));
}).nThen(function (waitFor) {
// Write the new login block
var temp = {
var content = {
User_hash: newHash,
edPublic: edPublic,
};
var content = Block.serialize(JSON.stringify(temp), blockKeys);
console.error("OLD AND NEW BLOCK KEYS", oldBlockKeys, blockKeys);
content.registrationProof = Block.proveAncestor(oldBlockKeys);
console.log("writing new login block");
var data = {
keys: Block.keysToRPCFormat(blockKeys),
content: content,
};
common.writeLoginBlock(data, waitFor(function (obj) {
if (obj && obj.error) {
Block.writeLoginBlock({
auth: auth,
blockKeys: blockKeys,
oldBlockKeys: oldBlockKeys,
content: content
}, waitFor(function (err, data) {
if (err) {
waitFor.abort();
return void cb(obj);
return void cb({error: err});
}
if (data && data.bearer) {
LocalStore.setSessionToken(data.bearer);
}
}));
}).nThen(function (waitFor) {
var blockUrl = Block.getBlockUrl(blockKeys);
Util.fetch(blockUrl, waitFor(function (err /* block */) {
var sessionToken = LocalStore.getSessionToken() || undefined;
Util.getBlock(blockUrl, {
bearer: sessionToken,
}, waitFor((err) => {
if (err) {
console.error(err);
waitFor.abort();
@ -2111,18 +2048,16 @@ define([
common.pinPads([newSecret.channel], waitFor());
}).nThen(function (waitFor) {
// Remove block hash
if (blockHash) {
if (!blockHash) { return; }
console.log('removing old login block');
var data = {
keys: Block.keysToRPCFormat(oldBlockKeys), // { edPrivate, edPublic }
content: Block.remove(oldBlockKeys),
};
common.removeLoginBlock(data, waitFor(function (obj) {
if (obj && obj.error) { return void console.error(obj.error); }
Block.removeLoginBlock({
auth: auth,
blockKeys: oldBlockKeys,
}, waitFor(function (err) {
if (err) { return void console.error(err); }
}));
}
}).nThen(function (waitFor) {
if (oldIsOwned) {
if (!oldIsOwned) { return; }
console.log('removing old drive');
common.removeOwnedChannel({
channel: secret.channel,
@ -2138,9 +2073,8 @@ define([
postMessage("DISCONNECT");
}));
}));
}
}).nThen(function (waitFor) {
if (!oldIsOwned) {
if (oldIsOwned) { return; }
console.error('deprecating old drive.');
postMessage("SET", {
teamId: data.teamId,
@ -2154,10 +2088,9 @@ define([
postMessage("DISCONNECT");
}));
}));
}
}).nThen(function () {
// We have the new drive, with the new login block
var feedbackKey = (password === newPassword)?
var feedbackKey = (data.password === data.newPassword)?
'OWNED_DRIVE_MIGRATION': 'PASSWORD_CHANGED';
Feedback.send(feedbackKey, undefined, function () {

View File

@ -21,6 +21,7 @@ define([
'/common/outer/messenger.js',
'/common/outer/history.js',
'/common/outer/calendar.js',
'/common/outer/login-block.js',
'/common/outer/network-config.js',
'/customize/application_config.js',
@ -34,7 +35,7 @@ define([
], function (ApiConfig, Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Cache,
SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
Calendar, NetConfig, AppConfig,
Calendar, Block, NetConfig, AppConfig,
Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen, Saferphore) {
var onReadyEvt = Util.mkEvent(true);
@ -453,38 +454,6 @@ define([
});
};
Store.writeLoginBlock = function (clientId, data, cb) {
Pinpad.create(store.network, data && data.keys, function (err, rpc) {
if (err) {
return void cb({
error: err,
});
}
rpc.writeLoginBlock(data && data.content, function (e, res) {
cb({
error: e,
data: res
});
});
});
};
Store.removeLoginBlock = function (clientId, data, cb) {
Pinpad.create(store.network, data && data.keys, function (err, rpc) {
if (err) {
return void cb({
error: err,
});
}
rpc.removeLoginBlock(data && data.content, function (e, res) {
cb({
error: e,
data: res
});
});
});
};
var initRpc = function (clientId, data, cb) {
if (!store.loggedIn) { return cb(); }
if (store.rpc) { return void cb(account); }
@ -751,8 +720,9 @@ define([
});
};
var getOwnedPads = function () {
var list = store.manager.getChannelsList('owned');
var getOwnedPads = function (account) {
var list = [];
if (account) {
if (store.proxy.todo) {
// No password for todo
list.push(Hash.hrefToHexChannelId('/todo/#' + store.proxy.todo, null));
@ -768,6 +738,10 @@ define([
list.push(m.channel);
});
}
// XXX calendars
} else {
list = store.manager.getChannelsList('owned');
/*
if (store.proxy.teams) {
Object.keys(store.proxy.teams || {}).forEach(function (id) {
var t = store.proxy.teams[id];
@ -778,16 +752,30 @@ define([
}
});
}
*/
}
return list.filter(function (channel) {
if (typeof(channel) !== 'string') { return; }
return [32, 48].indexOf(channel.length) !== -1;
});
};
var removeOwnedPads = function (waitFor) {
var removeOwnedPads = function (account, waitFor) {
// Delete owned pads
var edPublic = Util.find(store, ['proxy', 'edPublic']);
var ownedPads = getOwnedPads();
var ownedPads = getOwnedPads(account);
var sem = Saferphore.create(10);
var deleteChannel = function (c) {
// When deleting the whole account, no need to remove from drive
if (account) { return; }
var all = store.manager.findChannel(c);
all.forEach(function (d) {
var p = store.manager.findFile(d.id);
store.manager.delete({paths:p});
});
};
ownedPads.forEach(function (c) {
var w = waitFor();
sem.take(function (give) {
@ -805,6 +793,14 @@ define([
return void _w.abort();
}
var md = obj[0];
if (!Object.keys(md || {}).length) {
deleteChannel(c);
give();
w();
return void _w.abort();
}
var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(edPublic) !== -1;
if (!isOwner) {
give();
@ -824,7 +820,8 @@ define([
}
// We're the only owner: delete the pad
store.rpc.removeOwnedChannel(c, _w(function (err) {
if (err) { console.error(err); }
if (err) { return void console.error(err); }
deleteChannel(c);
}));
}).nThen(function () {
give();
@ -834,10 +831,17 @@ define([
});
};
Store.removeOwnedPads = function (clientId, data, cb) {
var edPublic = store.proxy.edPublic;
if (!edPublic) { return void cb({ error: 'NOT_LOGGED_IN' }); }
nThen(function (waitFor) {
removeOwnedPads(false, waitFor);
}).nThen(cb);
};
Store.deleteAccount = function (clientId, data, cb) {
var edPublic = store.proxy.edPublic;
var removeData = data && data.removeData;
var rpcKeys = data && data.keys;
var blockKeys = data && data.keys;
var auth = data && data.auth;
Store.anonRpcMsg(clientId, {
msg: 'GET_METADATA',
data: store.driveChannel
@ -848,13 +852,22 @@ define([
metadata.owners.indexOf(edPublic) !== -1) {
var token;
nThen(function (waitFor) {
Block.checkRights({
auth: auth,
blockKeys: blockKeys,
}, waitFor(function (err) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({ error: 'INVALID_CODE' });
}
}));
}).nThen(function (waitFor) {
self.accountDeletion = clientId;
// Log out from other workers
var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);
store.proxy[Constants.tokenKey] = token;
onSync(null, waitFor());
}).nThen(function (waitFor) {
removeOwnedPads(waitFor);
}).nThen(function (waitFor) {
// Delete Pin Store
store.rpc.removePins(waitFor(function (err) {
@ -867,16 +880,15 @@ define([
force: true
}, waitFor());
}).nThen(function (waitFor) {
if (!removeData) { return; }
var done = waitFor();
Pinpad.create(store.network, rpcKeys, function (err, rpc) {
if (err) {
console.error(err);
return void done();
}
// Delete the block. Don't abort if it fails, it doesn't leak any data.
rpc.removeLoginBlock(removeData, done);
});
if (!blockKeys) { return; }
Block.removeLoginBlock({
auth: auth,
blockKeys: blockKeys,
}, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
removeOwnedPads(true, waitFor);
}).nThen(function () {
// Log out current worker
postMessage(clientId, "DELETE_ACCOUNT", token, function () {});
@ -2928,7 +2940,7 @@ define([
readOnly: false,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
Cache: Cache, // ICE drive cache
Cache: Cache,
userName: 'fs',
logLevel: 1,
ChainPad: ChainPad,

View File

@ -18,7 +18,9 @@ define([
body: JSON.stringify(data),
}).then(response => {
if (response.ok) {
return void response.json().then(result => { CB(void 0, result); });
return void response.text().then(result => { CB(void 0, Util.tryParse(result)); }); // XXX checkup error when using .json()
//return void response.json().then(result => { CB(void 0, result); });
}
response.json().then().then(result => {

View File

@ -1,8 +1,9 @@
define([
'/common/common-util.js',
'/api/config',
'/common/outer/http-command.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, ApiConfig) {
], function (Util, ApiConfig, ServerCommand) {
var Nacl = window.nacl;
var Block = {};
@ -113,17 +114,6 @@ define([
}
};
Block.remove = function (keys) {
// sign the hash of the text 'DELETE_BLOCK'
var sig = Nacl.sign.detached(Nacl.hash(
Nacl.util.decodeUTF8('DELETE_BLOCK')), keys.sign.secretKey);
return {
publicKey: Nacl.util.encodeBase64(keys.sign.publicKey),
signature: Nacl.util.encodeBase64(sig),
};
};
var urlSafeB64 = function (u8) {
return Nacl.util.encodeBase64(u8).replace(/\//g, '-');
};
@ -170,5 +160,50 @@ define([
}
};
Block.checkRights = function (data, _cb) {
const cb = Util.mkAsync(_cb);
const { blockKeys, auth } = data;
var command = 'MFA_CHECK';
if (auth && auth.type === 'TOTP') {
command = 'TOTP_CHECK';
}
ServerCommand(blockKeys.sign, {
command: command,
auth: auth && auth.data
}, cb);
};
Block.writeLoginBlock = function (data, cb) {
const { content, blockKeys, oldBlockKeys, auth } = data;
var command = 'WRITE_BLOCK';
if (auth && auth.type === 'TOTP') {
command = 'TOTP_WRITE_BLOCK';
}
var block = Block.serialize(JSON.stringify(content), blockKeys);
block.auth = auth && auth.data;
block.registrationProof = oldBlockKeys && Block.proveAncestor(oldBlockKeys);
ServerCommand(blockKeys.sign, {
command: command,
content: block
}, cb);
};
Block.removeLoginBlock = function (data, cb) {
const { blockKeys, auth } = data;
var command = 'REMOVE_BLOCK';
if (auth && auth.type === 'TOTP') {
command = 'TOTP_REMOVE_BLOCK';
}
ServerCommand(blockKeys.sign, {
command: command,
auth: auth && auth.data
}, cb);
};
return Block;
});

View File

@ -24,8 +24,6 @@ define([
UPLOAD_COMPLETE: Store.uploadComplete,
UPLOAD_STATUS: Store.uploadStatus,
UPLOAD_CANCEL: Store.uploadCancel,
WRITE_LOGIN_BLOCK: Store.writeLoginBlock,
REMOVE_LOGIN_BLOCK: Store.removeLoginBlock,
PIN_PADS: Store.pinPads,
UNPIN_PADS: Store.unpinPads,
GET_DELETED_PADS: Store.getDeletedPads,
@ -93,6 +91,7 @@ define([
DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings,
DELETE_ACCOUNT: Store.deleteAccount,
REMOVE_OWNED_PADS: Store.removeOwnedPads,
// Admin
ADMIN_RPC: Store.adminRpc,
ADMIN_ADD_MAILBOX: Store.addAdminMailbox,

View File

@ -205,41 +205,6 @@ var factory = function (Util, Rpc) {
});
};
exp.writeLoginBlock = function (data, cb) {
if (!data) { return void cb('NO_DATA'); }
if (!data.publicKey || !data.signature || !data.ciphertext) {
console.log(data);
return void cb("MISSING_PARAMETERS");
}
if (['string', 'undefined'].indexOf(typeof(data.registrationProof)) === -1) {
return void cb("INVALID_REGISTRATION_PROOF");
}
rpc.send('WRITE_LOGIN_BLOCK', [
data.publicKey,
data.signature,
data.ciphertext,
data.registrationProof || undefined,
], function (e) {
cb(e);
});
};
exp.removeLoginBlock = function (data, cb) {
if (!data) { return void cb('NO_DATA'); }
if (!data.publicKey || !data.signature) {
console.log(data);
return void cb("MISSING_PARAMETERS");
}
rpc.send('REMOVE_LOGIN_BLOCK', [
data.publicKey, // publicKey
data.signature, // signature
], function (e) {
cb(e);
});
};
// Get data for the admin panel
exp.setMetadata = function (obj, cb) {
rpc.send('SET_METADATA', {

View File

@ -1733,14 +1733,6 @@ define([
Cryptpad.changeUserPassword(Cryptget, edPublic, data, cb);
});
sframeChan.on('Q_WRITE_LOGIN_BLOCK', function (data, cb) {
Cryptpad.writeLoginBlock(data, cb);
});
sframeChan.on('Q_REMOVE_LOGIN_BLOCK', function (data, cb) {
Cryptpad.removeLoginBlock(data, cb);
});
// It seems we have performance issues when we open and close a lot of channels over
// the same network, maybe a memory leak. To fix this, we kill and create a new
// network every 30 cryptget calls (1 call = 1 channel)

View File

@ -32,19 +32,23 @@ define([
}).nThen(function (waitFor) {
SFCommonO.initIframe(waitFor);
}).nThen(function (/*waitFor*/) {
var hash = localStorage[Constants.userHashKey] || localStorage[Constants.fileHashKey];
var drive = hash && ('#'+hash === window.location.hash);
var isDrive = false;
var isMyDrive = false;
if (!window.location.hash) {
drive = true;
window.location.hash = hash;
isDrive = true;
isMyDrive = true;
} else {
var p = Hash.parsePadUrl('/debug/'+window.location.hash);
if (p && p.hashData && p.hashData.app === 'drive') {
drive = true;
isDrive = true;
}
}
var addData = function (meta) {
meta.debugDrive = drive;
var addData = function (meta, Cryptpad) {
if (isMyDrive) { window.location.hash = Cryptpad.userHash; }
window.CryptPad_location.app = "debug";
window.CryptPad_location.hash = Cryptpad.userHash;
window.CryptPad_location.href = '/debug/#'+Cryptpad.userHash;
meta.debugDrive = isDrive;
};
SFCommonO.start({
noDrive: true,

View File

@ -6,12 +6,10 @@ define([
'/common/common-realtime.js',
'/common/common-feedback.js',
'/common/outer/local-store.js',
'/common/hyperscript.js',
'/customize/messages.js',
//'/common/test.js',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore, h, Msg /*, Test */) {
], function ($, Cryptpad, Login, UI, Realtime, Feedback, LocalStore/*, Test */) {
if (window.top !== window) { return; }
$(function () {
var $checkImport = $('#import-recent');
@ -21,11 +19,6 @@ define([
return;
}
Msg.settings_totp_code = "OTP code"; // XXX KEY ALREADY ADDED IN www/settings/inner.js
Msg.login_enter_totp = "This account is protected with MFA. Please enter your OTP code."; // XXX
Msg.login_invalid_otp = "Invalid OTP code";
/* Log in UI */
// deferred execution to avoid unnecessary asset loading
var loginReady = function (cb) {
@ -51,47 +44,13 @@ define([
$('button.login').click();
});
var onOTP = function (err, cb) {
var btn, input;
var error;
if (err) {
console.error(err);
error = h('p.cp-password-error', Msg.login_invalid_otp);
}
var block = h('div#cp-loading-password-prompt', [
error,
h('p.cp-password-info', Msg.login_enter_totp),
h('p.cp-password-form', [
input = h('input', {
placeholder: Msg.settings_totp_code,
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'off',
spellcheck: false,
}),
btn = h('button.btn.btn-primary', Msg.ui_confirm)
])
]);
var $input = $(input);
var $btn = $(btn).click(function () {
var val = $input.val();
if (!val) { return void onOTP('INVALID_CODE', cb); }
cb(val);
});
$(input).on('keydown', function (e) {
if (e.which !== 13) { return; } // enter
$btn.click();
});
UI.errorLoadingScreen(block, false, false);
};
//var test;
$('button.login').click(function () {
var shouldImport = $checkImport[0].checked;
var uname = $uname.val();
var passwd = $passwd.val();
Login.loginOrRegisterUI(uname, passwd, false, shouldImport, onOTP, /*Test.testing */ false, function () {
Login.loginOrRegisterUI(uname, passwd, false, shouldImport,
UI.getOTPScreen, /*Test.testing */ false, function () {
/*
if (test) {
localStorage.clear();

View File

@ -54,8 +54,8 @@ define([
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_otp_code = "OTP 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
@ -63,6 +63,9 @@ define([
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_removeOwnedButton = "Destroy documents";
Messages.settings_removeOwnedText = "Please wait while your document are being destroyed...";
var categories = {
'account': [ // Msg.settings_cat_account
'cp-settings-own-drive',
@ -70,12 +73,12 @@ define([
'cp-settings-displayname',
'cp-settings-language-selector',
'cp-settings-mediatag-size',
'cp-settings-change-password',
'cp-settings-delete'
],
'access': [ // Msg.settings_cat_access // XXX
// XXX add password change and account deletion here?
'cp-settings-totp'
'cp-settings-totp',
'cp-settings-remove-owned',
'cp-settings-change-password',
'cp-settings-delete'
],
'security': [ // Msg.settings_cat_security
'cp-settings-logout-everywhere',
@ -497,6 +500,40 @@ define([
});
}, true);
var deriveBytes = function (name, password, cb) {
Cred.deriveFromPassphrase(name, password, Login.requiredBytes, cb);
};
makeBlock('remove-owned', function(cb) { // Msg.settings_removeOwnedHint, .settings_removeOwnedTitle
if (!common.isLoggedIn()) { return cb(false); }
var button = h('button.btn.btn-danger', Messages.settings_removeOwnedButton);
var form = h('div', [
button
]);
var $button = $(button);
UI.confirmButton(button, {
classes: 'btn-danger',
multiple: true
}, function() {
UI.addLoadingScreen({
hideTips: true,
loadingText: Messages.settings_removeOwnedText
});
sframeChan.query("Q_SETTINGS_REMOVE_OWNED_PADS", {}, function (err, data) {
UI.removeLoadingScreen();
$button.prop('disabled', '');
if (data && data.error) {
console.error(data.error);
return void UI.warn(Messages.error);
}
UI.log(Messages.success);
});
});
cb(form);
}, true);
makeBlock('delete', function(cb) { // Msg.settings_deleteHint, .settings_deleteTitle
if (!common.isLoggedIn()) { return cb(false); }
@ -510,7 +547,6 @@ define([
]);
var $form = $(form);
var $button = $(button);
var spinner = UI.makeSpinner($form);
UI.confirmButton(button, {
classes: 'btn-danger',
@ -548,17 +584,62 @@ define([
if (!password) {
return void UI.warn(Messages.error);
}
spinner.spin();
UI.addLoadingScreen({
hideTips: true,
loadingText: Messages.settings_deleteTitle
});
setTimeout(function () {
var name = privateData.accountName;
var bytes;
var auth = {};
nThen(function (w) {
deriveBytes(name, password, w(function (_bytes) {
bytes = _bytes;
}));
}).nThen(function (w) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
userHash: result.userHash,
}, w(function (err, obj) {
if (!obj || !obj.correct) {
UI.warn(Messages.login_noSuchUser);
w.abort();
UI.removeLoadingScreen();
}
}));
}).nThen(function (w) {
// CHECK MFA
sframeChan.query('Q_SETTINGS_MFA_CHECK', {}, w(function (err, obj) {
// No block? no need for a code
if (err || !obj || (obj && obj.err === 'NOBLOCK')
|| !obj.mfa) { return; }
auth.type = obj.type;
if (auth.type === 'TOTP') {
UI.getOTPScreen(w(function (val) {
UI.addLoadingScreen({ loadingText: Messages.settings_deleteTitle });
auth.data = val;
}), function () {
w.abort(); // On exit OTP screen
});
}
}));
}).nThen(function () {
sframeChan.query("Q_SETTINGS_DELETE_ACCOUNT", {
password: password
bytes: bytes,
auth: auth
}, function(err, data) {
UI.removeLoadingScreen();
if (data && data.error) {
spinner.hide();
$button.prop('disabled', '');
if (data.error === 'INVALID_PASSWORD') {
return void UI.warn(Messages.drive_sfPasswordError);
}
console.error(data.error);
if (data.error === 'INVALID_CODE') {
return void UI.warn(Messages.settings_otp_invalid);
}
return void UI.warn(Messages.error);
}
// Owned drive
@ -567,7 +648,6 @@ define([
UI.alert(Messages.settings_deleted, function() {
common.gotoURL('/');
});
spinner.done();
});
}
// Not owned drive
@ -576,11 +656,12 @@ define([
h('pre', JSON.stringify(data, 0, 2))
]);
UI.alert(msg);
spinner.hide();
$button.prop('disabled', '');
});
});
});
});
});
cb(form);
}, true);
@ -596,7 +677,6 @@ define([
.append(Messages.settings_changePasswordHint).appendTo($div);
// var publicKey = privateData.edPublic;
var form = h('div', [
UI.passwordInput({
id: 'cp-settings-change-password-current',
@ -621,7 +701,7 @@ define([
sframeChan.query('Q_CHANGE_USER_PASSWORD', data, function(err, obj) {
if (err || obj.error) { return void cb({ error: err || obj.error }); }
cb(obj);
});
}, {raw: true});
};
var todo = function() {
@ -650,20 +730,70 @@ define([
function(yes) {
if (!yes) { return; }
UI.addLoadingScreen({
hideTips: true,
loadingText: Messages.settings_changePasswordPending,
UI.addLoadingScreen({ loadingText: Messages.settings_changePasswordPending });
// We're going to derive the bytes in inner in order to ask for the possible
// OTP code after the Scrypt execution. This will make it less likely to
// have the OTP code expire.
setTimeout(function () {
var oldBytes, newBytes;
var auth = {};
nThen(function (w) {
var name = privateData.accountName;
deriveBytes(name, oldPassword, w(function (bytes) {
oldBytes = bytes;
}));
deriveBytes(name, newPassword, w(function (bytes) {
newBytes = bytes;
}));
}).nThen(function (w) {
var result = Login.allocateBytes(oldBytes);
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
userHash: result.userHash,
}, w(function (err, obj) {
if (!obj || !obj.correct) {
UI.warn(Messages.login_noSuchUser);
w.abort();
UI.removeLoadingScreen();
}
}));
}).nThen(function (w) {
// CHECK MFA
sframeChan.query('Q_SETTINGS_MFA_CHECK', {}, w(function (err, obj) {
// No block? no need for a code
if (err || !obj || (obj && obj.err === 'NOBLOCK')
|| !obj.mfa) { return; }
auth.type = obj.type;
if (auth.type === 'TOTP') {
UI.getOTPScreen(w(function (val) {
auth.data = val;
UI.addLoadingScreen({ loadingText: Messages.settings_changePasswordPending });
}), function () {
w.abort(); // On exit OTP screen
});
}
}));
}).nThen(function () {
updateBlock({
password: oldPassword,
newPassword: newPassword
newPassword: newPassword,
oldBytes: oldBytes,
newBytes: newBytes,
auth: auth
}, function(obj) {
UI.removeLoadingScreen();
if (obj && obj.error) {
if (obj.error === 'INVALID_CODE') {
return void UI.warn(Messages.settings_otp_invalid);
}
// TODO more specific error message?
console.error(obj.error);
UI.alert(Messages.settings_changePasswordError);
}
});
});
});
}, {
ok: Messages.register_writtenPassword,
cancel: Messages.register_cancel,
@ -812,7 +942,7 @@ define([
placeholder: Messages.login_password,
})),
OTPEntry = h('input', {
placeholder: Messages.settings_totp_code
placeholder: Messages.settings_otp_code
}),
disable
]));
@ -831,7 +961,7 @@ define([
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, password, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_BLOCK", {
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
}, function (err, obj) {
if (!obj || !obj.correct) {
@ -851,7 +981,7 @@ define([
$OTPEntry.val("");
if (err || !obj || !obj.success) {
$b.removeAttr('disabled');
return void UI.warn(Messages.settings_totp_code_invalid);
return void UI.warn(Messages.settings_otp_invalid);
}
drawTotp(content, false);
}, {raw: true});
@ -910,7 +1040,7 @@ define([
setTimeout(function () {
Login.Cred.deriveFromPassphrase(name, password, Login.requiredBytes, function (bytes) {
var result = Login.allocateBytes(bytes);
sframeChan.query("Q_SETTINGS_CHECK_BLOCK", {
sframeChan.query("Q_SETTINGS_CHECK_PASSWORD", {
blockHash: result.blockHash,
}, function (err, obj) {
BUSY = false;
@ -956,7 +1086,7 @@ define([
updateQR(uri, qr);
var OTPEntry = h('input', {
placeholder: Messages.settings_totp_code
placeholder: Messages.settings_otp_code
});
var $OTPEntry = $(OTPEntry);
@ -1028,9 +1158,9 @@ define([
if (!common.isLoggedIn()) { return void cb(false); }
var content = h('div');
sframeChan.query('Q_SETTINGS_TOTP_CHECK', {}, function (err, obj) {
sframeChan.query('Q_SETTINGS_MFA_CHECK', {}, function (err, obj) {
if (err || !obj || (obj && obj.err === 'NOBLOCK')) { return void cb(false); }
var enabled = obj && obj.totp;
var enabled = obj && obj.mfa && obj.type === 'TOTP';
drawTotp(content, Boolean(enabled));
cb(content);
});

View File

@ -70,8 +70,12 @@ define([
sframeChan.on('Q_SETTINGS_IMPORT_LOCAL', function (data, cb) {
Cryptpad.mergeAnonDrive(cb);
});
sframeChan.on('Q_SETTINGS_CHECK_BLOCK', function (data, cb) {
cb({correct: data.blockHash === Utils.LocalStore.getBlockHash() });
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([
@ -97,18 +101,24 @@ define([
});
});
});
sframeChan.on('Q_SETTINGS_TOTP_CHECK', function (obj, cb) {
sframeChan.on('Q_SETTINGS_MFA_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});
Utils.Util.getBlock(parsed.href, {}, function (err, data) {
cb({
mfa: err === 401,
type: data && data.method
});
});
});
});
sframeChan.on('Q_SETTINGS_REMOVE_OWNED_PADS', function (data, cb) {
Cryptpad.removeOwnedPads(data, cb);
});
sframeChan.on('Q_SETTINGS_DELETE_ACCOUNT', function (data, cb) {
Cryptpad.deleteAccount(data, cb);
});