|
|
|
|
(function (window) {
|
|
|
|
|
var factory = function (Util, Crypto, Keys, Nacl) {
|
|
|
|
|
var Hash = window.CryptPad_Hash = {};
|
|
|
|
|
|
|
|
|
|
var uint8ArrayToHex = Util.uint8ArrayToHex;
|
|
|
|
|
var hexToBase64 = Util.hexToBase64;
|
|
|
|
|
var base64ToHex = Util.base64ToHex;
|
|
|
|
|
Hash.encodeBase64 = Nacl.util.encodeBase64;
|
|
|
|
|
Hash.decodeBase64 = Nacl.util.decodeBase64;
|
|
|
|
|
|
|
|
|
|
// This implementation must match that on the server
|
|
|
|
|
// it's used for a checksum
|
|
|
|
|
Hash.hashChannelList = function (list) {
|
|
|
|
|
return Nacl.util.encodeBase64(Nacl.hash(Nacl.util
|
|
|
|
|
.decodeUTF8(JSON.stringify(list))));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.generateSignPair = function () {
|
|
|
|
|
var ed = Nacl.sign.keyPair();
|
|
|
|
|
var makeSafe = function (key) {
|
|
|
|
|
return Crypto.b64RemoveSlashes(key).replace(/=+$/g, '');
|
|
|
|
|
};
|
|
|
|
|
return {
|
|
|
|
|
validateKey: Hash.encodeBase64(ed.publicKey),
|
|
|
|
|
signKey: Hash.encodeBase64(ed.secretKey),
|
|
|
|
|
safeValidateKey: makeSafe(Hash.encodeBase64(ed.publicKey)),
|
|
|
|
|
safeSignKey: makeSafe(Hash.encodeBase64(ed.secretKey)),
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.getSignPublicFromPrivate = function (edPrivateSafeStr) {
|
|
|
|
|
var edPrivateStr = Crypto.b64AddSlashes(edPrivateSafeStr);
|
|
|
|
|
var privateKey = Nacl.util.decodeBase64(edPrivateStr);
|
|
|
|
|
var keyPair = Nacl.sign.keyPair.fromSecretKey(privateKey);
|
|
|
|
|
return Nacl.util.encodeBase64(keyPair.publicKey);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) {
|
|
|
|
|
var version = secret.version;
|
|
|
|
|
var data = secret.keys;
|
|
|
|
|
if (version === 0) {
|
|
|
|
|
return secret.channel + secret.key;
|
|
|
|
|
}
|
|
|
|
|
if (version === 1) {
|
|
|
|
|
if (!data.editKeyStr) { return; }
|
|
|
|
|
return '/1/edit/' + hexToBase64(secret.channel) +
|
|
|
|
|
'/' + Crypto.b64RemoveSlashes(data.editKeyStr) + '/';
|
|
|
|
|
}
|
|
|
|
|
if (version === 2) {
|
|
|
|
|
if (!data.editKeyStr) { return; }
|
|
|
|
|
var pass = secret.password ? 'p/' : '';
|
|
|
|
|
return '/2/' + secret.type + '/edit/' + Crypto.b64RemoveSlashes(data.editKeyStr) + '/' + pass;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var getViewHashFromKeys = Hash.getViewHashFromKeys = function (secret) {
|
|
|
|
|
var version = secret.version;
|
|
|
|
|
var data = secret.keys;
|
|
|
|
|
if (version === 0) { return; }
|
|
|
|
|
if (version === 1) {
|
|
|
|
|
if (!data.viewKeyStr) { return; }
|
|
|
|
|
return '/1/view/' + hexToBase64(secret.channel) +
|
|
|
|
|
'/'+Crypto.b64RemoveSlashes(data.viewKeyStr)+'/';
|
|
|
|
|
}
|
|
|
|
|
if (version === 2) {
|
|
|
|
|
if (!data.viewKeyStr) { return; }
|
|
|
|
|
var pass = secret.password ? 'p/' : '';
|
|
|
|
|
return '/2/' + secret.type + '/view/' + Crypto.b64RemoveSlashes(data.viewKeyStr) + '/' + pass;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.getHiddenHashFromKeys = function (type, secret, opts) {
|
|
|
|
|
opts = opts || {};
|
|
|
|
|
var canEdit = (secret.keys && secret.keys.editKeyStr) || secret.key;
|
|
|
|
|
var mode = (!opts.view && canEdit) ? 'edit/' : 'view/';
|
|
|
|
|
var pass = secret.password ? 'p/' : '';
|
|
|
|
|
|
|
|
|
|
if (secret.keys && secret.keys.fileKeyStr) { mode = ''; }
|
|
|
|
|
|
|
|
|
|
var hash = '/3/' + type + '/' + mode + secret.channel + '/' + pass;
|
|
|
|
|
var hashData = Hash.parseTypeHash(type, hash);
|
|
|
|
|
if (hashData && hashData.getHash) {
|
|
|
|
|
return hashData.getHash(opts || {});
|
|
|
|
|
}
|
|
|
|
|
return hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var getFileHashFromKeys = Hash.getFileHashFromKeys = function (secret) {
|
|
|
|
|
var version = secret.version;
|
|
|
|
|
var data = secret.keys;
|
|
|
|
|
if (version === 0) { return; }
|
|
|
|
|
if (version === 1) {
|
|
|
|
|
return '/1/' + hexToBase64(secret.channel) + '/' +
|
|
|
|
|
Crypto.b64RemoveSlashes(data.fileKeyStr) + '/';
|
|
|
|
|
}
|
|
|
|
|
if (version === 2) {
|
|
|
|
|
if (!data.fileKeyStr) { return; }
|
|
|
|
|
var pass = secret.password ? 'p/' : '';
|
|
|
|
|
return '/2/' + secret.type + '/' + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/' + pass;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.getPublicSigningKeyString = Keys.serialize;
|
|
|
|
|
|
|
|
|
|
var fixDuplicateSlashes = function (s) {
|
|
|
|
|
return s.replace(/\/+/g, '/');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.ephemeralChannelLength = 34;
|
|
|
|
|
Hash.createChannelId = function (ephemeral) {
|
|
|
|
|
var id = uint8ArrayToHex(Crypto.Nacl.randomBytes(ephemeral? 17: 16));
|
|
|
|
|
if ([32, 34].indexOf(id.length) === -1 || /[^a-f0-9]/.test(id)) {
|
|
|
|
|
throw new Error('channel ids must consist of 32 hex characters');
|
|
|
|
|
}
|
|
|
|
|
return id;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Given a base64-encoded public key, deterministically derive a channel id
|
|
|
|
|
Used for support mailboxes
|
|
|
|
|
*/
|
|
|
|
|
Hash.getChannelIdFromKey = function (publicKey) {
|
|
|
|
|
if (!publicKey) { return; }
|
|
|
|
|
return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Given a base64-encoded asymmetric private key
|
|
|
|
|
derive the corresponding public key
|
|
|
|
|
*/
|
|
|
|
|
Hash.getBoxPublicFromSecret = function (priv) {
|
|
|
|
|
if (!priv) { return; }
|
|
|
|
|
var u8_priv = Hash.decodeBase64(priv);
|
|
|
|
|
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
|
|
|
|
|
return Hash.encodeBase64(pair.publicKey);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Given a base64-encoded private key and public key
|
|
|
|
|
check that the keys are part of a valid keypair
|
|
|
|
|
*/
|
|
|
|
|
Hash.checkBoxKeyPair = function (priv, pub) {
|
|
|
|
|
if (!pub || !priv) { return false; }
|
|
|
|
|
var u8_priv = Hash.decodeBase64(priv);
|
|
|
|
|
var pair = Nacl.box.keyPair.fromSecretKey(u8_priv);
|
|
|
|
|
return pub === Hash.encodeBase64(pair.publicKey);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.createRandomHash = function (type, password) {
|
|
|
|
|
var cryptor;
|
|
|
|
|
if (type === 'file') {
|
|
|
|
|
cryptor = Crypto.createFileCryptor2(void 0, password);
|
|
|
|
|
return getFileHashFromKeys({
|
|
|
|
|
password: Boolean(password),
|
|
|
|
|
version: 2,
|
|
|
|
|
type: type,
|
|
|
|
|
keys: cryptor
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
cryptor = Crypto.createEditCryptor2(void 0, void 0, password);
|
|
|
|
|
return getEditHashFromKeys({
|
|
|
|
|
password: Boolean(password),
|
|
|
|
|
version: 2,
|
|
|
|
|
type: type,
|
|
|
|
|
keys: cryptor
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Version 0
|
|
|
|
|
/pad/#67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy
|
|
|
|
|
Version 1
|
|
|
|
|
/code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
var getNewPadOpts = function (hashArr) {
|
|
|
|
|
var k;
|
|
|
|
|
// Check if we have a ownerKey for this pad
|
|
|
|
|
hashArr.some(function (data) {
|
|
|
|
|
if (/^newpad=/.test(data)) {
|
|
|
|
|
k = data.slice(7);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return k || '';
|
|
|
|
|
};
|
|
|
|
|
var getVersionHash = function (hashArr) {
|
|
|
|
|
var k;
|
|
|
|
|
// Check if we have a ownerKey for this pad
|
|
|
|
|
hashArr.some(function (data) {
|
|
|
|
|
if (/^hash=/.test(data)) {
|
|
|
|
|
k = data.slice(5);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return k ? Crypto.b64AddSlashes(k) : '';
|
|
|
|
|
};
|
|
|
|
|
var getOwnerKey = function (hashArr) {
|
|
|
|
|
var k;
|
|
|
|
|
// Check if we have a ownerKey for this pad
|
|
|
|
|
hashArr.some(function (data) {
|
|
|
|
|
if (data.length === 86) {
|
|
|
|
|
k = data;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return k;
|
|
|
|
|
};
|
|
|
|
|
var parseTypeHash = Hash.parseTypeHash = function (type, hash) {
|
|
|
|
|
if (!hash) { return; }
|
|
|
|
|
var options = [];
|
|
|
|
|
var parsed = {};
|
|
|
|
|
var hashArr = fixDuplicateSlashes(hash).split('/');
|
|
|
|
|
|
|
|
|
|
var addOptions = function () {
|
|
|
|
|
parsed.password = options.indexOf('p') !== -1;
|
|
|
|
|
parsed.present = options.indexOf('present') !== -1;
|
|
|
|
|
parsed.embed = options.indexOf('embed') !== -1;
|
|
|
|
|
parsed.versionHash = getVersionHash(options);
|
|
|
|
|
parsed.newPadOpts = getNewPadOpts(options);
|
|
|
|
|
parsed.ownerKey = getOwnerKey(options);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) {
|
|
|
|
|
parsed.type = 'pad';
|
|
|
|
|
parsed.getHash = function () { return hash; };
|
|
|
|
|
parsed.getOptions = function () {
|
|
|
|
|
return {
|
|
|
|
|
embed: parsed.embed,
|
|
|
|
|
present: parsed.present,
|
|
|
|
|
ownerKey: parsed.ownerKey,
|
|
|
|
|
versionHash: parsed.versionHash,
|
|
|
|
|
password: parsed.password
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (/^\/newpad=/.test(hash)) {
|
|
|
|
|
return {
|
|
|
|
|
newPadOpts: hash.slice(8, -1)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hash.slice(0,1) !== '/' && hash.length >= 56) { // Version 0
|
|
|
|
|
// Old hash
|
|
|
|
|
parsed.channel = hash.slice(0, 32);
|
|
|
|
|
parsed.key = hash.slice(32, 56);
|
|
|
|
|
parsed.version = 0;
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Version >= 1: more hash options
|
|
|
|
|
parsed.getHash = function (opts) {
|
|
|
|
|
var hash = hashArr.slice(0, 5).join('/') + '/';
|
|
|
|
|
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
|
|
|
|
|
if (owner) { hash += owner + '/'; }
|
|
|
|
|
if (parsed.password || opts.password) { hash += 'p/'; }
|
|
|
|
|
if (opts.embed) { hash += 'embed/'; }
|
|
|
|
|
if (opts.present) { hash += 'present/'; }
|
|
|
|
|
var versionHash = typeof(opts.versionHash) !== "undefined" ? opts.versionHash : parsed.versionHash;
|
|
|
|
|
if (versionHash) {
|
|
|
|
|
hash += 'hash=' + Crypto.b64RemoveSlashes(versionHash) + '/';
|
|
|
|
|
}
|
|
|
|
|
if (opts.newPadOpts) {
|
|
|
|
|
hash += 'newpad=' + opts.newPadOpts + '/';
|
|
|
|
|
}
|
|
|
|
|
return hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '1') { // Version 1
|
|
|
|
|
parsed.version = 1;
|
|
|
|
|
parsed.mode = hashArr[2];
|
|
|
|
|
parsed.channel = hashArr[3];
|
|
|
|
|
parsed.key = Crypto.b64AddSlashes(hashArr[4]);
|
|
|
|
|
|
|
|
|
|
options = hashArr.slice(5);
|
|
|
|
|
addOptions();
|
|
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '2') { // Version 2
|
|
|
|
|
parsed.version = 2;
|
|
|
|
|
parsed.app = hashArr[2];
|
|
|
|
|
parsed.mode = hashArr[3];
|
|
|
|
|
parsed.key = hashArr[4];
|
|
|
|
|
|
|
|
|
|
options = hashArr.slice(5);
|
|
|
|
|
addOptions();
|
|
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '3') { // Version 3: hidden hash
|
|
|
|
|
parsed.version = 3;
|
|
|
|
|
parsed.app = hashArr[2];
|
|
|
|
|
parsed.mode = hashArr[3];
|
|
|
|
|
parsed.channel = hashArr[4];
|
|
|
|
|
|
|
|
|
|
options = hashArr.slice(5);
|
|
|
|
|
addOptions();
|
|
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
parsed.getHash = function () { return hashArr.join('/'); };
|
|
|
|
|
if (['media', 'file'].indexOf(type) !== -1) {
|
|
|
|
|
parsed.type = 'file';
|
|
|
|
|
|
|
|
|
|
parsed.getOptions = function () {
|
|
|
|
|
return {
|
|
|
|
|
embed: parsed.embed,
|
|
|
|
|
present: parsed.present,
|
|
|
|
|
ownerKey: parsed.ownerKey,
|
|
|
|
|
password: parsed.password
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
parsed.getHash = function (opts) {
|
|
|
|
|
var hash = hashArr.slice(0, 4).join('/') + '/';
|
|
|
|
|
var owner = typeof(opts.ownerKey) !== "undefined" ? opts.ownerKey : parsed.ownerKey;
|
|
|
|
|
if (owner) { hash += owner + '/'; }
|
|
|
|
|
if (parsed.password || opts.password) { hash += 'p/'; }
|
|
|
|
|
if (opts.embed) { hash += 'embed/'; }
|
|
|
|
|
if (opts.present) { hash += 'present/'; }
|
|
|
|
|
return hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '1') {
|
|
|
|
|
parsed.version = 1;
|
|
|
|
|
parsed.channel = hashArr[2].replace(/-/g, '/');
|
|
|
|
|
parsed.key = hashArr[3].replace(/-/g, '/');
|
|
|
|
|
options = hashArr.slice(4);
|
|
|
|
|
addOptions();
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '2') { // Version 2
|
|
|
|
|
parsed.version = 2;
|
|
|
|
|
parsed.app = hashArr[2];
|
|
|
|
|
parsed.key = hashArr[3];
|
|
|
|
|
|
|
|
|
|
options = hashArr.slice(4);
|
|
|
|
|
addOptions();
|
|
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '3') { // Version 3: hidden hash
|
|
|
|
|
parsed.version = 3;
|
|
|
|
|
parsed.app = hashArr[2];
|
|
|
|
|
parsed.channel = hashArr[3];
|
|
|
|
|
|
|
|
|
|
options = hashArr.slice(4);
|
|
|
|
|
addOptions();
|
|
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
if (['user'].indexOf(type) !== -1) {
|
|
|
|
|
parsed.type = 'user';
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '1') {
|
|
|
|
|
parsed.version = 1;
|
|
|
|
|
parsed.user = hashArr[2];
|
|
|
|
|
parsed.pubkey = hashArr[3].replace(/-/g, '/');
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
if (['invite'].indexOf(type) !== -1) {
|
|
|
|
|
parsed.type = 'invite';
|
|
|
|
|
if (hashArr[1] && hashArr[1] === '2') {
|
|
|
|
|
parsed.version = 2;
|
|
|
|
|
parsed.app = hashArr[2];
|
|
|
|
|
parsed.mode = hashArr[3];
|
|
|
|
|
parsed.key = hashArr[4];
|
|
|
|
|
|
|
|
|
|
options = hashArr.slice(5);
|
|
|
|
|
parsed.password = options.indexOf('p') !== -1;
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
var parsePadUrl = Hash.parsePadUrl = function (href) {
|
|
|
|
|
var patt = /^https*:\/\/([^\/]*)\/(.*?)\//i;
|
|
|
|
|
|
|
|
|
|
var ret = {};
|
|
|
|
|
|
|
|
|
|
if (!href) { return ret; }
|
|
|
|
|
if (href.slice(-1) !== '/' && href.slice(-1) !== '#') { href += '/'; }
|
|
|
|
|
href = href.replace(/\/\?[^#]+#/, '/#');
|
|
|
|
|
|
|
|
|
|
var idx;
|
|
|
|
|
|
|
|
|
|
ret.getUrl = function (options) {
|
|
|
|
|
options = options || {};
|
|
|
|
|
var url = '/';
|
|
|
|
|
if (!ret.type) { return url; }
|
|
|
|
|
url += ret.type + '/';
|
|
|
|
|
// New pad with options: append the options to the hash
|
|
|
|
|
if (!ret.hashData && options.newPadOpts) {
|
|
|
|
|
return url + '#/newpad=' + options.newPadOpts + '/';
|
|
|
|
|
}
|
|
|
|
|
if (!ret.hashData) { return url; }
|
|
|
|
|
if (ret.hashData.type !== 'pad') { return url + '#' + ret.hash; }
|
|
|
|
|
if (ret.hashData.version === 0) { return url + '#' + ret.hash; }
|
|
|
|
|
var hash = ret.hashData.getHash(options);
|
|
|
|
|
url += '#' + hash;
|
|
|
|
|
return url;
|
|
|
|
|
};
|
|
|
|
|
ret.getOptions = function () {
|
|
|
|
|
if (!ret.hashData || !ret.hashData.getOptions) { return {}; }
|
|
|
|
|
return ret.hashData.getOptions();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!/^https*:\/\//.test(href)) {
|
|
|
|
|
idx = href.indexOf('/#');
|
|
|
|
|
ret.type = href.slice(1, idx);
|
|
|
|
|
if (idx === -1) { return ret; }
|
|
|
|
|
ret.hash = href.slice(idx + 2);
|
|
|
|
|
ret.hashData = parseTypeHash(ret.type, ret.hash);
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
href.replace(patt, function (a, domain, type) {
|
|
|
|
|
ret.domain = domain;
|
|
|
|
|
ret.type = type;
|
|
|
|
|
return '';
|
|
|
|
|
});
|
|
|
|
|
idx = href.indexOf('/#');
|
|
|
|
|
if (idx === -1) { return ret; }
|
|
|
|
|
ret.hash = href.slice(idx + 2);
|
|
|
|
|
ret.hashData = parseTypeHash(ret.type, ret.hash);
|
|
|
|
|
return ret;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.hashToHref = function (hash, type) {
|
|
|
|
|
return '/' + type + '/#' + hash;
|
|
|
|
|
};
|
|
|
|
|
Hash.hrefToHash = function (href) {
|
|
|
|
|
var parsed = Hash.parsePadUrl(href);
|
|
|
|
|
return parsed.hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.getRelativeHref = function (href) {
|
|
|
|
|
if (!href) { return; }
|
|
|
|
|
if (href.indexOf('#') === -1) { return; }
|
|
|
|
|
var parsed = parsePadUrl(href);
|
|
|
|
|
return '/' + parsed.type + '/#' + parsed.hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Returns all needed keys for a realtime channel
|
|
|
|
|
* - no argument: use the URL hash or create one if it doesn't exist
|
|
|
|
|
* - secretHash provided: use secretHash to find the keys
|
|
|
|
|
*/
|
|
|
|
|
Hash.getSecrets = function (type, secretHash, password) {
|
|
|
|
|
var secret = {};
|
|
|
|
|
var generate = function () {
|
|
|
|
|
secret.keys = Crypto.createEditCryptor2(void 0, void 0, password);
|
|
|
|
|
secret.channel = base64ToHex(secret.keys.chanId);
|
|
|
|
|
secret.version = 2;
|
|
|
|
|
secret.type = type;
|
|
|
|
|
};
|
|
|
|
|
if (!secretHash) {
|
|
|
|
|
generate();
|
|
|
|
|
return secret;
|
|
|
|
|
} else {
|
|
|
|
|
var parsed;
|
|
|
|
|
var hash;
|
|
|
|
|
if (secretHash) {
|
|
|
|
|
if (!type) { throw new Error("getSecrets with a hash requires a type parameter"); }
|
|
|
|
|
parsed = parseTypeHash(type, secretHash);
|
|
|
|
|
hash = secretHash;
|
|
|
|
|
}
|
|
|
|
|
if (hash.length === 0) {
|
|
|
|
|
generate();
|
|
|
|
|
return secret;
|
|
|
|
|
}
|
|
|
|
|
// old hash system : #{hexChanKey}{cryptKey}
|
|
|
|
|
// new hash system : #/{hashVersion}/{b64ChanKey}/{cryptKey}
|
|
|
|
|
if (parsed.version === 0) {
|
|
|
|
|
// Old hash
|
|
|
|
|
secret.channel = parsed.channel;
|
|
|
|
|
secret.key = parsed.key;
|
|
|
|
|
secret.version = 0;
|
|
|
|
|
} else if (parsed.version === 1) {
|
|
|
|
|
// New hash
|
|
|
|
|
secret.version = 1;
|
|
|
|
|
if (parsed.type === "pad") {
|
|
|
|
|
secret.channel = base64ToHex(parsed.channel);
|
|
|
|
|
if (parsed.mode === 'edit') {
|
|
|
|
|
secret.keys = Crypto.createEditCryptor(parsed.key);
|
|
|
|
|
secret.key = secret.keys.editKeyStr;
|
|
|
|
|
if (secret.channel.length !== 32 || secret.key.length !== 24) {
|
|
|
|
|
throw new Error("The channel key and/or the encryption key is invalid");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (parsed.mode === 'view') {
|
|
|
|
|
secret.keys = Crypto.createViewCryptor(parsed.key);
|
|
|
|
|
if (secret.channel.length !== 32) {
|
|
|
|
|
throw new Error("The channel key is invalid");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (parsed.type === "file") {
|
|
|
|
|
secret.channel = base64ToHex(parsed.channel);
|
|
|
|
|
secret.keys = {
|
|
|
|
|
fileKeyStr: parsed.key,
|
|
|
|
|
cryptKey: Nacl.util.decodeBase64(parsed.key)
|
|
|
|
|
};
|
|
|
|
|
} else if (parsed.type === "user") {
|
|
|
|
|
throw new Error("User hashes can't be opened (yet)");
|
|
|
|
|
}
|
|
|
|
|
} else if (parsed.version === 2) {
|
|
|
|
|
// New hash
|
|
|
|
|
secret.version = 2;
|
|
|
|
|
secret.type = type;
|
|
|
|
|
secret.password = password;
|
|
|
|
|
if (parsed.type === "pad") {
|
|
|
|
|
if (parsed.mode === 'edit') {
|
|
|
|
|
secret.keys = Crypto.createEditCryptor2(parsed.key, void 0, password);
|
|
|
|
|
secret.channel = base64ToHex(secret.keys.chanId);
|
|
|
|
|
secret.key = secret.keys.editKeyStr;
|
|
|
|
|
if (secret.channel.length !== 32 || secret.key.length !== 24) {
|
|
|
|
|
throw new Error("The channel key and/or the encryption key is invalid");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (parsed.mode === 'view') {
|
|
|
|
|
secret.keys = Crypto.createViewCryptor2(parsed.key, password);
|
|
|
|
|
secret.channel = base64ToHex(secret.keys.chanId);
|
|
|
|
|
if (secret.channel.length !== 32) {
|
|
|
|
|
throw new Error("The channel key is invalid");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (parsed.type === "file") {
|
|
|
|
|
secret.keys = Crypto.createFileCryptor2(parsed.key, password);
|
|
|
|
|
secret.channel = base64ToHex(secret.keys.chanId);
|
|
|
|
|
secret.key = secret.keys.fileKeyStr;
|
|
|
|
|
if (secret.channel.length !== 48 || secret.key.length !== 24) {
|
|
|
|
|
throw new Error("The channel key and/or the encryption key is invalid");
|
|
|
|
|
}
|
|
|
|
|
} else if (parsed.type === "user") {
|
|
|
|
|
throw new Error("User hashes can't be opened (yet)");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return secret;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.getHashes = function (secret) {
|
|
|
|
|
var hashes = {};
|
|
|
|
|
secret = JSON.parse(JSON.stringify(secret));
|
|
|
|
|
|
|
|
|
|
if (!secret.keys && !secret.key) {
|
|
|
|
|
console.error('e');
|
|
|
|
|
return hashes;
|
|
|
|
|
} else if (!secret.keys) {
|
|
|
|
|
secret.keys = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (secret.keys.editKeyStr || (secret.version === 0 && secret.key)) {
|
|
|
|
|
hashes.editHash = getEditHashFromKeys(secret);
|
|
|
|
|
}
|
|
|
|
|
if (secret.keys.viewKeyStr) {
|
|
|
|
|
hashes.viewHash = getViewHashFromKeys(secret);
|
|
|
|
|
}
|
|
|
|
|
if (secret.keys.fileKeyStr) {
|
|
|
|
|
hashes.fileHash = getFileHashFromKeys(secret);
|
|
|
|
|
}
|
|
|
|
|
return hashes;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// STORAGE
|
|
|
|
|
Hash.hrefToHexChannelId = function (href, password) {
|
|
|
|
|
var parsed = Hash.parsePadUrl(href);
|
|
|
|
|
if (!parsed || !parsed.hash) { return; }
|
|
|
|
|
var secret = Hash.getSecrets(parsed.type, parsed.hash, password);
|
|
|
|
|
return secret.channel;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.getBlobPathFromHex = function (id) {
|
|
|
|
|
return '/blob/' + id.slice(0,2) + '/' + id;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.serializeHash = function (hash) {
|
|
|
|
|
if (hash && hash.slice(-1) !== "/") { hash += "/"; }
|
|
|
|
|
return hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.createInviteUrl = function (curvePublic, channel) {
|
|
|
|
|
channel = channel || Hash.createChannelId();
|
|
|
|
|
return window.location.origin + '/invite/#/1/' + channel +
|
|
|
|
|
'/' + curvePublic.replace(/\//g, '-') + '/';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Hash.isValidHref = function (href) {
|
|
|
|
|
// Non-empty href?
|
|
|
|
|
if (!href) { return; }
|
|
|
|
|
var parsed = Hash.parsePadUrl(href);
|
|
|
|
|
// Can be parsed?
|
|
|
|
|
if (!parsed) { return; }
|
|
|
|
|
// Link to a CryptPad app?
|
|
|
|
|
if (!parsed.type) { return; }
|
|
|
|
|
// Valid hash?
|
|
|
|
|
if (parsed.hash) {
|
|
|
|
|
if (!parsed.hashData) { return; }
|
|
|
|
|
// New pad: only newPadOpts allowed
|
|
|
|
|
if (Object.keys(parsed.hashData).length === 1 &&
|
|
|
|
|
parsed.hashData.newPadOpts) { return true; }
|
|
|
|
|
// Version should be a number
|
|
|
|
|
if (typeof(parsed.hashData.version) === "undefined") { return; }
|
|
|
|
|
// pads and files should have a base64 (or hex) key
|
|
|
|
|
if (parsed.hashData.type === 'pad' || parsed.hashData.type === 'file') {
|
|
|
|
|
if (!parsed.hashData.key && !parsed.hashData.channel) { return; }
|
|
|
|
|
if (parsed.hashData.key && !/^[a-zA-Z0-9+-/=]+$/.test(parsed.hashData.key)) { return; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return Hash;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (typeof(module) !== 'undefined' && module.exports) {
|
|
|
|
|
module.exports = factory(
|
|
|
|
|
require("./common-util"),
|
|
|
|
|
require("chainpad-crypto"),
|
|
|
|
|
require("./common-signing-keys"),
|
|
|
|
|
require("tweetnacl/nacl-fast")
|
|
|
|
|
);
|
|
|
|
|
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
|
|
|
|
define([
|
|
|
|
|
'/common/common-util.js',
|
|
|
|
|
'/bower_components/chainpad-crypto/crypto.js',
|
|
|
|
|
'/common/common-signing-keys.js',
|
|
|
|
|
'/bower_components/tweetnacl/nacl-fast.min.js'
|
|
|
|
|
], function (Util, Crypto, Keys) {
|
|
|
|
|
return factory(Util, Crypto, Keys, window.nacl);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// unsupported initialization
|
|
|
|
|
}
|
|
|
|
|
}(typeof(window) !== 'undefined'? window : {}));
|