Merge branch 'modern-users' into staging

This commit is contained in:
ansuz 2018-06-28 15:30:10 +02:00
commit 57d0738f32
18 changed files with 732 additions and 38 deletions

1
.gitignore vendored
View File

@ -14,6 +14,7 @@ data
npm-debug.log
pins/
blob/
block/
blobstage/
block/
privileged.conf

View File

@ -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

View File

@ -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); });
});
});
});

View File

@ -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
View File

@ -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);
}

View File

@ -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'));

View File

@ -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');

View File

@ -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'
};
});

View File

@ -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');

View File

@ -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,

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
});

View File

@ -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,

View File

@ -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);

View File

@ -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);
}

View File

@ -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

View File

@ -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) {