mirror of https://github.com/xwiki-labs/cryptpad
TOTP: Use HTTP challenges to write and remove blocks
This commit is contained in:
parent
b3a620edc0
commit
9aac9d1c2f
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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, '-');
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue