<commit_before>/* vim: ts=4:sw=4
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

window.textsecure = window.textsecure || {};

window.textsecure.crypto = function() {
	'use strict';

	var self = {};
	// functions exposed for replacement and direct calling in test code
	var testing_only = {};

	/******************************
	 *** Random constants/utils ***
	 ******************************/
	// We consider messages lost after a week and might throw away keys at that point
	// (also the time between signedPreKey regenerations)
	var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7;

	var getRandomBytes = function(size) {
		// At some point we might consider XORing in hashes of random
		// UI events to strengthen ourselves against RNG flaws in crypto.getRandomValues
		// ie maybe take a look at how Gibson does it at https://www.grc.com/r&d/js.htm
		var array = new Uint8Array(size);
		window.crypto.getRandomValues(array);
		return array.buffer;
	}
	self.getRandomBytes = getRandomBytes;

	function intToArrayBuffer(nInt) {
		var res = new ArrayBuffer(16);
		var thing = new Uint8Array(res);
		thing[0] = (nInt >> 24) & 0xff;
		thing[1] = (nInt >> 16) & 0xff;
		thing[2] = (nInt >> 8 ) & 0xff;
		thing[3] = (nInt >> 0 ) & 0xff;
		return res;
	}

	function objectContainsKeys(object) {
		var count = 0;
		for (var key in object) {
			count++;
			break;
		}
		return count != 0;
	}

	function HmacSHA256(key, input) {
		return window.textsecure.subtle.sign(key, input);
	}

	testing_only.privToPub = function(privKey, isIdentity) {
		if (privKey.byteLength != 32)
			throw new Error("Invalid private key");

		var prependVersion = function(pubKey) {
			var origPub = new Uint8Array(pubKey);
			var pub = new ArrayBuffer(33);
			var pubWithPrefix = new Uint8Array(pub);
			pubWithPrefix.set(origPub, 1);
			pubWithPrefix[0] = 5;
			return pub;
		}

		if (textsecure.nacl.USE_NACL) {
			return textsecure.nacl.postNaclMessage({command: "bytesToPriv", priv: privKey}).then(function(message) {
				var priv = message.res.slice(0, 32);
				if (!isIdentity)
					new Uint8Array(priv)[0] |= 0x01;
				return textsecure.nacl.postNaclMessage({command: "privToPub", priv: priv}).then(function(message) {
					return { pubKey: prependVersion(message.res.slice(0, 32)), privKey: priv };
				});
			});
		} else {
			privKey = privKey.slice(0);
			var priv = new Uint16Array(privKey);
			priv[0] &= 0xFFF8;
			priv[15] = (priv[15] & 0x7FFF) | 0x4000;

			if (!isIdentity)
				priv[0] |= 0x0001;

			//TODO: fscking type conversion
			return Promise.resolve({ pubKey: prependVersion(toArrayBuffer(curve25519(priv))), privKey: privKey});
		}
	}
	var privToPub = function(privKey, isIdentity) { return testing_only.privToPub(privKey, isIdentity); }

	testing_only.createNewKeyPair = function(isIdentity) {
		return privToPub(getRandomBytes(32), isIdentity);
	}
	var createNewKeyPair = function(isIdentity) { return testing_only.createNewKeyPair(isIdentity); }

	/***************************
	 *** Key/session storage ***
	 ***************************/
	var crypto_storage = {};

	crypto_storage.putKeyPair = function(keyName, keyPair) {
		textsecure.storage.putEncrypted("25519Key" + keyName, keyPair);
	}

	crypto_storage.getNewStoredKeyPair = function(keyName, isIdentity) {
		return createNewKeyPair(isIdentity).then(function(keyPair) {
			crypto_storage.putKeyPair(keyName, keyPair);
			return keyPair;
		});
	}

	crypto_storage.getStoredKeyPair = function(keyName) {
		var res = textsecure.storage.getEncrypted("25519Key" + keyName);
		if (res === undefined)
			return undefined;
		return { pubKey: toArrayBuffer(res.pubKey), privKey: toArrayBuffer(res.privKey) };
	}

	crypto_storage.removeStoredKeyPair = function(keyName) {
		textsecure.storage.removeEncrypted("25519Key" + keyName);
	}

	crypto_storage.getIdentityKey = function() {
		return this.getStoredKeyPair("identityKey");
	}

	crypto_storage.saveSession = function(encodedNumber, session, registrationId) {
		var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
		if (device === undefined)
			device = { sessions: {}, encodedNumber: encodedNumber };

		if (registrationId !== undefined)
			device.registrationId = registrationId;

		crypto_storage.saveSessionAndDevice(device, session);
	}

	crypto_storage.saveSessionAndDevice = function(device, session) {
		if (device.sessions === undefined)
			device.sessions = {};
		var sessions = device.sessions;

		var doDeleteSession = false;
		if (session.indexInfo.closed == -1 || device.identityKey === undefined)
			device.identityKey = session.indexInfo.remoteIdentityKey;

		if (session.indexInfo.closed != -1) {
			doDeleteSession = (session.indexInfo.closed < (new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS));

			if (!doDeleteSession) {
				var keysLeft = false;
				for (var key in session) {
					if (key != "indexInfo" && key != "oldRatchetList" && key != "currentRatchet") {
						keysLeft = true;
						break;
					}
				}
				doDeleteSession = !keysLeft;
				console.log((doDeleteSession ? "Deleting " : "Not deleting ") + "closed session which has not yet timed out");
			} else
				console.log("Deleting closed session due to timeout (created at " + session.indexInfo.closed + ")");
		}

		if (doDeleteSession)
			delete sessions[getString(session.indexInfo.baseKey)];
		else
			sessions[getString(session.indexInfo.baseKey)] = session;

		var openSessionRemaining = false;
		for (var key in sessions)
			if (sessions[key].indexInfo.closed == -1)
				openSessionRemaining = true;
		if (!openSessionRemaining)
			try {
				delete device['registrationId'];
			} catch(_) {}

		textsecure.storage.devices.saveDeviceObject(device);
	}

	var getSessions = function(encodedNumber) {
		var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
		if (device === undefined || device.sessions === undefined)
			return undefined;
		return device.sessions;
	}

	crypto_storage.getOpenSession = function(encodedNumber) {
		var sessions = getSessions(encodedNumber);
		if (sessions === undefined)
			return undefined;

		for (var key in sessions)
			if (sessions[key].indexInfo.closed == -1)
				return sessions[key];
		return undefined;
	}

	crypto_storage.getSessionByRemoteEphemeralKey = function(encodedNumber, remoteEphemeralKey) {
		var sessions = getSessions(encodedNumber);
		if (sessions === undefined)
			return undefined;

		var searchKey = getString(remoteEphemeralKey);

		var openSession = undefined;
		for (var key in sessions) {
			if (sessions[key].indexInfo.closed == -1) {
				if (openSession !== undefined)
					throw new Error("Datastore inconsistensy: multiple open sessions for " + encodedNumber);
				openSession = sessions[key];
			}
			if (sessions[key][searchKey] !== undefined)
				return sessions[key];
		}
		if (openSession !== undefined)
			return openSession;

		return undefined;
	}

	crypto_storage.getSessionOrIdentityKeyByBaseKey = function(encodedNumber, baseKey) {
		var sessions = getSessions(encodedNumber);
		var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
		if (device === undefined)
			return undefined;

		var preferredSession = device.sessions && device.sessions[getString(baseKey)];
		if (preferredSession !== undefined)
			return preferredSession;

		if (device.identityKey !== undefined)
			return { indexInfo: { remoteIdentityKey: device.identityKey } };

		throw new Error("Datastore inconsistency: device was stored without identity key");
	}

	/*****************************
	 *** Internal Crypto stuff ***
	 *****************************/
	var validatePubKeyFormat = function(pubKey, needVersionByte) {
		if (pubKey === undefined || ((pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) && pubKey.byteLength != 32))
			throw new Error("Invalid public key");
		if (pubKey.byteLength == 33) {
			if (!needVersionByte)
				return pubKey.slice(1);
			else
				return pubKey;
		} else {
			console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
			if (!needVersionByte)
				return pubKey;
			var res = new Uint8Array(33);
			res[0] = 5;
			res.set(new Uint8Array(pubKey), 1);
			return res.buffer;
		}
	}

	testing_only.ECDHE = function(pubKey, privKey) {
		if (privKey === undefined || privKey.byteLength != 32)
			throw new Error("Invalid private key");

		pubKey = validatePubKeyFormat(pubKey, false);

		return new Promise(function(resolve) {
			if (textsecure.nacl.USE_NACL) {
				textsecure.nacl.postNaclMessage({command: "ECDHE", priv: privKey, pub: pubKey}).then(function(message) {
					resolve(message.res.slice(0, 32));
				});
			} else {
				resolve(toArrayBuffer(curve25519(new Uint16Array(privKey), new Uint16Array(pubKey))));
			}
		});
	}
	var ECDHE = function(pubKey, privKey) { return testing_only.ECDHE(pubKey, privKey); }

	testing_only.Ed25519Sign = function(privKey, message) {
		if (privKey === undefined || privKey.byteLength != 32)
			throw new Error("Invalid private key");

		if (message === undefined)
			throw new Error("Invalid message");

		if (textsecure.nacl.USE_NACL) {
			return textsecure.nacl.postNaclMessage({command: "Ed25519Sign", priv: privKey, msg: message}).then(function(message) {
				return message.res;
			});
		} else {
			throw new Error("Ed25519 in JS not yet supported");
		}
	}
	var Ed25519Sign = function(privKey, pubKeyToSign) {
		pubKeyToSign = validatePubKeyFormat(pubKeyToSign, true);
		return testing_only.Ed25519Sign(privKey, pubKeyToSign);
	}

	testing_only.Ed25519Verify = function(pubKey, msg, sig) {
		pubKey = validatePubKeyFormat(pubKey, false);

		if (msg === undefined)
			throw new Error("Invalid message");

		if (sig === undefined || sig.byteLength != 64)
			throw new Error("Invalid signature");

		if (textsecure.nacl.USE_NACL) {
			return textsecure.nacl.postNaclMessage({command: "Ed25519Verify", pub: pubKey, msg: msg, sig: sig}).then(function(message) {
				if (!message.res)
					throw new Error("Invalid signature");
			});
		} else {
			throw new Error("Ed25519 in JS not yet supported");
		}
	}
	var Ed25519Verify = function(pubKey, signedPubKey, sig) {
		signedPubKey = validatePubKeyFormat(signedPubKey, true);
		return testing_only.Ed25519Verify(pubKey, signedPubKey, sig);
	}

	testing_only.HKDF = function(input, salt, info) {
		// Specific implementation of RFC 5869 that only returns the first 3 32-byte chunks
		// TODO: We dont always need the third chunk, we might skip it
		return HmacSHA256(salt, input).then(function(PRK) {
			var infoBuffer = new ArrayBuffer(info.byteLength + 1 + 32);
			var infoArray = new Uint8Array(infoBuffer);
			infoArray.set(new Uint8Array(info), 32);
			infoArray[infoArray.length - 1] = 1;
			return HmacSHA256(PRK, infoBuffer.slice(32)).then(function(T1) {
				infoArray.set(new Uint8Array(T1));
				infoArray[infoArray.length - 1] = 2;
				return HmacSHA256(PRK, infoBuffer).then(function(T2) {
					infoArray.set(new Uint8Array(T2));
					infoArray[infoArray.length - 1] = 3;
					return HmacSHA256(PRK, infoBuffer).then(function(T3) {
						return [ T1, T2, T3 ];
					});
				});
			});
		});
	}

	var HKDF = function(input, salt, info) {
		// HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes
		if (salt == '')
			salt = new ArrayBuffer(32);
		if (salt.byteLength != 32)
			throw new Error("Got salt of incorrect length");

		info = toArrayBuffer(info); // TODO: maybe convert calls?

		return testing_only.HKDF(input, salt, info);
	}

	var calculateMACWithVersionByte = function(data, key, version) {
		if (version === undefined)
			version = 1;

		var prependedData = new Uint8Array(data.byteLength + 1);
		prependedData[0] = version;
		prependedData.set(new Uint8Array(data), 1);

		return HmacSHA256(key, prependedData.buffer);
	}

	var verifyMACWithVersionByte = function(data, key, mac, version) {
		return calculateMACWithVersionByte(data, key, version).then(function(calculated_mac) {
			if (!isEqual(calculated_mac, mac, true))
				throw new Error("Bad MAC");
		});
	}

	var verifyMAC = function(data, key, mac) {
		return HmacSHA256(key, data).then(function(calculated_mac) {
			if (!isEqual(calculated_mac, mac, true))
				throw new Error("Bad MAC");<commit_msg>Remove unused *MACWithVersionByte functions<commit_after>/* vim: ts=4:sw=4
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

window.textsecure = window.textsecure || {};

window.textsecure.crypto = function() {
	'use strict';

	var self = {};
	// functions exposed for replacement and direct calling in test code
	var testing_only = {};

	/******************************
	 *** Random constants/utils ***
	 ******************************/
	// We consider messages lost after a week and might throw away keys at that point
	// (also the time between signedPreKey regenerations)
	var MESSAGE_LOST_THRESHOLD_MS = 1000*60*60*24*7;

	var getRandomBytes = function(size) {
		// At some point we might consider XORing in hashes of random
		// UI events to strengthen ourselves against RNG flaws in crypto.getRandomValues
		// ie maybe take a look at how Gibson does it at https://www.grc.com/r&d/js.htm
		var array = new Uint8Array(size);
		window.crypto.getRandomValues(array);
		return array.buffer;
	}
	self.getRandomBytes = getRandomBytes;

	function intToArrayBuffer(nInt) {
		var res = new ArrayBuffer(16);
		var thing = new Uint8Array(res);
		thing[0] = (nInt >> 24) & 0xff;
		thing[1] = (nInt >> 16) & 0xff;
		thing[2] = (nInt >> 8 ) & 0xff;
		thing[3] = (nInt >> 0 ) & 0xff;
		return res;
	}

	function objectContainsKeys(object) {
		var count = 0;
		for (var key in object) {
			count++;
			break;
		}
		return count != 0;
	}

	function HmacSHA256(key, input) {
		return window.textsecure.subtle.sign(key, input);
	}

	testing_only.privToPub = function(privKey, isIdentity) {
		if (privKey.byteLength != 32)
			throw new Error("Invalid private key");

		var prependVersion = function(pubKey) {
			var origPub = new Uint8Array(pubKey);
			var pub = new ArrayBuffer(33);
			var pubWithPrefix = new Uint8Array(pub);
			pubWithPrefix.set(origPub, 1);
			pubWithPrefix[0] = 5;
			return pub;
		}

		if (textsecure.nacl.USE_NACL) {
			return textsecure.nacl.postNaclMessage({command: "bytesToPriv", priv: privKey}).then(function(message) {
				var priv = message.res.slice(0, 32);
				if (!isIdentity)
					new Uint8Array(priv)[0] |= 0x01;
				return textsecure.nacl.postNaclMessage({command: "privToPub", priv: priv}).then(function(message) {
					return { pubKey: prependVersion(message.res.slice(0, 32)), privKey: priv };
				});
			});
		} else {
			privKey = privKey.slice(0);
			var priv = new Uint16Array(privKey);
			priv[0] &= 0xFFF8;
			priv[15] = (priv[15] & 0x7FFF) | 0x4000;

			if (!isIdentity)
				priv[0] |= 0x0001;

			//TODO: fscking type conversion
			return Promise.resolve({ pubKey: prependVersion(toArrayBuffer(curve25519(priv))), privKey: privKey});
		}
	}
	var privToPub = function(privKey, isIdentity) { return testing_only.privToPub(privKey, isIdentity); }

	testing_only.createNewKeyPair = function(isIdentity) {
		return privToPub(getRandomBytes(32), isIdentity);
	}
	var createNewKeyPair = function(isIdentity) { return testing_only.createNewKeyPair(isIdentity); }

	/***************************
	 *** Key/session storage ***
	 ***************************/
	var crypto_storage = {};

	crypto_storage.putKeyPair = function(keyName, keyPair) {
		textsecure.storage.putEncrypted("25519Key" + keyName, keyPair);
	}

	crypto_storage.getNewStoredKeyPair = function(keyName, isIdentity) {
		return createNewKeyPair(isIdentity).then(function(keyPair) {
			crypto_storage.putKeyPair(keyName, keyPair);
			return keyPair;
		});
	}

	crypto_storage.getStoredKeyPair = function(keyName) {
		var res = textsecure.storage.getEncrypted("25519Key" + keyName);
		if (res === undefined)
			return undefined;
		return { pubKey: toArrayBuffer(res.pubKey), privKey: toArrayBuffer(res.privKey) };
	}

	crypto_storage.removeStoredKeyPair = function(keyName) {
		textsecure.storage.removeEncrypted("25519Key" + keyName);
	}

	crypto_storage.getIdentityKey = function() {
		return this.getStoredKeyPair("identityKey");
	}

	crypto_storage.saveSession = function(encodedNumber, session, registrationId) {
		var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
		if (device === undefined)
			device = { sessions: {}, encodedNumber: encodedNumber };

		if (registrationId !== undefined)
			device.registrationId = registrationId;

		crypto_storage.saveSessionAndDevice(device, session);
	}

	crypto_storage.saveSessionAndDevice = function(device, session) {
		if (device.sessions === undefined)
			device.sessions = {};
		var sessions = device.sessions;

		var doDeleteSession = false;
		if (session.indexInfo.closed == -1 || device.identityKey === undefined)
			device.identityKey = session.indexInfo.remoteIdentityKey;

		if (session.indexInfo.closed != -1) {
			doDeleteSession = (session.indexInfo.closed < (new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS));

			if (!doDeleteSession) {
				var keysLeft = false;
				for (var key in session) {
					if (key != "indexInfo" && key != "oldRatchetList" && key != "currentRatchet") {
						keysLeft = true;
						break;
					}
				}
				doDeleteSession = !keysLeft;
				console.log((doDeleteSession ? "Deleting " : "Not deleting ") + "closed session which has not yet timed out");
			} else
				console.log("Deleting closed session due to timeout (created at " + session.indexInfo.closed + ")");
		}

		if (doDeleteSession)
			delete sessions[getString(session.indexInfo.baseKey)];
		else
			sessions[getString(session.indexInfo.baseKey)] = session;

		var openSessionRemaining = false;
		for (var key in sessions)
			if (sessions[key].indexInfo.closed == -1)
				openSessionRemaining = true;
		if (!openSessionRemaining)
			try {
				delete device['registrationId'];
			} catch(_) {}

		textsecure.storage.devices.saveDeviceObject(device);
	}

	var getSessions = function(encodedNumber) {
		var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
		if (device === undefined || device.sessions === undefined)
			return undefined;
		return device.sessions;
	}

	crypto_storage.getOpenSession = function(encodedNumber) {
		var sessions = getSessions(encodedNumber);
		if (sessions === undefined)
			return undefined;

		for (var key in sessions)
			if (sessions[key].indexInfo.closed == -1)
				return sessions[key];
		return undefined;
	}

	crypto_storage.getSessionByRemoteEphemeralKey = function(encodedNumber, remoteEphemeralKey) {
		var sessions = getSessions(encodedNumber);
		if (sessions === undefined)
			return undefined;

		var searchKey = getString(remoteEphemeralKey);

		var openSession = undefined;
		for (var key in sessions) {
			if (sessions[key].indexInfo.closed == -1) {
				if (openSession !== undefined)
					throw new Error("Datastore inconsistensy: multiple open sessions for " + encodedNumber);
				openSession = sessions[key];
			}
			if (sessions[key][searchKey] !== undefined)
				return sessions[key];
		}
		if (openSession !== undefined)
			return openSession;

		return undefined;
	}

	crypto_storage.getSessionOrIdentityKeyByBaseKey = function(encodedNumber, baseKey) {
		var sessions = getSessions(encodedNumber);
		var device = textsecure.storage.devices.getDeviceObject(encodedNumber);
		if (device === undefined)
			return undefined;

		var preferredSession = device.sessions && device.sessions[getString(baseKey)];
		if (preferredSession !== undefined)
			return preferredSession;

		if (device.identityKey !== undefined)
			return { indexInfo: { remoteIdentityKey: device.identityKey } };

		throw new Error("Datastore inconsistency: device was stored without identity key");
	}

	/*****************************
	 *** Internal Crypto stuff ***
	 *****************************/
	var validatePubKeyFormat = function(pubKey, needVersionByte) {
		if (pubKey === undefined || ((pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) && pubKey.byteLength != 32))
			throw new Error("Invalid public key");
		if (pubKey.byteLength == 33) {
			if (!needVersionByte)
				return pubKey.slice(1);
			else
				return pubKey;
		} else {
			console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey");
			if (!needVersionByte)
				return pubKey;
			var res = new Uint8Array(33);
			res[0] = 5;
			res.set(new Uint8Array(pubKey), 1);
			return res.buffer;
		}
	}

	testing_only.ECDHE = function(pubKey, privKey) {
		if (privKey === undefined || privKey.byteLength != 32)
			throw new Error("Invalid private key");

		pubKey = validatePubKeyFormat(pubKey, false);

		return new Promise(function(resolve) {
			if (textsecure.nacl.USE_NACL) {
				textsecure.nacl.postNaclMessage({command: "ECDHE", priv: privKey, pub: pubKey}).then(function(message) {
					resolve(message.res.slice(0, 32));
				});
			} else {
				resolve(toArrayBuffer(curve25519(new Uint16Array(privKey), new Uint16Array(pubKey))));
			}
		});
	}
	var ECDHE = function(pubKey, privKey) { return testing_only.ECDHE(pubKey, privKey); }

	testing_only.Ed25519Sign = function(privKey, message) {
		if (privKey === undefined || privKey.byteLength != 32)
			throw new Error("Invalid private key");

		if (message === undefined)
			throw new Error("Invalid message");

		if (textsecure.nacl.USE_NACL) {
			return textsecure.nacl.postNaclMessage({command: "Ed25519Sign", priv: privKey, msg: message}).then(function(message) {
				return message.res;
			});
		} else {
			throw new Error("Ed25519 in JS not yet supported");
		}
	}
	var Ed25519Sign = function(privKey, pubKeyToSign) {
		pubKeyToSign = validatePubKeyFormat(pubKeyToSign, true);
		return testing_only.Ed25519Sign(privKey, pubKeyToSign);
	}

	testing_only.Ed25519Verify = function(pubKey, msg, sig) {
		pubKey = validatePubKeyFormat(pubKey, false);

		if (msg === undefined)
			throw new Error("Invalid message");

		if (sig === undefined || sig.byteLength != 64)
			throw new Error("Invalid signature");

		if (textsecure.nacl.USE_NACL) {
			return textsecure.nacl.postNaclMessage({command: "Ed25519Verify", pub: pubKey, msg: msg, sig: sig}).then(function(message) {
				if (!message.res)
					throw new Error("Invalid signature");
			});
		} else {
			throw new Error("Ed25519 in JS not yet supported");
		}
	}
	var Ed25519Verify = function(pubKey, signedPubKey, sig) {
		signedPubKey = validatePubKeyFormat(signedPubKey, true);
		return testing_only.Ed25519Verify(pubKey, signedPubKey, sig);
	}

	testing_only.HKDF = function(input, salt, info) {
		// Specific implementation of RFC 5869 that only returns the first 3 32-byte chunks
		// TODO: We dont always need the third chunk, we might skip it
		return HmacSHA256(salt, input).then(function(PRK) {
			var infoBuffer = new ArrayBuffer(info.byteLength + 1 + 32);
			var infoArray = new Uint8Array(infoBuffer);
			infoArray.set(new Uint8Array(info), 32);
			infoArray[infoArray.length - 1] = 1;
			return HmacSHA256(PRK, infoBuffer.slice(32)).then(function(T1) {
				infoArray.set(new Uint8Array(T1));
				infoArray[infoArray.length - 1] = 2;
				return HmacSHA256(PRK, infoBuffer).then(function(T2) {
					infoArray.set(new Uint8Array(T2));
					infoArray[infoArray.length - 1] = 3;
					return HmacSHA256(PRK, infoBuffer).then(function(T3) {
						return [ T1, T2, T3 ];
					});
				});
			});
		});
	}

	var HKDF = function(input, salt, info) {
		// HKDF for TextSecure has a bit of additional handling - salts always end up being 32 bytes
		if (salt == '')
			salt = new ArrayBuffer(32);
		if (salt.byteLength != 32)
			throw new Error("Got salt of incorrect length");

		info = toArrayBuffer(info); // TODO: maybe convert calls?

		return testing_only.HKDF(input, salt, info);
	}

	var verifyMAC = function(data, key, mac) {
		return HmacSHA256(key, data).then(function(calculated_mac) {
			if (!isEqual(calculated_mac, mac, true))
				throw new Error("Bad MAC");
{'fraction_non_alphanumeric': 0.1349568948392992, 'fraction_numerical': 0.019983314131341624, 'mean_word_length': 4.290458175704077, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 21, 'https://': 2, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5139475', 'n_tokens_mistral': 9177, 'n_tokens_neox': 7837, 'n_words': 2019}