Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging

This commit is contained in:
yflory
2018-07-02 11:03:58 +02:00
18 changed files with 739 additions and 40 deletions

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