cryptpad/www/common/common-hash.js

528 lines
20 KiB
JavaScript

define([
'/common/common-util.js',
'/customize/messages.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], function (Util, Messages, Crypto) {
var Nacl = window.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))));
};
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;
}
};
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.getUserHrefFromKeys = function (origin, username, pubkey) {
return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-');
};
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;
};
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 parseTypeHash = Hash.parseTypeHash = function (type, hash) {
if (!hash) { return; }
var options;
var parsed = {};
var hashArr = fixDuplicateSlashes(hash).split('/');
if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) {
parsed.type = 'pad';
parsed.getHash = function () { return hash; };
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;
}
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);
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 5).join('/') + '/';
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
return hash;
};
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);
parsed.password = options.indexOf('p') !== -1;
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 5).join('/') + '/';
if (parsed.password) { hash += 'p/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
return hash;
};
return parsed;
}
return parsed;
}
parsed.getHash = function () { return hashArr.join('/'); };
if (['media', 'file'].indexOf(type) !== -1) {
parsed.type = 'file';
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.channel = hashArr[2].replace(/-/g, '/');
parsed.key = hashArr[3].replace(/-/g, '/');
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);
parsed.password = options.indexOf('p') !== -1;
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.getHash = function (opts) {
var hash = hashArr.slice(0, 4).join('/') + '/';
if (parsed.password) { hash += 'p/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
return hash;
};
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] === '1') {
parsed.version = 1;
parsed.channel = hashArr[2];
parsed.pubkey = hashArr[3].replace(/-/g, '/');
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 + '/';
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;
};
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.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 && !window.location.hash) { //!/#/.test(window.location.href)) {
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;
} else {
var pHref = parsePadUrl(window.location.href);
parsed = pHref.hashData;
hash = pHref.hash;
}
//var hash = secretHash || window.location.hash.slice(1);
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.findWeaker = function (href, channel, recents) {
var parsed = parsePadUrl(href);
if (!parsed.hash) { return false; }
// We can't have a weaker hash if we're already in view mode
if (parsed.hashData && parsed.hashData.mode === 'view') { return; }
var weaker;
Object.keys(recents).some(function (id) {
var pad = recents[id];
if (pad.href || !pad.roHref) {
// This pad has an edit link, so it can't be weaker
return;
}
var p = parsePadUrl(pad.roHref);
if (p.type !== parsed.type) { return; } // Not the same type
if (p.hash === parsed.hash) { return; } // Same hash, not stronger
if (channel !== pad.channel) { return; } // Not the same channel
var pHash = p.hashData;
var parsedHash = parsed.hashData;
if (!parsedHash || !pHash) { return; }
// We don't have stronger/weaker versions of files or users
if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; }
if (pHash.version !== parsedHash.version) { return; }
if (pHash.mode === 'view' && parsedHash.mode === 'edit') {
weaker = pad;
return true;
}
return;
});
return weaker;
};
Hash.findStronger = function (href, channel, recents) {
var parsed = parsePadUrl(href);
if (!parsed.hash) { return false; }
// We can't have a stronger hash if we're already in edit mode
if (parsed.hashData && parsed.hashData.mode === 'edit') { return; }
var stronger;
Object.keys(recents).some(function (id) {
var pad = recents[id];
if (!pad.href) {
// This pad doesn't have an edit link, so it can't be stronger
return;
}
var p = parsePadUrl(pad.href);
if (p.type !== parsed.type) { return; } // Not the same type
if (p.hash === parsed.hash) { return; } // Same hash, not stronger
if (channel !== pad.channel) { return; } // Not the same channel
var pHash = p.hashData;
var parsedHash = parsed.hashData;
if (!parsedHash || !pHash) { return; }
// We don't have stronger/weaker versions of files or users
if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; }
if (pHash.version !== parsedHash.version) { return; }
if (pHash.mode === 'edit' && parsedHash.mode === 'view') {
stronger = pad;
return true;
}
return;
});
return stronger;
};
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, '-') + '/';
};
// Create untitled documents when no name is given
var getLocaleDate = function () {
if (window.Intl && window.Intl.DateTimeFormat) {
var options = {weekday: "short", year: "numeric", month: "long", day: "numeric"};
return new window.Intl.DateTimeFormat(undefined, options).format(new Date());
}
return new Date().toString().split(' ').slice(0,4).join(' ');
};
Hash.getDefaultName = function (parsed) {
var type = parsed.type;
var name = (Messages.type)[type] + ' - ' + getLocaleDate();
return name;
};
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; }
// 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) { return; }
if (!/^[a-zA-Z0-9+-/=]+$/.test(parsed.hashData.key)) { return; }
}
}
return true;
};
return Hash;
});