Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging
This commit is contained in:
@@ -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) {
|
||||
|
||||
156
www/common/outer/login-block.js
Normal file
156
www/common/outer/login-block.js
Normal 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;
|
||||
});
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user