mirror of https://github.com/xwiki-labs/cryptpad
Merge branch 'modern-users' into staging
This commit is contained in:
commit
57d0738f32
|
@ -14,6 +14,7 @@ data
|
|||
npm-debug.log
|
||||
pins/
|
||||
blob/
|
||||
block/
|
||||
blobstage/
|
||||
block/
|
||||
privileged.conf
|
||||
|
|
|
@ -211,6 +211,11 @@ module.exports = {
|
|||
*/
|
||||
taskPath: './tasks',
|
||||
|
||||
/* if you would like users' authenticated blocks to be stored in
|
||||
a custom location, change the path below:
|
||||
*/
|
||||
blockPath: './block',
|
||||
|
||||
/*
|
||||
* By default, CryptPad also contacts our accounts server once a day to check for changes in
|
||||
* the people who have accounts. This check-in will also send the version of your CryptPad
|
||||
|
|
|
@ -12,17 +12,22 @@ define([
|
|||
'/common/common-feedback.js',
|
||||
'/common/outer/local-store.js',
|
||||
'/customize/messages.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/common/outer/login-block.js',
|
||||
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
'/bower_components/scrypt-async/scrypt-async.min.js', // better load speed
|
||||
], function ($, Listmap, Crypto, Util, NetConfig, Cred, ChainPad, Realtime, Constants, UI,
|
||||
Feedback, LocalStore, Messages) {
|
||||
Feedback, LocalStore, Messages, nThen, Block) {
|
||||
var Exports = {
|
||||
Cred: Cred,
|
||||
// this is depended on by non-customizable files
|
||||
// be careful when modifying login.js
|
||||
requiredBytes: 192,
|
||||
};
|
||||
|
||||
var Nacl = window.nacl;
|
||||
var allocateBytes = function (bytes) {
|
||||
var allocateBytes = Exports.allocateBytes = function (bytes) {
|
||||
var dispense = Cred.dispenser(bytes);
|
||||
|
||||
var opt = {};
|
||||
|
@ -41,6 +46,12 @@ define([
|
|||
// 32 more for a signing key
|
||||
var edSeed = opt.edSeed = dispense(32);
|
||||
|
||||
// 64 more bytes to seed an additional signing key
|
||||
opt.blockSeed = new Uint8Array(dispense(64));
|
||||
|
||||
var blockKeys = opt.blockKeys = Block.genkeys(opt.blockSeed);
|
||||
opt.blockHash = Block.getBlockHash(blockKeys);
|
||||
|
||||
// derive a private key from the ed seed
|
||||
var signingKeypair = Nacl.sign.keyPair.fromSeed(new Uint8Array(edSeed));
|
||||
|
||||
|
@ -105,18 +116,32 @@ define([
|
|||
return void cb('PASS_TOO_SHORT');
|
||||
}
|
||||
|
||||
Cred.deriveFromPassphrase(uname, passwd, 128, function (bytes) {
|
||||
// results...
|
||||
var res = {
|
||||
register: isRegister,
|
||||
};
|
||||
// results...
|
||||
var res = {
|
||||
register: isRegister,
|
||||
};
|
||||
|
||||
// run scrypt to derive the user's keys
|
||||
var opt = res.opt = allocateBytes(bytes);
|
||||
var RT;
|
||||
|
||||
nThen(function (waitFor) {
|
||||
Cred.deriveFromPassphrase(uname, passwd, Exports.requiredBytes, waitFor(function (bytes) {
|
||||
// run scrypt to derive the user's keys
|
||||
res.opt = allocateBytes(bytes);
|
||||
}));
|
||||
|
||||
|
||||
// TODO consider checking the block here
|
||||
}).nThen(function (/* waitFor */) {
|
||||
// check for blocks
|
||||
Block = Block; // jshint
|
||||
|
||||
|
||||
}).nThen(function (waitFor) {
|
||||
var opt = res.opt;
|
||||
// use the derived key to generate an object
|
||||
loadUserObject(opt, function (err, rt) {
|
||||
loadUserObject(opt, waitFor(function (err, rt) {
|
||||
if (err) { return void cb(err); }
|
||||
RT = rt;
|
||||
|
||||
res.proxy = rt.proxy;
|
||||
res.realtime = rt.realtime;
|
||||
|
@ -136,12 +161,14 @@ define([
|
|||
// they tried to just log in but there's no such user
|
||||
if (!isRegister && isProxyEmpty(rt.proxy)) {
|
||||
rt.network.disconnect(); // clean up after yourself
|
||||
waitFor.abort();
|
||||
return void cb('NO_SUCH_USER', res);
|
||||
}
|
||||
|
||||
// they tried to register, but those exact credentials exist
|
||||
if (isRegister && !isProxyEmpty(rt.proxy)) {
|
||||
rt.network.disconnect();
|
||||
waitFor.abort();
|
||||
return void cb('ALREADY_REGISTERED', res);
|
||||
}
|
||||
|
||||
|
@ -163,17 +190,17 @@ define([
|
|||
if (shouldImport) {
|
||||
sessionStorage.migrateAnonDrive = 1;
|
||||
}
|
||||
|
||||
// We have to call whenRealtimeSyncs asynchronously here because in the current
|
||||
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
|
||||
// asynchronously.
|
||||
// The following setTimeout is here to make sure whenRealtimeSyncs is called after
|
||||
// `contentUpdate` so that we have an update userDoc in chainpad.
|
||||
setTimeout(function () {
|
||||
Realtime.whenRealtimeSyncs(rt.realtime, function () {
|
||||
LocalStore.login(res.userHash, res.userName, function () {
|
||||
setTimeout(function () { cb(void 0, res); });
|
||||
});
|
||||
}));
|
||||
}).nThen(function () {
|
||||
// We have to call whenRealtimeSyncs asynchronously here because in the current
|
||||
// version of listmap, onLocal calls `chainpad.contentUpdate(newValue)`
|
||||
// asynchronously.
|
||||
// The following setTimeout is here to make sure whenRealtimeSyncs is called after
|
||||
// `contentUpdate` so that we have an update userDoc in chainpad.
|
||||
setTimeout(function () {
|
||||
Realtime.whenRealtimeSyncs(RT.realtime, function () {
|
||||
LocalStore.login(res.userHash, res.userName, function () {
|
||||
setTimeout(function () { cb(void 0, res); });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -600,6 +600,11 @@ define(function () {
|
|||
out.settings_templateSkip = "Skip the template selection modal";
|
||||
out.settings_templateSkipHint = "When you create a new empty pad, if you have stored templates for this type of pad, a modal appears to ask if you want to use a template. Here you can choose to never show this modal and so to never use a template.";
|
||||
|
||||
out.settings_ownDriveTitle = "Drive migration"; // XXX
|
||||
out.settings_ownDriveHint = "Migrating your drive to the new version will give you access to new features..."; // XXX
|
||||
out.settings_ownDriveButton = "Migrate"; // XXX
|
||||
out.settings_ownDriveConfirm = "Are you sure?"; // XXX
|
||||
|
||||
out.settings_changePasswordTitle = "Change your password"; // XXX
|
||||
out.settings_changePasswordHint = "Change your account's password without losing its data. You have to enter your existing password once, and the new password you want twice.<br>" +
|
||||
"<b>We can't reset your password if you forget it so be very careful!</b>"; // XXX
|
||||
|
@ -608,6 +613,7 @@ define(function () {
|
|||
out.settings_changePasswordNew = "New password"; // XXX
|
||||
out.settings_changePasswordNewConfirm = "Confirm new password"; // XXX
|
||||
out.settings_changePasswordConfirm = "Are you sure?"; // XXX
|
||||
out.settings_changePasswordError = "Error {0}"; // XXX
|
||||
|
||||
out.upload_title = "File upload";
|
||||
out.upload_modal_title = "File upload options";
|
||||
|
|
173
rpc.js
173
rpc.js
|
@ -1297,6 +1297,160 @@ var upload_status = function (Env, publicKey, filesize, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
/*
|
||||
We assume that the server is secured against MitM attacks
|
||||
via HTTPS, and that malicious actors do not have code execution
|
||||
capabilities. If they do, we have much more serious problems.
|
||||
|
||||
The capability to replay a block write or remove results in either
|
||||
a denial of service for the user whose block was removed, or in the
|
||||
case of a write, a rollback to an earlier password.
|
||||
|
||||
Since block modification is destructive, this can result in loss
|
||||
of access to the user's drive.
|
||||
|
||||
So long as the detached signature is never observed by a malicious
|
||||
party, and the server discards it after proof of knowledge, replays
|
||||
are not possible. However, this precludes verification of the signature
|
||||
at a later time.
|
||||
|
||||
Despite this, an integrity check is still possible by the original
|
||||
author of the block, since we assume that the block will have been
|
||||
encrypted with xsalsa20-poly1305 which is authenticated.
|
||||
*/
|
||||
var validateLoginBlock = function (Env, publicKey, signature, block, cb) {
|
||||
// convert the public key to a Uint8Array and validate it
|
||||
if (typeof(publicKey) !== 'string') { return void cb('E_INVALID_KEY'); }
|
||||
|
||||
var u8_public_key;
|
||||
try {
|
||||
u8_public_key = Nacl.util.decodeBase64(publicKey);
|
||||
} catch (e) {
|
||||
return void cb('E_INVALID_KEY');
|
||||
}
|
||||
|
||||
var u8_signature;
|
||||
try {
|
||||
u8_signature = Nacl.util.decodeBase64(signature);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return void cb('E_INVALID_SIGNATURE');
|
||||
}
|
||||
|
||||
// convert the block to a Uint8Array
|
||||
var u8_block;
|
||||
try {
|
||||
u8_block = Nacl.util.decodeBase64(block);
|
||||
} catch (e) {
|
||||
return void cb('E_INVALID_BLOCK');
|
||||
}
|
||||
|
||||
// take its hash
|
||||
var hash = Nacl.hash(u8_block);
|
||||
|
||||
// validate the signature against the hash of the content
|
||||
var verified = Nacl.sign.detached.verify(hash, u8_signature, u8_public_key);
|
||||
|
||||
// existing authentication ensures that users cannot replay old blocks
|
||||
|
||||
// call back with (err) if unsuccessful
|
||||
if (!verified) { return void cb("E_COULD_NOT_VERIFY"); }
|
||||
|
||||
return void cb(null, u8_block);
|
||||
|
||||
// signature 64 bytes
|
||||
// sign.detached(hash(decodeBase64_content(base64_content)), decodeBase64(publicKey))
|
||||
|
||||
// 1 byte version
|
||||
// base64_content
|
||||
};
|
||||
|
||||
var createLoginBlockPath = function (Env, publicKey) {
|
||||
// prepare publicKey to be used as a file name
|
||||
var safeKey = escapeKeyCharacters(publicKey);
|
||||
|
||||
// validate safeKey
|
||||
if (typeof(safeKey) !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// derive the full path
|
||||
// /home/cryptpad/cryptpad/block/fg/fg32kefksjdgjkewrjksdfksjdfsdfskdjfsfd
|
||||
return Path.join(Env.paths.block, safeKey.slice(0, 2), safeKey);
|
||||
};
|
||||
|
||||
var writeLoginBlock = function (Env, msg, cb) {
|
||||
//console.log(msg);
|
||||
var publicKey = msg[0];
|
||||
var signature = msg[1];
|
||||
var block = msg[2];
|
||||
|
||||
validateLoginBlock(Env, publicKey, signature, block, function (e, verified_block) {
|
||||
if (e) { return void cb(e); }
|
||||
|
||||
// derive the filepath
|
||||
var path = createLoginBlockPath(Env, publicKey);
|
||||
|
||||
// make sure the path is valid
|
||||
if (typeof(path) !== 'string') {
|
||||
return void cb('E_INVALID_BLOCK_PATH');
|
||||
}
|
||||
|
||||
var parsed = Path.parse(path);
|
||||
if (!parsed || typeof(parsed.dir) !== 'string') {
|
||||
return void cb("E_INVALID_BLOCK_PATH_2");
|
||||
}
|
||||
|
||||
nThen(function (w) {
|
||||
// make sure the path to the file exists
|
||||
Mkdirp(parsed.dir, w(function (e) {
|
||||
if (e) {
|
||||
w.abort();
|
||||
cb(e);
|
||||
}
|
||||
}));
|
||||
}).nThen(function () {
|
||||
// actually write the block
|
||||
Fs.writeFile(path, new Buffer(verified_block), { encoding: "binary", }, function (err) {
|
||||
if (err) { return void cb(err); }
|
||||
cb();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
When users write a block, they upload the block, and provide
|
||||
a signature proving that they deserve to be able to write to
|
||||
the location determined by the public key.
|
||||
|
||||
When removing a block, there is nothing to upload, but we need
|
||||
to sign something. Since the signature is considered sensitive
|
||||
information, we can just sign some constant and use that as proof.
|
||||
|
||||
*/
|
||||
var removeLoginBlock = function (Env, msg, cb) {
|
||||
var publicKey = msg[0];
|
||||
var signature = msg[1];
|
||||
var block = Nacl.util.decodeUTF8('DELETE_BLOCK'); // clients and the server will have to agree on this constant
|
||||
|
||||
validateLoginBlock(Env, publicKey, signature, block, function (e) {
|
||||
if (e) { return void cb(e); }
|
||||
// derive the filepath
|
||||
var path = createLoginBlockPath(Env, publicKey);
|
||||
|
||||
// make sure the path is valid
|
||||
if (typeof(path) !== 'string') {
|
||||
return void cb('E_INVALID_BLOCK_PATH');
|
||||
}
|
||||
|
||||
Fs.unlink(path, function (err) {
|
||||
if (err) { return void cb(err); }
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var isNewChannel = function (Env, channel, cb) {
|
||||
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
|
||||
if (channel.length !== 32) { return void cb('INVALID_CHAN'); }
|
||||
|
@ -1353,6 +1507,8 @@ var isAuthenticatedCall = function (call) {
|
|||
'CLEAR_OWNED_CHANNEL',
|
||||
'REMOVE_OWNED_CHANNEL',
|
||||
'REMOVE_PINS',
|
||||
'WRITE_LOGIN_BLOCK',
|
||||
'REMOVE_LOGIN_BLOCK',
|
||||
].indexOf(call) !== -1;
|
||||
};
|
||||
|
||||
|
@ -1423,6 +1579,7 @@ RPC.create = function (
|
|||
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
|
||||
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
|
||||
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
|
||||
paths.block = keyOrDefaultString('blockPath', './block');
|
||||
|
||||
var isUnauthenticateMessage = function (msg) {
|
||||
return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]);
|
||||
|
@ -1692,6 +1849,22 @@ RPC.create = function (
|
|||
WARN(e, 'UPLOAD_CANCEL');
|
||||
Respond(e);
|
||||
});
|
||||
case 'WRITE_LOGIN_BLOCK':
|
||||
return void writeLoginBlock(Env, msg[1], function (e) {
|
||||
if (e) {
|
||||
WARN(e, 'WRITE_LOGIN_BLOCK');
|
||||
return void Respond(e);
|
||||
}
|
||||
Respond(e);
|
||||
});
|
||||
case 'REMOVE_LOGIN_BLOCK':
|
||||
return void removeLoginBlock(Env, msg[1], function (e) {
|
||||
if (e) {
|
||||
WARN(e, 'REMOVE_LOGIN_BLOCK');
|
||||
return void Respond(e);
|
||||
}
|
||||
Respond(e);
|
||||
});
|
||||
default:
|
||||
return void Respond('UNSUPPORTED_RPC_CALL', msg);
|
||||
}
|
||||
|
|
|
@ -126,6 +126,9 @@ app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob
|
|||
app.use("/datastore", Express.static(Path.join(__dirname, (config.filePath || './datastore')), {
|
||||
maxAge: "0d"
|
||||
}));
|
||||
app.use("/block", Express.static(Path.join(__dirname, (config.blockPath || '/block')), {
|
||||
maxAge: "0d",
|
||||
}));
|
||||
|
||||
app.use("/customize", Express.static(__dirname + '/customize'));
|
||||
app.use("/customize", Express.static(__dirname + '/customize.dist'));
|
||||
|
|
|
@ -10,9 +10,13 @@ define([
|
|||
'/common/wire.js',
|
||||
'/common/flat-dom.js',
|
||||
'/common/media-tag.js',
|
||||
], function ($, Hyperjson, Sortify, Drive, Test, Hash, Util, Thumb, Wire, Flat, MediaTag) {
|
||||
'/common/outer/login-block.js',
|
||||
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function ($, Hyperjson, Sortify, Drive, Test, Hash, Util, Thumb, Wire, Flat, MediaTag, Block) {
|
||||
window.Hyperjson = Hyperjson;
|
||||
window.Sortify = Sortify;
|
||||
var Nacl = window.nacl;
|
||||
|
||||
var assertions = 0;
|
||||
var failed = false;
|
||||
|
@ -296,6 +300,15 @@ define([
|
|||
!secret.hashData.present);
|
||||
}, "test support for ugly tracking query paramaters in url");
|
||||
|
||||
assert(function (cb) {
|
||||
var keys = Block.genkeys(Nacl.randomBytes(64));
|
||||
var hash = Block.getBlockHash(keys);
|
||||
var parsed = Block.parseBlockHash(hash);
|
||||
|
||||
cb(parsed &&
|
||||
parsed.keys.symmetric.length === keys.symmetric.length);
|
||||
}, 'parse a block hash');
|
||||
|
||||
assert(function (cb) {
|
||||
try {
|
||||
MediaTag(void 0).on('progress').on('decryption');
|
||||
|
|
|
@ -3,6 +3,7 @@ define(function () {
|
|||
// localStorage
|
||||
userHashKey: 'User_hash',
|
||||
userNameKey: 'User_name',
|
||||
blockHashKey: 'Block_hash',
|
||||
fileHashKey: 'FS_hash',
|
||||
// sessionStorage
|
||||
newPadPathKey: "newPadPath",
|
||||
|
@ -11,6 +12,7 @@ define(function () {
|
|||
oldStorageKey: 'CryptPad_RECENTPADS',
|
||||
storageKey: 'filesData',
|
||||
tokenKey: 'loginToken',
|
||||
displayPadCreationScreen: 'displayPadCreationScreen'
|
||||
displayPadCreationScreen: 'displayPadCreationScreen',
|
||||
deprecatedKey: 'deprecated'
|
||||
};
|
||||
});
|
||||
|
|
|
@ -83,6 +83,21 @@ define([], function () {
|
|||
}).join('');
|
||||
};
|
||||
|
||||
// given an array of Uint8Arrays, return a new Array with all their values
|
||||
Util.uint8ArrayJoin = function (AA) {
|
||||
var l = 0;
|
||||
var i = 0;
|
||||
for (; i < AA.length; i++) { l += AA[i].length; }
|
||||
var C = new Uint8Array(l);
|
||||
|
||||
i = 0;
|
||||
for (var offset = 0; i < AA.length; i++) {
|
||||
C.set(AA[i], offset);
|
||||
offset += AA[i].length;
|
||||
}
|
||||
return C;
|
||||
};
|
||||
|
||||
Util.deduplicateString = function (array) {
|
||||
var a = array.slice();
|
||||
for(var i=0; i<a.length; i++) {
|
||||
|
@ -122,17 +137,14 @@ define([], function () {
|
|||
else if (bytes >= oneMegabyte) { return 'MB'; }
|
||||
};
|
||||
|
||||
// given a path, asynchronously return an arraybuffer
|
||||
Util.fetch = function (src, cb) {
|
||||
var done = false;
|
||||
var CB = function (err, res) {
|
||||
if (done) { return; }
|
||||
done = true;
|
||||
cb(err, res);
|
||||
};
|
||||
var CB = Util.once(cb);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", src, true);
|
||||
xhr.responseType = "arraybuffer";
|
||||
xhr.onerror = function (err) { CB(err); };
|
||||
xhr.onload = function () {
|
||||
if (/^4/.test(''+this.status)) {
|
||||
return CB('XHR_ERROR');
|
||||
|
|
|
@ -8,11 +8,12 @@ define([
|
|||
'/common/common-feedback.js',
|
||||
'/common/outer/local-store.js',
|
||||
'/common/outer/worker-channel.js',
|
||||
'/common/outer/login-block.js',
|
||||
|
||||
'/customize/application_config.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
], function (Config, Messages, Util, Hash,
|
||||
Messaging, Constants, Feedback, LocalStore, Channel,
|
||||
Messaging, Constants, Feedback, LocalStore, Channel, Block,
|
||||
AppConfig, Nthen) {
|
||||
|
||||
/* This file exposes functionality which is specific to Cryptpad, but not to
|
||||
|
@ -240,6 +241,12 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
common.removeLoginBlock = function (data, cb) {
|
||||
postMessage('REMOVE_LOGIN_BLOCK', data, function (obj) {
|
||||
cb(obj);
|
||||
});
|
||||
};
|
||||
|
||||
// ANON RPC
|
||||
|
||||
// SFRAME: talk to anon_rpc from the iframe
|
||||
|
@ -692,6 +699,157 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
common.changeUserPassword = function (Crypt, edPublic, data, cb) {
|
||||
if (!edPublic) {
|
||||
return void cb({
|
||||
error: 'E_NOT_LOGGED_IN'
|
||||
});
|
||||
}
|
||||
var accountName = LocalStore.getAccountName();
|
||||
var hash = LocalStore.getUserHash(); // To load your old drive
|
||||
var password = data.password; // To remove your old block
|
||||
var newPassword = data.newPassword; // To create your new block
|
||||
var secret = Hash.getSecrets('drive', hash);
|
||||
var newHash, newHref, newSecret, newBlockSeed;
|
||||
var oldIsOwned = false;
|
||||
|
||||
var blockHash = LocalStore.getBlockHash();
|
||||
var oldBlockKeys;
|
||||
|
||||
var Cred, Block, Login;
|
||||
Nthen(function (waitFor) {
|
||||
require([
|
||||
'/customize/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);
|
||||
oldBlockKeys = allocated.blockKeys;
|
||||
if (blockHash) {
|
||||
if (blockHash !== allocated.blockHash) {
|
||||
// incorrect password probably
|
||||
waitFor.abort();
|
||||
return void cb({
|
||||
error: 'INVALID_PASSWORD',
|
||||
});
|
||||
}
|
||||
// the user has already created a block, so you should compare against that
|
||||
} else {
|
||||
// otherwise they're a legacy user, and we should check against the User_hash
|
||||
if (hash !== allocated.userHash) {
|
||||
waitFor.abort();
|
||||
return void cb({
|
||||
error: 'INVALID_PASSWORD',
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// Check if our drive is already owned
|
||||
common.anonRpcMsg('GET_METADATA', secret.channel, waitFor(function (err, obj) {
|
||||
if (err || obj.error) { return; }
|
||||
if (obj.owners && Array.isArray(obj.owners) &&
|
||||
obj.owners.indexOf(edPublic) !== -1) {
|
||||
oldIsOwned = true;
|
||||
}
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// Create a new user hash
|
||||
// Get the current content, store it in the new user file
|
||||
// and make sure the new user drive is owned
|
||||
newHash = Hash.createRandomHash('drive');
|
||||
newHref = '/drive/#' + newHash;
|
||||
newSecret = Hash.getSecrets('drive', newHash);
|
||||
|
||||
var optsPut = {
|
||||
owners: [edPublic]
|
||||
};
|
||||
|
||||
Crypt.get(hash, waitFor(function (err, val) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb({ error: err });
|
||||
}
|
||||
Crypt.put(newHash, val, waitFor(function (err) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb({ error: err });
|
||||
}
|
||||
}), optsPut);
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// Drive content copied: get the new block location
|
||||
Cred.deriveFromPassphrase(accountName, newPassword, Login.requiredBytes, waitFor(function (bytes) {
|
||||
var allocated = Login.allocateBytes(bytes);
|
||||
newBlockSeed = allocated.blockSeed;
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// Write the new login block
|
||||
var keys = Block.genkeys(newBlockSeed);
|
||||
var content = Block.serialize(JSON.stringify({
|
||||
User_name: accountName,
|
||||
User_hash: newHash
|
||||
}), keys);
|
||||
common.writeLoginBlock(content, waitFor(function (obj) {
|
||||
var newBlockHash = Block.getBlockHash(keys);
|
||||
LocalStore.setBlockHash(newBlockHash);
|
||||
if (obj && obj.error) {
|
||||
waitFor.abort();
|
||||
return void cb(obj);
|
||||
}
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
// New drive hash is in login block, unpin the old one and pin the new one
|
||||
common.unpinPads([secret.channel], waitFor());
|
||||
common.pinPads([newSecret.channel], waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
// Remove block hash
|
||||
if (blockHash) {
|
||||
var removeData = Block.remove(oldBlockKeys);
|
||||
common.removeLoginBlock(removeData, waitFor(function (obj) {
|
||||
if (obj && obj.error) { return void console.error(obj.error); }
|
||||
}));
|
||||
}
|
||||
}).nThen(function (waitFor) {
|
||||
if (oldIsOwned) {
|
||||
common.removeOwnedChannel(secret.channel, waitFor(function (obj) {
|
||||
if (obj && obj.error) {
|
||||
// Deal with it as if it was not owned
|
||||
oldIsOwned = false;
|
||||
return;
|
||||
}
|
||||
common.logoutFromAll(waitFor(function () {
|
||||
postMessage("DISCONNECT");
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}).nThen(function (waitFor) {
|
||||
if (!oldIsOwned) {
|
||||
postMessage("SET", {
|
||||
key: [Constants.deprecatedKey],
|
||||
value: true
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) {
|
||||
console.error(obj.error);
|
||||
}
|
||||
common.logoutFromAll(waitFor(function () {
|
||||
postMessage("DISCONNECT");
|
||||
}));
|
||||
}));
|
||||
}
|
||||
}).nThen(function () {
|
||||
// We have the new drive, with the new login block
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
// Loading events
|
||||
common.loading = {};
|
||||
common.loading.onDriveEvent = Util.mkEvent();
|
||||
|
@ -887,6 +1045,34 @@ define([
|
|||
if (AppConfig.beforeLogin) {
|
||||
AppConfig.beforeLogin(LocalStore.isLoggedIn(), waitFor());
|
||||
}
|
||||
|
||||
}).nThen(function (waitFor) {
|
||||
var blockHash = LocalStore.getBlockHash();
|
||||
if (blockHash) {
|
||||
console.log(blockHash);
|
||||
var parsed = Hash.parseBlockHash(blockHash);
|
||||
|
||||
if (typeof(parsed) !== 'object') {
|
||||
console.error("Failed to parse blockHash");
|
||||
console.log(parsed);
|
||||
return;
|
||||
} else {
|
||||
console.log(parsed);
|
||||
}
|
||||
Util.fetch(parsed.href, waitFor(function (err, arraybuffer) {
|
||||
if (err) { return void console.log(err); }
|
||||
|
||||
// use the results to load your user hash and
|
||||
// put your userhash into localStorage
|
||||
try {
|
||||
var block_info = Block.decrypt(arraybuffer, parsed.keys);
|
||||
if (block_info[Constants.userHashKey]) { LocalStore.setUserHash(block_info[Constants.userHashKey]); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return void console.error("failed to decrypt or decode block content");
|
||||
}
|
||||
}));
|
||||
}
|
||||
}).nThen(function (waitFor) {
|
||||
var cfg = {
|
||||
init: true,
|
||||
|
|
|
@ -13,7 +13,7 @@ define([
|
|||
'/common/outer/network-config.js',
|
||||
'/customize/application_config.js',
|
||||
|
||||
'/bower_components/chainpad-crypto/crypto.js?v=0.1.5',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
'/bower_components/chainpad-listmap/chainpad-listmap.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
|
@ -285,6 +285,15 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
Store.removeLoginBlock = function (clientId, data, cb) {
|
||||
store.rpc.removeLoginBlock(data, function (e, res) {
|
||||
cb({
|
||||
error: e,
|
||||
data: res
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Store.initRpc = function (clientId, data, cb) {
|
||||
if (store.rpc) { return void cb(account); }
|
||||
require(['/common/pinpad.js'], function (Pinpad) {
|
||||
|
|
|
@ -58,6 +58,14 @@ define([
|
|||
localStorage[Constants.userHashKey] = sHash;
|
||||
};
|
||||
|
||||
LocalStore.getBlockHash = function () {
|
||||
return localStorage[Constants.blockHashKey];
|
||||
};
|
||||
|
||||
LocalStore.setBlockHash = function (hash) {
|
||||
localStorage[Constants.blockHashKey] = hash;
|
||||
};
|
||||
|
||||
LocalStore.getAccountName = function () {
|
||||
return localStorage[Constants.userNameKey];
|
||||
};
|
||||
|
@ -69,7 +77,7 @@ define([
|
|||
|
||||
|
||||
|
||||
|
||||
// XXX update this to take into account blockHash values
|
||||
LocalStore.login = function (hash, name, cb) {
|
||||
if (!hash) { throw new Error('expected a user hash'); }
|
||||
if (!name) { throw new Error('expected a user name'); }
|
||||
|
@ -96,6 +104,7 @@ define([
|
|||
[
|
||||
Constants.userNameKey,
|
||||
Constants.userHashKey,
|
||||
Constants.blockHashKey,
|
||||
'loginToken',
|
||||
'plan',
|
||||
].forEach(function (k) {
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
define([
|
||||
'/common/common-util.js',
|
||||
'/api/config',
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function (Util, ApiConfig) {
|
||||
var Nacl = window.nacl;
|
||||
|
||||
var Block = {};
|
||||
|
||||
Block.join = Util.uint8ArrayJoin;
|
||||
|
||||
// publickey <base64 string>
|
||||
|
||||
// signature <base64 string>
|
||||
|
||||
// block <base64 string>
|
||||
|
||||
// [b64_public, b64_sig, b64_block [version, nonce, content]]
|
||||
|
||||
Block.seed = function () {
|
||||
return Nacl.hash(Nacl.util.decodeUTF8('pewpewpew'));
|
||||
};
|
||||
|
||||
// should be deterministic from a seed...
|
||||
Block.genkeys = function (seed) {
|
||||
if (!(seed instanceof Uint8Array)) {
|
||||
throw new Error('INVALID_SEED_FORMAT');
|
||||
}
|
||||
if (!seed || typeof(seed.length) !== 'number' || seed.length < 64) {
|
||||
throw new Error('INVALID_SEED_LENGTH');
|
||||
}
|
||||
|
||||
var signSeed = seed.subarray(0, Nacl.sign.seedLength);
|
||||
var symmetric = seed.subarray(Nacl.sign.seedLength,
|
||||
Nacl.sign.seedLength + Nacl.secretbox.keyLength);
|
||||
|
||||
console.log("symmetric key: ", Nacl.util.encodeBase64(symmetric));
|
||||
|
||||
return {
|
||||
sign: Nacl.sign.keyPair.fromSeed(signSeed), // 32 bytes
|
||||
symmetric: symmetric, // 32 bytes ...
|
||||
};
|
||||
};
|
||||
|
||||
// (UTF8 content, keys object) => Uint8Array block
|
||||
Block.encrypt = function (version, content, keys) {
|
||||
var u8 = Nacl.util.decodeUTF8(content);
|
||||
var nonce = Nacl.randomBytes(Nacl.secretbox.nonceLength);
|
||||
return Block.join([
|
||||
[0],
|
||||
nonce,
|
||||
Nacl.secretbox(u8, nonce, keys.symmetric)
|
||||
]);
|
||||
};
|
||||
|
||||
// (uint8Array block) => payload object
|
||||
Block.decrypt = function (u8_content, keys) {
|
||||
// version is currently ignored since there is only one
|
||||
var nonce = u8_content.subarray(1, 1 + Nacl.secretbox.nonceLength);
|
||||
var box = u8_content.subarray(1 + Nacl.secretbox.nonceLength);
|
||||
|
||||
var plaintext = Nacl.secretbox.open(box, nonce, keys.symmetric);
|
||||
try {
|
||||
return JSON.parse(Nacl.util.encodeUTF8(plaintext));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// (Uint8Array block) => signature
|
||||
Block.sign = function (ciphertext, keys) {
|
||||
return Nacl.sign.detached(Nacl.hash(ciphertext), keys.sign.secretKey);
|
||||
};
|
||||
|
||||
Block.serialize = function (content, keys) {
|
||||
// encrypt the content
|
||||
var ciphertext = Block.encrypt(0, content, keys);
|
||||
|
||||
// generate a detached signature
|
||||
var sig = Block.sign(ciphertext, keys);
|
||||
|
||||
// serialize {publickey, sig, ciphertext}
|
||||
return {
|
||||
publicKey: Nacl.util.encodeBase64(keys.sign.publicKey),
|
||||
signature: Nacl.util.encodeBase64(sig),
|
||||
ciphertext: Nacl.util.encodeBase64(ciphertext),
|
||||
};
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
};
|
||||
|
||||
// FIXME don't spread the functions below across this file and common-hash
|
||||
// find a permanent home for these hacks
|
||||
var urlSafeB64 = function (u8) {
|
||||
return Nacl.util.encodeBase64(u8).replace(/\//g, '-');
|
||||
};
|
||||
|
||||
Block.getBlockHash = function (keys) {
|
||||
var publicKey = urlSafeB64(keys.sign.publicKey);
|
||||
// 'block/' here is hardcoded because it's hardcoded on the server
|
||||
// if we want to make CryptPad work in server subfolders, we'll need
|
||||
// to update this path derivation
|
||||
var relative = 'block/' + publicKey.slice(0, 2) + '/' + publicKey;
|
||||
var symmetric = urlSafeB64(keys.symmetric);
|
||||
return ApiConfig.httpUnsafeOrigin + relative + '#' + symmetric;
|
||||
};
|
||||
|
||||
/*
|
||||
Block.createBlockHash = function (href, key) {
|
||||
if (typeof(href) !== 'string') { return; }
|
||||
if (!(key instanceof Uint8Array)) { return; }
|
||||
|
||||
try { return href + '#' + Nacl.util.encodeBase64(key); }
|
||||
catch (e) { return; }
|
||||
};
|
||||
*/
|
||||
|
||||
var decodeSafeB64 = function (b64) {
|
||||
try {
|
||||
return Nacl.util.decodeBase64(b64.replace(/\-/g, '/'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
Block.parseBlockHash = function (hash) {
|
||||
if (typeof(hash) !== 'string') { return; }
|
||||
var parts = hash.split('#');
|
||||
if (parts.length !== 2) { return; }
|
||||
|
||||
try {
|
||||
return {
|
||||
href: parts[0],
|
||||
keys: {
|
||||
symmetric: decodeSafeB64(parts[1]),
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return Block;
|
||||
});
|
|
@ -24,6 +24,7 @@ define([
|
|||
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,
|
||||
|
|
|
@ -222,7 +222,34 @@ define([
|
|||
};
|
||||
|
||||
exp.writeLoginBlock = function (data, cb) {
|
||||
cb();
|
||||
if (!data) { return void cb('NO_DATA'); }
|
||||
if (!data.publicKey || !data.signature || !data.ciphertext) {
|
||||
console.log(data);
|
||||
return void cb("MISSING_PARAMETERS");
|
||||
}
|
||||
|
||||
rpc.send('WRITE_LOGIN_BLOCK', [
|
||||
data.publicKey,
|
||||
data.signature,
|
||||
data.ciphertext
|
||||
], 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);
|
||||
});
|
||||
};
|
||||
|
||||
cb(e, exp);
|
||||
|
|
|
@ -661,10 +661,18 @@ define([
|
|||
Cryptpad.changePadPassword(Cryptget, href, data.password, edPublic, cb);
|
||||
});
|
||||
|
||||
sframeChan.on('Q_CHANGE_USER_PASSWORD', function (data, cb) {
|
||||
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);
|
||||
});
|
||||
|
||||
if (cfg.addRpc) {
|
||||
cfg.addRpc(sframeChan, Cryptpad, Utils);
|
||||
}
|
||||
|
|
|
@ -77,6 +77,9 @@ define({
|
|||
// Write/update the login block when the account password is changed
|
||||
'Q_WRITE_LOGIN_BLOCK': true,
|
||||
|
||||
// Remove login blocks
|
||||
'Q_REMOVE_LOGIN_BLOCK': true,
|
||||
|
||||
// Check the pin limit to determine if we can store the pad in the drive or if we should.
|
||||
// display a warning
|
||||
'Q_GET_PIN_LIMIT_STATUS': true,
|
||||
|
@ -235,6 +238,9 @@ define({
|
|||
// Change pad password
|
||||
'Q_PAD_PASSWORD_CHANGE': true,
|
||||
|
||||
// Migrate drive to owned drive
|
||||
'Q_CHANGE_USER_PASSWORD': true,
|
||||
|
||||
// Loading events to display in the loading screen
|
||||
'EV_LOADING_INFO': true,
|
||||
// Critical error outside the iframe during loading screen
|
||||
|
|
|
@ -50,7 +50,7 @@ define([
|
|||
'cp-settings-resettips',
|
||||
'cp-settings-thumbnails',
|
||||
'cp-settings-userfeedback',
|
||||
//'cp-settings-change-password',
|
||||
'cp-settings-change-password',
|
||||
'cp-settings-delete'
|
||||
],
|
||||
'creation': [
|
||||
|
@ -404,12 +404,11 @@ define([
|
|||
$(form).appendTo($div);
|
||||
|
||||
var updateBlock = function (data, cb) {
|
||||
sframeChan.query('Q_WRITE_LOGIN_BLOCK', data, function (err, obj) {
|
||||
sframeChan.query('Q_CHANGE_USER_PASSWORD', data, function (err, obj) {
|
||||
if (err || obj.error) { return void cb ({error: err || obj.error}); }
|
||||
cb (obj);
|
||||
});
|
||||
};
|
||||
updateBlock = updateBlock; // jshint..
|
||||
|
||||
var todo = function () {
|
||||
var oldPassword = $(form).find('#cp-settings-change-password-current').val();
|
||||
|
@ -432,8 +431,15 @@ define([
|
|||
UI.confirm(Messages.settings_changePasswordConfirm,
|
||||
function (yes) {
|
||||
if (!yes) { return; }
|
||||
// TODO
|
||||
console.log(oldPassword, newPassword, newPasswordConfirm);
|
||||
updateBlock({
|
||||
password: oldPassword,
|
||||
newPassword: newPassword
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
// TODO
|
||||
UI.alert(Messages.settings_changePasswordError);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
ok: Messages.register_writtenPassword,
|
||||
cancel: Messages.register_cancel,
|
||||
|
@ -461,6 +467,50 @@ define([
|
|||
return $div;
|
||||
};
|
||||
|
||||
create['migrate'] = function () {
|
||||
if (true) { return; } // XXX js hint
|
||||
// TODO
|
||||
// if (!loginBlock) { return; }
|
||||
// if (alreadyMigrated) { return; }
|
||||
if (!common.isLoggedIn()) { return; }
|
||||
|
||||
var $div = $('<div>', { 'class': 'cp-settings-migrate cp-sidebarlayout-element'});
|
||||
|
||||
$('<span>', {'class': 'label'}).text(Messages.settings_ownDriveTitle).appendTo($div);
|
||||
|
||||
$('<span>', {'class': 'cp-sidebarlayout-description'})
|
||||
.append(Messages.settings_ownDriveHint).appendTo($div);
|
||||
|
||||
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved});
|
||||
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'});
|
||||
|
||||
var $button = $('<button>', {'id': 'cp-settings-delete', 'class': 'btn btn-primary'})
|
||||
.text(Messages.settings_ownDriveButton).appendTo($div);
|
||||
|
||||
$button.click(function () {
|
||||
$spinner.show();
|
||||
UI.confirm(Messages.settings_ownDriveConfirm, function (yes) {
|
||||
if (!yes) { return; }
|
||||
sframeChan.query("Q_OWN_USER_DRIVE", null, function (err, data) {
|
||||
if (err || data.error) {
|
||||
console.error(err || data.error);
|
||||
// TODO
|
||||
$spinner.hide();
|
||||
return;
|
||||
}
|
||||
// TODO: drive is migrated, autoamtic redirect from outer?
|
||||
$ok.show();
|
||||
$spinner.hide();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$spinner.hide().appendTo($div);
|
||||
$ok.hide().appendTo($div);
|
||||
|
||||
return $div;
|
||||
};
|
||||
|
||||
// Pad Creation settings
|
||||
|
||||
var setHTML = function (e, html) {
|
||||
|
|
Loading…
Reference in New Issue