From 9f676af9bbd8da58b04723bcfa0594207f199310 Mon Sep 17 00:00:00 2001 From: lilia Date: Tue, 4 Nov 2014 14:59:48 -0800 Subject: [PATCH] Refactor crypto.js and native client interface NB: this diff is best viewed with --ignore-whitespace Distills crypto.js down to the hard cryptoey bones. It pulls from webcrypto for aes and hmac, and from native client for curve25519 stuff or potentially another object implementing the handful of needed curve25519 functions. Everything else formerly known as crypto, including session storage and management, axolotl, etc.. is now protocol.js. The separation is not quite perfect, but it's a big step. nativeclient.js now enables talking to the native client module through a high level interface as well as registering callbacks that will be executed once the module is loaded. And it has tests! Finally, this commit removes all references to the "testing_only" object, preferring to run tests on textsecure.crypto instead. --- background.html | 4 +- index.html | 4 + js/crypto.js | 1063 +++++-------------------------------- js/helpers.js | 73 +-- js/nativeclient.js | 85 +++ js/options.js | 2 +- js/protocol.js | 807 ++++++++++++++++++++++++++++ js/sendmessage.js | 6 +- js/webcrypto.js | 18 - options.html | 2 + test/_test.js | 41 ++ test/crypto_test.js | 51 +- test/index.html | 5 +- test/nativeclient_test.js | 96 ++++ test/test.js | 41 ++ test/testvectors.js | 7 - 16 files changed, 1255 insertions(+), 1050 deletions(-) create mode 100644 js/nativeclient.js create mode 100644 js/protocol.js create mode 100644 test/nativeclient_test.js diff --git a/background.html b/background.html index 1b02fc0d298..b7ef10166e3 100644 --- a/background.html +++ b/background.html @@ -21,13 +21,15 @@ + - + + diff --git a/index.html b/index.html index 98488b08192..ceb9e93a49e 100644 --- a/index.html +++ b/index.html @@ -131,13 +131,17 @@ + + + + diff --git a/js/crypto.js b/js/crypto.js index e2b9554612c..be06af99bd9 100644 --- a/js/crypto.js +++ b/js/crypto.js @@ -13,940 +13,129 @@ * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ - -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 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); - 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; - - //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"); - }); - } - - /****************************** - *** Ratchet implementation *** - ******************************/ - var calculateRatchet = function(session, remoteKey, sending) { - var ratchet = session.currentRatchet; - - return ECDHE(remoteKey, toArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) { - return HKDF(sharedSecret, toArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) { - if (sending) - session[getString(ratchet.ephemeralKeyPair.pubKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } }; - else - session[getString(remoteKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } }; - ratchet.rootKey = masterKey[0]; - }); - }); - } - - var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) { - var ourIdentityKey = crypto_storage.getIdentityKey(); - - if (isInitiator) { - if (ourSignedKey !== undefined) - throw new Error("Invalid call to initSession"); - ourSignedKey = ourEphemeralKey; - } else { - if (theirSignedPubKey !== undefined) - throw new Error("Invalid call to initSession"); - theirSignedPubKey = theirEphemeralPubKey; - } - - var sharedSecret; - if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) - sharedSecret = new Uint8Array(32 * 4); - else - sharedSecret = new Uint8Array(32 * 5); - - for (var i = 0; i < 32; i++) - sharedSecret[i] = 0xff; - - return ECDHE(theirSignedPubKey, ourIdentityKey.privKey).then(function(ecRes1) { - function finishInit() { - return ECDHE(theirSignedPubKey, ourSignedKey.privKey).then(function(ecRes) { - sharedSecret.set(new Uint8Array(ecRes), 32 * 3); - - return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) { - var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey, previousCounter: 0 }, - indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 }, - oldRatchetList: [] - }; - if (!isInitiator) - session.indexInfo.baseKey = theirEphemeralPubKey; - else - session.indexInfo.baseKey = ourEphemeralKey.pubKey; - - // If we're initiating we go ahead and set our first sending ephemeral key now, - // otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key - if (isInitiator) { - return createNewKeyPair(false).then(function(ourSendingEphemeralKey) { - session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey; - return calculateRatchet(session, theirSignedPubKey, true).then(function() { - return session; - }); - }); - } else { - session.currentRatchet.ephemeralKeyPair = ourSignedKey; - return session; - } - }); - }); - } - - var promise; - if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) - promise = Promise.resolve(new ArrayBuffer(0)); - else - promise = ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey); - return promise.then(function(ecRes4) { - sharedSecret.set(new Uint8Array(ecRes4), 32 * 4); - - if (isInitiator) - return ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { - sharedSecret.set(new Uint8Array(ecRes1), 32); - sharedSecret.set(new Uint8Array(ecRes2), 32 * 2); - }).then(finishInit); - else - return ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { - sharedSecret.set(new Uint8Array(ecRes1), 32 * 2); - sharedSecret.set(new Uint8Array(ecRes2), 32) - }).then(finishInit); - }); - }); - } - - var removeOldChains = function(session) { - // Sending ratchets are always removed when we step because we never need them again - // Receiving ratchets are either removed if we step with all keys used up to previousCounter - // and are otherwise added to the oldRatchetList, which we parse here and remove ratchets - // older than a week (we assume the message was lost and move on with our lives at that point) - var newList = []; - for (var i = 0; i < session.oldRatchetList.length; i++) { - var entry = session.oldRatchetList[i]; - var ratchet = getString(entry.ephemeralKey); - console.log("Checking old chain with added time " + (entry.added/1000)); - if ((!objectContainsKeys(session[ratchet].messageKeys) && (session[ratchet].chainKey === undefined || session[ratchet].chainKey.key === undefined)) - || entry.added < new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS) { - delete session[ratchet]; - console.log("...deleted"); - } else - newList[newList.length] = entry; - } - session.oldRatchetList = newList; - } - - var closeSession = function(session, sessionClosedByRemote) { - if (session.indexInfo.closed > -1) - return; - - // After this has run, we can still receive messages on ratchet chains which - // were already open (unless we know we dont need them), - // but we cannot send messages or step the ratchet - - // Delete current sending ratchet - delete session[getString(session.currentRatchet.ephemeralKeyPair.pubKey)]; - // Move all receive ratchets to the oldRatchetList to mark them for deletion - for (var i in session) { - if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) { - if (!sessionClosedByRemote) - session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: i }; - else - delete session[i].chainKey.key; - } - } - // Delete current root key and our ephemeral key pair to disallow ratchet stepping - delete session.currentRatchet['rootKey']; - delete session.currentRatchet['ephemeralKeyPair']; - session.indexInfo.closed = new Date().getTime(); - removeOldChains(session); - } - - self.closeOpenSessionForDevice = function(encodedNumber) { - var session = crypto_storage.getOpenSession(encodedNumber); - if (session === undefined) - return; - - closeSession(session); - crypto_storage.saveSession(encodedNumber, session); - } - - var initSessionFromPreKeyWhisperMessage; - var decryptWhisperMessage; - var handlePreKeyWhisperMessage = function(from, encodedMessage) { - var preKeyProto = textsecure.protobuf.PreKeyWhisperMessage.decode(encodedMessage, 'binary'); - return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) { - return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) { - if (sessions[1] !== undefined) - sessions[1](); - return result; - }); - }); - } - - var wipeIdentityAndTryMessageAgain = function(from, encodedMessage) { - //TODO: Wipe identity key! - return handlePreKeyWhisperMessage(from, encodedMessage); - } - textsecure.replay.registerReplayFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.REPLAY_FUNCS.INIT_SESSION); - - initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) { - var preKeyPair = crypto_storage.getStoredKeyPair("preKey" + message.preKeyId); - var signedPreKeyPair = crypto_storage.getStoredKeyPair("signedKey" + message.signedPreKeyId); - - var session = crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, toArrayBuffer(message.baseKey)); - var open_session = crypto_storage.getOpenSession(encodedNumber); - if (signedPreKeyPair === undefined) { - // Session may or may not be the right one, but if its not, we can't do anything about it - // ...fall through and let decryptWhisperMessage handle that case - if (session !== undefined && session.currentRatchet !== undefined) - return Promise.resolve([session, undefined]); - else - throw new Error("Missing Signed PreKey for PreKeyWhisperMessage"); - } - if (session !== undefined) { - // Duplicate PreKeyMessage for session: - if (isEqual(session.indexInfo.baseKey, message.baseKey, false)) - return Promise.resolve([session, undefined]); - - // We already had a session/known identity key: - if (isEqual(session.indexInfo.remoteIdentityKey, message.identityKey, false)) { - // If the identity key matches the previous one, close the previous one and use the new one - if (open_session !== undefined) - closeSession(open_session); // To be returned and saved later - } else { - // ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate - throw new Error("Received message with unknown identity key", "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure.", textsecure.replay.REPLAY_FUNCS.INIT_SESSION, [encodedNumber, getString(message.encode())]); - } - } - return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey), undefined) - .then(function(new_session) { - // Note that the session is not actually saved until the very end of decryptWhisperMessage - // ... to ensure that the sender actually holds the private keys for all reported pubkeys - return [new_session, function() { - if (open_session !== undefined) - crypto_storage.saveSession(encodedNumber, open_session); - crypto_storage.removeStoredKeyPair("preKey" + message.preKeyId); - }]; - });; - } - - var fillMessageKeys = function(chain, counter) { - if (chain.chainKey.counter + 1000 < counter) //TODO: maybe 1000 is too low/high in some cases? - return Promise.resolve(); // Stalker, much? - - if (chain.chainKey.counter >= counter) - return Promise.resolve(); // Already calculated - - if (chain.chainKey.key === undefined) - throw new Error("Got invalid request to extend chain after it was already closed"); - - var key = toArrayBuffer(chain.chainKey.key); - var byteArray = new Uint8Array(1); - byteArray[0] = 1; - return HmacSHA256(key, byteArray.buffer).then(function(mac) { - byteArray[0] = 2; - return HmacSHA256(key, byteArray.buffer).then(function(key) { - chain.messageKeys[chain.chainKey.counter + 1] = mac; - chain.chainKey.key = key - chain.chainKey.counter += 1; - return fillMessageKeys(chain, counter); - }); - }); - } - - var maybeStepRatchet = function(session, remoteKey, previousCounter) { - if (session[getString(remoteKey)] !== undefined) - return Promise.resolve(); - - var ratchet = session.currentRatchet; - - var finish = function() { - return calculateRatchet(session, remoteKey, false).then(function() { - // Now swap the ephemeral key and calculate the new sending chain - var previousRatchet = getString(ratchet.ephemeralKeyPair.pubKey); - if (session[previousRatchet] !== undefined) { - ratchet.previousCounter = session[previousRatchet].chainKey.counter; - delete session[previousRatchet]; - } - - return createNewKeyPair(false).then(function(keyPair) { - ratchet.ephemeralKeyPair = keyPair; - return calculateRatchet(session, remoteKey, true).then(function() { - ratchet.lastRemoteEphemeralKey = remoteKey; - }); - }); - }); - } - - var previousRatchet = session[getString(ratchet.lastRemoteEphemeralKey)]; - if (previousRatchet !== undefined) { - return fillMessageKeys(previousRatchet, previousCounter).then(function() { - delete previousRatchet.chainKey.key; - if (!objectContainsKeys(previousRatchet.messageKeys)) - delete session[getString(ratchet.lastRemoteEphemeralKey)]; - else - session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: ratchet.lastRemoteEphemeralKey }; - }).then(finish); - } else - return finish(); - } - - // returns decrypted protobuf - decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) { - if (messageBytes[0] != String.fromCharCode((3 << 4) | 3)) - throw new Error("Bad version number on WhisperMessage"); - - var messageProto = messageBytes.substring(1, messageBytes.length - 8); - var mac = messageBytes.substring(messageBytes.length - 8, messageBytes.length); - - var message = textsecure.protobuf.WhisperMessage.decode(messageProto, 'binary'); - var remoteEphemeralKey = toArrayBuffer(message.ephemeralKey); - - if (session === undefined) { - var session = crypto_storage.getSessionByRemoteEphemeralKey(encodedNumber, remoteEphemeralKey); - if (session === undefined) - throw new Error("No session found to decrypt message from " + encodedNumber); - } - - return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() { - var chain = session[getString(message.ephemeralKey)]; - - return fillMessageKeys(chain, message.counter).then(function() { - return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) { - delete chain.messageKeys[message.counter]; - - var messageProtoArray = toArrayBuffer(messageProto); - var macInput = new Uint8Array(messageProtoArray.byteLength + 33*2 + 1); - macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey))); - macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey)), 33); - macInput[33*2] = (3 << 4) | 3; - macInput.set(new Uint8Array(messageProtoArray), 33*2 + 1); - - return verifyMAC(macInput.buffer, keys[1], mac).then(function() { - return window.textsecure.subtle.decrypt(keys[0], toArrayBuffer(message.ciphertext), keys[2].slice(0, 16)) - .then(function(paddedPlaintext) { - - paddedPlaintext = new Uint8Array(paddedPlaintext); - var plaintext; - for (var i = paddedPlaintext.length - 1; i >= 0; i--) { - if (paddedPlaintext[i] == 0x80) { - plaintext = new Uint8Array(i); - plaintext.set(paddedPlaintext.subarray(0, i)); - plaintext = plaintext.buffer; - break; - } else if (paddedPlaintext[i] != 0x00) - throw new Error('Invalid padding'); - } - - delete session['pendingPreKey']; - - var finalMessage = textsecure.protobuf.PushMessageContent.decode(plaintext); - - if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) - == textsecure.protobuf.PushMessageContent.Flags.END_SESSION) - closeSession(session, true); - - removeOldChains(session); - - crypto_storage.saveSession(encodedNumber, session, registrationId); - return finalMessage; - }); - }); - }); - }); - }); - } - - /************************* - *** Public crypto API *** - *************************/ - // Decrypts message into a raw string - self.decryptWebsocketMessage = function(message) { - var signaling_key = textsecure.storage.getEncrypted("signaling_key"); //TODO: in crypto_storage - var aes_key = toArrayBuffer(signaling_key.substring(0, 32)); - var mac_key = toArrayBuffer(signaling_key.substring(32, 32 + 20)); - - var decodedMessage = base64DecToArr(getString(message)); - if (new Uint8Array(decodedMessage)[0] != 1) - throw new Error("Got bad version number: " + decodedMessage[0]); - - var iv = decodedMessage.slice(1, 1 + 16); - var ciphertext = decodedMessage.slice(1 + 16, decodedMessage.byteLength - 10); - var ivAndCiphertext = decodedMessage.slice(0, decodedMessage.byteLength - 10); - var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength); - - return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() { - return window.textsecure.subtle.decrypt(aes_key, ciphertext, iv); - }); - }; - - self.decryptAttachment = function(encryptedBin, keys) { - var aes_key = keys.slice(0, 32); - var mac_key = keys.slice(32, 64); - - var iv = encryptedBin.slice(0, 16); - var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); - var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); - var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength); - - return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() { - return window.textsecure.subtle.decrypt(aes_key, ciphertext, iv); - }); - }; - - self.encryptAttachment = function(plaintext, keys, iv) { - var aes_key = keys.slice(0, 32); - var mac_key = keys.slice(32, 64); - - return window.textsecure.subtle.encrypt(aes_key, plaintext, iv).then(function(ciphertext) { - var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); - ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set(new Uint8Array(ciphertext), 16); - - return HmacSHA256(mac_key, ivAndCiphertext.buffer).then(function(mac) { - var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32); - encryptedBin.set(ivAndCiphertext); - encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); - return encryptedBin.buffer; - }); - }); - }; - - self.handleIncomingPushMessageProto = function(proto) { - switch(proto.type) { - case textsecure.protobuf.IncomingPushMessageSignal.Type.PLAINTEXT: - return Promise.resolve(textsecure.protobuf.PushMessageContent.decode(proto.message)); - case textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT: - var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice); - return decryptWhisperMessage(from, getString(proto.message)); - case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE: - if (proto.message.readUint8() != ((3 << 4) | 3)) - throw new Error("Bad version byte"); - var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice); - return handlePreKeyWhisperMessage(from, getString(proto.message)); - case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT: - return Promise.resolve(null); - default: - return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); }); - } - } - - // return Promise(encoded [PreKey]WhisperMessage) - self.encryptMessageFor = function(deviceObject, pushMessageContent) { - var session = crypto_storage.getOpenSession(deviceObject.encodedNumber); - - var doEncryptPushMessageContent = function() { - var msg = new textsecure.protobuf.WhisperMessage(); - var plaintext = toArrayBuffer(pushMessageContent.encode()); - - var paddedPlaintext = new Uint8Array(Math.ceil((plaintext.byteLength + 1) / 160.0) * 160); - paddedPlaintext.set(new Uint8Array(plaintext)); - paddedPlaintext[plaintext.byteLength] = 0x80; - - msg.ephemeralKey = toArrayBuffer(session.currentRatchet.ephemeralKeyPair.pubKey); - var chain = session[getString(msg.ephemeralKey)]; - - return fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() { - return HKDF(toArrayBuffer(chain.messageKeys[chain.chainKey.counter]), '', "WhisperMessageKeys").then(function(keys) { - delete chain.messageKeys[chain.chainKey.counter]; - msg.counter = chain.chainKey.counter; - msg.previousCounter = session.currentRatchet.previousCounter; - - return window.textsecure.subtle.encrypt(keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)).then(function(ciphertext) { - msg.ciphertext = ciphertext; - var encodedMsg = toArrayBuffer(msg.encode()); - - var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1); - macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey))); - macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33); - macInput[33*2] = (3 << 4) | 3; - macInput.set(new Uint8Array(encodedMsg), 33*2 + 1); - - return HmacSHA256(keys[1], macInput.buffer).then(function(mac) { - var result = new Uint8Array(encodedMsg.byteLength + 9); - result[0] = (3 << 4) | 3; - result.set(new Uint8Array(encodedMsg), 1); - result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1); - - try { - delete deviceObject['signedKey']; - delete deviceObject['signedKeyId']; - delete deviceObject['preKey']; - delete deviceObject['preKeyId']; - } catch(_) {} - - removeOldChains(session); - - crypto_storage.saveSessionAndDevice(deviceObject, session); - return result; - }); - }); - }); - }); - } - - var preKeyMsg = new textsecure.protobuf.PreKeyWhisperMessage(); - preKeyMsg.identityKey = toArrayBuffer(crypto_storage.getIdentityKey().pubKey); - preKeyMsg.registrationId = textsecure.storage.getUnencrypted("registrationId"); - - if (session === undefined) { - return createNewKeyPair(false).then(function(baseKey) { - preKeyMsg.preKeyId = deviceObject.preKeyId; - preKeyMsg.signedPreKeyId = deviceObject.signedKeyId; - preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey); - return initSession(true, baseKey, undefined, deviceObject.encodedNumber, - toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.preKey), toArrayBuffer(deviceObject.signedKey)) - .then(function(new_session) { - session = new_session; - session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey }; - return doEncryptPushMessageContent().then(function(message) { - preKeyMsg.message = message; - var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode()); - return {type: 3, body: result}; - }); - }); - }); - } else - return doEncryptPushMessageContent().then(function(message) { - if (session.pendingPreKey !== undefined) { - preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey.baseKey); - preKeyMsg.preKeyId = session.pendingPreKey.preKeyId; - preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId; - preKeyMsg.message = message; - - var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode()); - return {type: 3, body: result}; - } else - return {type: 1, body: getString(message)}; - }); - } - - var GENERATE_KEYS_KEYS_GENERATED = 100; - self.generateKeys = function() { - var identityKeyPair = crypto_storage.getIdentityKey(); - var identityKeyCalculated = function(identityKeyPair) { - var firstPreKeyId = textsecure.storage.getEncrypted("maxPreKeyId", 0); - textsecure.storage.putEncrypted("maxPreKeyId", firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED); - - var signedKeyId = textsecure.storage.getEncrypted("signedKeyId", 0); - textsecure.storage.putEncrypted("signedKeyId", signedKeyId + 1); - - var keys = {}; - keys.identityKey = identityKeyPair.pubKey; - keys.preKeys = []; - - var generateKey = function(keyId) { - return crypto_storage.getNewStoredKeyPair("preKey" + keyId, false).then(function(keyPair) { - keys.preKeys[keyId] = {keyId: keyId, publicKey: keyPair.pubKey}; - }); - }; - - var promises = []; - for (var i = firstPreKeyId; i < firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED; i++) - promises[i] = generateKey(i); - - promises[firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED] = crypto_storage.getNewStoredKeyPair("signedKey" + signedKeyId).then(function(keyPair) { - return Ed25519Sign(identityKeyPair.privKey, keyPair.pubKey).then(function(sig) { - keys.signedPreKey = {keyId: signedKeyId, publicKey: keyPair.pubKey, signature: sig}; - }); - }); - - //TODO: Process by date added and agressively call generateKeys when we get near maxPreKeyId in a message - crypto_storage.removeStoredKeyPair("signedKey" + (signedKeyId - 2)); - - return Promise.all(promises).then(function() { - return keys; - }); - } - if (identityKeyPair === undefined) - return crypto_storage.getNewStoredKeyPair("identityKey", true).then(function(keyPair) { return identityKeyCalculated(keyPair); }); - else - return identityKeyCalculated(identityKeyPair); - } - - window.textsecure.registerOnLoadFunction(function() { - //TODO: Dont always update prekeys here - if (textsecure.storage.getEncrypted("lastSignedKeyUpdate", Date.now()) < Date.now() - MESSAGE_LOST_THRESHOLD_MS) - self.generateKeys(); - }); - - self.Ed25519Verify = Ed25519Verify; - - self.prepareTempWebsocket = function() { - var socketInfo = {}; - var keyPair; - - socketInfo.decryptAndHandleDeviceInit = function(deviceInit) { - var masterEphemeral = toArrayBuffer(deviceInit.masterEphemeralPubKey); - var message = toArrayBuffer(deviceInit.identityKeyMessage); - - return ECDHE(masterEphemeral, keyPair.privKey).then(function(ecRes) { - return HKDF(ecRes, masterEphemeral, "WhisperDeviceInit").then(function(keys) { - if (new Uint8Array(message)[0] != (3 << 4) | 3) - throw new Error("Bad version number on IdentityKeyMessage"); - - var iv = message.slice(1, 16 + 1); - var mac = message.slice(message.length - 32, message.length); - var ivAndCiphertext = message.slice(0, message.length - 32); - var ciphertext = message.slice(16 + 1, message.length - 32); - - return verifyMAC(ivAndCiphertext, ecRes[1], mac).then(function() { - window.textsecure.subtle.decrypt(ecRes[0], ciphertext, iv).then(function(plaintext) { - var identityKeyMsg = textsecure.protobuf.IdentityKey.decode(plaintext); - - privToPub(toArrayBuffer(identityKeyMsg.identityKey)).then(function(identityKeyPair) { - crypto_storage.putKeyPair("identityKey", identityKeyPair); - identityKeyMsg.identityKey = null; - - return identityKeyMsg; - }); - }); - }); - }); - }); - } - - return createNewKeyPair(false).then(function(newKeyPair) { - keyPair = newKeyPair; - socketInfo.pubKey = keyPair.pubKey; - return socketInfo; - }); - } - - self.testing_only = testing_only; - return self; -}(); +;(function() { + window.textsecure = window.textsecure || {}; + + /* + * textsecure.crypto + * glues together various implementations into a single interface + * for all low-level crypto operations, + */ + + // TODO Select an ecc implementation + //var ecc = textsecure.NATIVE_CLIENT ? textsecure.nativeclient : textsecure.tweetnacl || asmjs; + var ecc = textsecure.nativeclient; + + window.textsecure.crypto = { + 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; + }, + encrypt: function(key, data, iv) { + return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) { + return window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); + }); + }, + decrypt: function(key, data, iv) { + return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['decrypt']).then(function(key) { + return window.crypto.subtle.decrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); + }); + }, + sign: function(key, data) { + return window.crypto.subtle.importKey('raw', key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) { + return window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, data); + }); + }, + + 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 window.textsecure.crypto.sign(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 window.textsecure.crypto.sign(PRK, infoBuffer.slice(32)).then(function(T1) { + infoArray.set(new Uint8Array(T1)); + infoArray[infoArray.length - 1] = 2; + return window.textsecure.crypto.sign(PRK, infoBuffer).then(function(T2) { + infoArray.set(new Uint8Array(T2)); + infoArray[infoArray.length - 1] = 3; + return window.textsecure.crypto.sign(PRK, infoBuffer).then(function(T3) { + return [ T1, T2, T3 ]; + }); + }); + }); + }); + }, + + // Curve 25519 crypto + createKeyPair: function(privKey) { + if (privKey === undefined) { + privKey = textsecure.crypto.getRandomBytes(32); + } + if (privKey.byteLength != 32) { + throw new Error("Invalid private key"); + } + + return ecc.privToPub(privKey).then(function(raw_keys) { + // prepend version byte + var origPub = new Uint8Array(raw_keys.pubKey); + var pub = new Uint8Array(33); + pub.set(origPub, 1); + pub[0] = 5; + + return { pubKey: pub.buffer, privKey: raw_keys.privKey }; + }); + }, + ECDHE: function(pubKey, privKey) { + pubKey = validatePubKeyFormat(pubKey); + if (privKey === undefined || privKey.byteLength != 32) + throw new Error("Invalid private key"); + + if (pubKey === undefined || pubKey.byteLength != 32) + throw new Error("Invalid public key"); + + return ecc.ECDHE(pubKey, privKey); + }, + Ed25519Sign: function(privKey, message) { + if (privKey === undefined || privKey.byteLength != 32) + throw new Error("Invalid private key"); + + if (message === undefined) + throw new Error("Invalid message"); + + return ecc.Ed25519Sign(privKey, message); + }, + Ed25519Verify: function(pubKey, msg, sig) { + pubKey = validatePubKeyFormat(pubKey); + + if (pubKey === undefined || pubKey.byteLength != 32) + throw new Error("Invalid public key"); + + if (msg === undefined) + throw new Error("Invalid message"); + + if (sig === undefined || sig.byteLength != 64) + throw new Error("Invalid signature"); + + return ecc.Ed25519Verify(pubKey, msg, sig); + } + }; + + var validatePubKeyFormat = function(pubKey) { + if (pubKey === undefined || ((pubKey.byteLength != 33 || new Uint8Array(pubKey)[0] != 5) && pubKey.byteLength != 32)) + throw new Error("Invalid public key"); + if (pubKey.byteLength == 33) { + return pubKey.slice(1); + } else { + console.error("WARNING: Expected pubkey of length 33, please report the ST and client that generated the pubkey"); + return pubKey; + } + }; + +})(); diff --git a/js/helpers.js b/js/helpers.js index 47e4df3e067..0cebb359b05 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -205,60 +205,21 @@ window.textsecure.throwHumanError = function(error, type, humanError) { throw e; } -/********************** - *** NaCL Interface *** - **********************/ -window.textsecure.nacl = function() { - var self = {}; +;(function() { + 'use strict'; + window.textsecure = window.textsecure || {}; + window.textsecure.NATIVE_CLIENT = window.textsecure.NATIVE_CLIENT || true; - self.USE_NACL = true; - - var onLoadCallbacks = []; - var naclLoaded = 0; - self.registerOnLoadFunction = function(func) { - return new Promise(function(resolve, reject) { - if (naclLoaded || !self.USE_NACL) + if (!textsecure.NATIVE_CLIENT) { + window.textsecure.registerOnLoadFunction = window.textsecure.nativeclient.registerOnLoadFunction; + } else { + window.textsecure.registerOnLoadFunction = function(func) { + return new Promise(function(resolve, reject) { return resolve(func()); - onLoadCallbacks[onLoadCallbacks.length] = [ func, resolve, reject ]; - }); + }); + }; } - - var naclMessageNextId = 0; - var naclMessageIdCallbackMap = {}; - window.moduleDidLoad = function() { - common.hideModule(); - naclLoaded = 1; - for (var i = 0; i < onLoadCallbacks.length; i++) { - try { - onLoadCallbacks[i][1](onLoadCallbacks[i][0]()); - } catch (e) { - onLoadCallbacks[i][2](e); - } - } - onLoadCallbacks = []; - } - - window.handleMessage = function(message) { - naclMessageIdCallbackMap[message.data.call_id](message.data); - } - - self.postNaclMessage = function(message) { - if (!self.USE_NACL) - throw new Error("Attempted to make NaCL call with !USE_NACL?"); - - return new Promise(function(resolve) { - naclMessageIdCallbackMap[naclMessageNextId] = resolve; - message.call_id = naclMessageNextId++; - - common.naclModule.postMessage(message); - }); - } - - return self; -}(); - -//TODO: Some kind of textsecure.init(use_nacl) -window.textsecure.registerOnLoadFunction = window.textsecure.nacl.registerOnLoadFunction; +})(); window.textsecure.replay = function() { var self = {}; @@ -300,13 +261,13 @@ window.textsecure.subscribeToPush = function(message_callback) { var socket = textsecure.api.getMessageWebsocket(); socket.onmessage = function(message) { - textsecure.crypto.decryptWebsocketMessage(message.message).then(function(plaintext) { + textsecure.protocol.decryptWebsocketMessage(message.message).then(function(plaintext) { var proto = textsecure.protobuf.IncomingPushMessageSignal.decode(plaintext); // After this point, a) decoding errors are not the server's fault, and // b) we should handle them gracefully and tell the user they received an invalid message console.log("Successfully decoded message with id: " + message.id); socket.send(JSON.stringify({type: 1, id: message.id})); - return textsecure.crypto.handleIncomingPushMessageProto(proto).then(function(decrypted) { + return textsecure.protocol.handleIncomingPushMessageProto(proto).then(function(decrypted) { // Delivery receipt if (decrypted === null) //TODO: Pass to UI @@ -327,7 +288,7 @@ window.textsecure.subscribeToPush = function(message_callback) { var handleAttachment = function(attachment) { return textsecure.api.getAttachment(attachment.id.toString()).then(function(encryptedBin) { - return textsecure.crypto.decryptAttachment(encryptedBin, attachment.key.toArrayBuffer()).then(function(decryptedBin) { + return textsecure.protocol.decryptAttachment(encryptedBin, attachment.key.toArrayBuffer()).then(function(decryptedBin) { attachment.decrypted = decryptedBin; }); }); @@ -421,7 +382,7 @@ window.textsecure.registerSingleDevice = function(number, verificationCode, step textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegionCodeForNumber(number)); stepDone(1); - return textsecure.crypto.generateKeys().then(function(keys) { + return textsecure.protocol.generateKeys().then(function(keys) { stepDone(2); return textsecure.api.registerKeys(keys).then(function() { stepDone(3); @@ -456,7 +417,7 @@ window.textsecure.registerSecondDevice = function(encodedDeviceInit, cryptoInfo, textsecure.storage.putUnencrypted("regionCode", libphonenumber.util.getRegion(number)); stepDone(2); - return textsecure.crypto.generateKeys().then(function(keys) { + return textsecure.protocol.generateKeys().then(function(keys) { stepDone(3); return textsecure.api.registerKeys(keys).then(function() { stepDone(4); diff --git a/js/nativeclient.js b/js/nativeclient.js new file mode 100644 index 00000000000..441f44b423a --- /dev/null +++ b/js/nativeclient.js @@ -0,0 +1,85 @@ +/* vim: ts=4:sw=4:expandtab + * + * 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 . + */ +;(function() { + 'use strict'; + window.textsecure = window.textsecure || {}; + + var naclMessageNextId = 0; + var naclMessageIdCallbackMap = {}; + window.handleMessage = function(message) { + naclMessageIdCallbackMap[message.data.call_id](message.data); + } + + function postMessage(message) { + return new Promise(function(resolve) { + naclMessageIdCallbackMap[naclMessageNextId] = resolve; + message.call_id = naclMessageNextId++; + common.naclModule.postMessage(message); + }); + }; + + var onLoadCallbacks = []; + var naclLoaded = false; + window.moduleDidLoad = function() { + common.hideModule(); + naclLoaded = true; + for (var i = 0; i < onLoadCallbacks.length; i++) { + try { + onLoadCallbacks[i][1](onLoadCallbacks[i][0]()); + } catch (e) { + onLoadCallbacks[i][2](e); + } + } + onLoadCallbacks = []; + } + + window.textsecure.nativeclient = { + privToPub: function(priv) { + return postMessage({command: "bytesToPriv", priv: priv}).then(function(message) { + var priv = message.res.slice(0, 32); + return postMessage({command: "privToPub", priv: priv}).then(function(message) { + return { pubKey: message.res.slice(0, 32), privKey: priv }; + }); + }); + }, + ECDHE: function(pub, priv) { + return postMessage({command: "ECDHE", pub: pub, priv: priv}).then(function(message) { + return message.res.slice(0, 32); + }); + }, + Ed25519Sign: function(priv, msg) { + return postMessage({command: "Ed25519Sign", priv: priv, msg: msg}).then(function(message) { + return message.res; + }); + }, + Ed25519Verify: function(pub, msg, sig) { + return postMessage({command: "Ed25519Verify", pub: pub, msg: msg, sig: sig}).then(function(message) { + if (!message.res) + throw new Error("Invalid signature"); + }); + }, + registerOnLoadFunction: function(func) { + return new Promise(function(resolve, reject) { + if (naclLoaded) { + return resolve(func()); + } else { + onLoadCallbacks[onLoadCallbacks.length] = [ func, resolve, reject ]; + } + }); + } + }; + +})(); diff --git a/js/options.js b/js/options.js index 5606177b71f..e3f82efb2f2 100644 --- a/js/options.js +++ b/js/options.js @@ -151,7 +151,7 @@ $('#multi-device .status').text("Connecting..."); $('#setup-qr').html(''); - textsecure.crypto.prepareTempWebsocket().then(function(cryptoInfo) { + textsecure.protocol.prepareTempWebsocket().then(function(cryptoInfo) { var qrCode = new QRCode(document.getElementById('setup-qr')); var socket = textsecure.api.getTempWebsocket(); diff --git a/js/protocol.js b/js/protocol.js new file mode 100644 index 00000000000..209a35f7e89 --- /dev/null +++ b/js/protocol.js @@ -0,0 +1,807 @@ +/* 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 . + */ +;(function() { + +'use strict'; +window.textsecure = window.textsecure || {}; + +window.textsecure.protocol = function() { + var self = {}; + + /****************************** + *** 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 objectContainsKeys(object) { + var count = 0; + for (var key in object) { + count++; + break; + } + return count != 0; + } + + /*************************** + *** Key/session storage *** + ***************************/ + var crypto_storage = {}; + + crypto_storage.putKeyPair = function(keyName, keyPair) { + textsecure.storage.putEncrypted("25519Key" + keyName, keyPair); + } + + crypto_storage.getNewStoredKeyPair = function(keyName) { + return textsecure.crypto.createKeyPair().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 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 textsecure.crypto.HKDF(input, salt, info); + } + + var verifyMAC = function(data, key, mac) { + return textsecure.crypto.sign(key, data).then(function(calculated_mac) { + if (!isEqual(calculated_mac, mac, true)) + throw new Error("Bad MAC"); + }); + } + + /****************************** + *** Ratchet implementation *** + ******************************/ + var calculateRatchet = function(session, remoteKey, sending) { + var ratchet = session.currentRatchet; + + return textsecure.crypto.ECDHE(remoteKey, toArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) { + return HKDF(sharedSecret, toArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) { + if (sending) + session[getString(ratchet.ephemeralKeyPair.pubKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } }; + else + session[getString(remoteKey)] = { messageKeys: {}, chainKey: { counter: -1, key: masterKey[1] } }; + ratchet.rootKey = masterKey[0]; + }); + }); + } + + var initSession = function(isInitiator, ourEphemeralKey, ourSignedKey, encodedNumber, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey) { + var ourIdentityKey = crypto_storage.getIdentityKey(); + + if (isInitiator) { + if (ourSignedKey !== undefined) + throw new Error("Invalid call to initSession"); + ourSignedKey = ourEphemeralKey; + } else { + if (theirSignedPubKey !== undefined) + throw new Error("Invalid call to initSession"); + theirSignedPubKey = theirEphemeralPubKey; + } + + var sharedSecret; + if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) + sharedSecret = new Uint8Array(32 * 4); + else + sharedSecret = new Uint8Array(32 * 5); + + for (var i = 0; i < 32; i++) + sharedSecret[i] = 0xff; + + return textsecure.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey).then(function(ecRes1) { + function finishInit() { + return textsecure.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey).then(function(ecRes) { + sharedSecret.set(new Uint8Array(ecRes), 32 * 3); + + return HKDF(sharedSecret.buffer, '', "WhisperText").then(function(masterKey) { + var session = {currentRatchet: { rootKey: masterKey[0], lastRemoteEphemeralKey: theirSignedPubKey, previousCounter: 0 }, + indexInfo: { remoteIdentityKey: theirIdentityPubKey, closed: -1 }, + oldRatchetList: [] + }; + if (!isInitiator) + session.indexInfo.baseKey = theirEphemeralPubKey; + else + session.indexInfo.baseKey = ourEphemeralKey.pubKey; + + // If we're initiating we go ahead and set our first sending ephemeral key now, + // otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key + if (isInitiator) { + return textsecure.crypto.createKeyPair().then(function(ourSendingEphemeralKey) { + session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey; + return calculateRatchet(session, theirSignedPubKey, true).then(function() { + return session; + }); + }); + } else { + session.currentRatchet.ephemeralKeyPair = ourSignedKey; + return session; + } + }); + }); + } + + var promise; + if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) + promise = Promise.resolve(new ArrayBuffer(0)); + else + promise = textsecure.crypto.ECDHE(theirEphemeralPubKey, ourEphemeralKey.privKey); + return promise.then(function(ecRes4) { + sharedSecret.set(new Uint8Array(ecRes4), 32 * 4); + + if (isInitiator) + return textsecure.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { + sharedSecret.set(new Uint8Array(ecRes1), 32); + sharedSecret.set(new Uint8Array(ecRes2), 32 * 2); + }).then(finishInit); + else + return textsecure.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey).then(function(ecRes2) { + sharedSecret.set(new Uint8Array(ecRes1), 32 * 2); + sharedSecret.set(new Uint8Array(ecRes2), 32) + }).then(finishInit); + }); + }); + } + + var removeOldChains = function(session) { + // Sending ratchets are always removed when we step because we never need them again + // Receiving ratchets are either removed if we step with all keys used up to previousCounter + // and are otherwise added to the oldRatchetList, which we parse here and remove ratchets + // older than a week (we assume the message was lost and move on with our lives at that point) + var newList = []; + for (var i = 0; i < session.oldRatchetList.length; i++) { + var entry = session.oldRatchetList[i]; + var ratchet = getString(entry.ephemeralKey); + console.log("Checking old chain with added time " + (entry.added/1000)); + if ((!objectContainsKeys(session[ratchet].messageKeys) && (session[ratchet].chainKey === undefined || session[ratchet].chainKey.key === undefined)) + || entry.added < new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS) { + delete session[ratchet]; + console.log("...deleted"); + } else + newList[newList.length] = entry; + } + session.oldRatchetList = newList; + } + + var closeSession = function(session, sessionClosedByRemote) { + if (session.indexInfo.closed > -1) + return; + + // After this has run, we can still receive messages on ratchet chains which + // were already open (unless we know we dont need them), + // but we cannot send messages or step the ratchet + + // Delete current sending ratchet + delete session[getString(session.currentRatchet.ephemeralKeyPair.pubKey)]; + // Move all receive ratchets to the oldRatchetList to mark them for deletion + for (var i in session) { + if (session[i].chainKey !== undefined && session[i].chainKey.key !== undefined) { + if (!sessionClosedByRemote) + session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: i }; + else + delete session[i].chainKey.key; + } + } + // Delete current root key and our ephemeral key pair to disallow ratchet stepping + delete session.currentRatchet['rootKey']; + delete session.currentRatchet['ephemeralKeyPair']; + session.indexInfo.closed = new Date().getTime(); + removeOldChains(session); + } + + self.closeOpenSessionForDevice = function(encodedNumber) { + var session = crypto_storage.getOpenSession(encodedNumber); + if (session === undefined) + return; + + closeSession(session); + crypto_storage.saveSession(encodedNumber, session); + } + + var initSessionFromPreKeyWhisperMessage; + var decryptWhisperMessage; + var handlePreKeyWhisperMessage = function(from, encodedMessage) { + var preKeyProto = textsecure.protobuf.PreKeyWhisperMessage.decode(encodedMessage, 'binary'); + return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) { + return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) { + if (sessions[1] !== undefined) + sessions[1](); + return result; + }); + }); + } + + var wipeIdentityAndTryMessageAgain = function(from, encodedMessage) { + //TODO: Wipe identity key! + return handlePreKeyWhisperMessage(from, encodedMessage); + } + textsecure.replay.registerReplayFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.REPLAY_FUNCS.INIT_SESSION); + + initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) { + var preKeyPair = crypto_storage.getStoredKeyPair("preKey" + message.preKeyId); + var signedPreKeyPair = crypto_storage.getStoredKeyPair("signedKey" + message.signedPreKeyId); + + var session = crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, toArrayBuffer(message.baseKey)); + var open_session = crypto_storage.getOpenSession(encodedNumber); + if (signedPreKeyPair === undefined) { + // Session may or may not be the right one, but if its not, we can't do anything about it + // ...fall through and let decryptWhisperMessage handle that case + if (session !== undefined && session.currentRatchet !== undefined) + return Promise.resolve([session, undefined]); + else + throw new Error("Missing Signed PreKey for PreKeyWhisperMessage"); + } + if (session !== undefined) { + // Duplicate PreKeyMessage for session: + if (isEqual(session.indexInfo.baseKey, message.baseKey, false)) + return Promise.resolve([session, undefined]); + + // We already had a session/known identity key: + if (isEqual(session.indexInfo.remoteIdentityKey, message.identityKey, false)) { + // If the identity key matches the previous one, close the previous one and use the new one + if (open_session !== undefined) + closeSession(open_session); // To be returned and saved later + } else { + // ...otherwise create an error that the UI will pick up and ask the user if they want to re-negotiate + throw new Error("Received message with unknown identity key", "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure.", textsecure.replay.REPLAY_FUNCS.INIT_SESSION, [encodedNumber, getString(message.encode())]); + } + } + return initSession(false, preKeyPair, signedPreKeyPair, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey), undefined) + .then(function(new_session) { + // Note that the session is not actually saved until the very end of decryptWhisperMessage + // ... to ensure that the sender actually holds the private keys for all reported pubkeys + return [new_session, function() { + if (open_session !== undefined) + crypto_storage.saveSession(encodedNumber, open_session); + crypto_storage.removeStoredKeyPair("preKey" + message.preKeyId); + }]; + });; + } + + var fillMessageKeys = function(chain, counter) { + if (chain.chainKey.counter + 1000 < counter) //TODO: maybe 1000 is too low/high in some cases? + return Promise.resolve(); // Stalker, much? + + if (chain.chainKey.counter >= counter) + return Promise.resolve(); // Already calculated + + if (chain.chainKey.key === undefined) + throw new Error("Got invalid request to extend chain after it was already closed"); + + var key = toArrayBuffer(chain.chainKey.key); + var byteArray = new Uint8Array(1); + byteArray[0] = 1; + return textsecure.crypto.sign(key, byteArray.buffer).then(function(mac) { + byteArray[0] = 2; + return textsecure.crypto.sign(key, byteArray.buffer).then(function(key) { + chain.messageKeys[chain.chainKey.counter + 1] = mac; + chain.chainKey.key = key + chain.chainKey.counter += 1; + return fillMessageKeys(chain, counter); + }); + }); + } + + var maybeStepRatchet = function(session, remoteKey, previousCounter) { + if (session[getString(remoteKey)] !== undefined) + return Promise.resolve(); + + var ratchet = session.currentRatchet; + + var finish = function() { + return calculateRatchet(session, remoteKey, false).then(function() { + // Now swap the ephemeral key and calculate the new sending chain + var previousRatchet = getString(ratchet.ephemeralKeyPair.pubKey); + if (session[previousRatchet] !== undefined) { + ratchet.previousCounter = session[previousRatchet].chainKey.counter; + delete session[previousRatchet]; + } + + return textsecure.crypto.createKeyPair().then(function(keyPair) { + ratchet.ephemeralKeyPair = keyPair; + return calculateRatchet(session, remoteKey, true).then(function() { + ratchet.lastRemoteEphemeralKey = remoteKey; + }); + }); + }); + } + + var previousRatchet = session[getString(ratchet.lastRemoteEphemeralKey)]; + if (previousRatchet !== undefined) { + return fillMessageKeys(previousRatchet, previousCounter).then(function() { + delete previousRatchet.chainKey.key; + if (!objectContainsKeys(previousRatchet.messageKeys)) + delete session[getString(ratchet.lastRemoteEphemeralKey)]; + else + session.oldRatchetList[session.oldRatchetList.length] = { added: new Date().getTime(), ephemeralKey: ratchet.lastRemoteEphemeralKey }; + }).then(finish); + } else + return finish(); + } + + // returns decrypted protobuf + decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) { + if (messageBytes[0] != String.fromCharCode((3 << 4) | 3)) + throw new Error("Bad version number on WhisperMessage"); + + var messageProto = messageBytes.substring(1, messageBytes.length - 8); + var mac = messageBytes.substring(messageBytes.length - 8, messageBytes.length); + + var message = textsecure.protobuf.WhisperMessage.decode(messageProto, 'binary'); + var remoteEphemeralKey = toArrayBuffer(message.ephemeralKey); + + if (session === undefined) { + var session = crypto_storage.getSessionByRemoteEphemeralKey(encodedNumber, remoteEphemeralKey); + if (session === undefined) + throw new Error("No session found to decrypt message from " + encodedNumber); + } + + return maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() { + var chain = session[getString(message.ephemeralKey)]; + + return fillMessageKeys(chain, message.counter).then(function() { + return HKDF(toArrayBuffer(chain.messageKeys[message.counter]), '', "WhisperMessageKeys").then(function(keys) { + delete chain.messageKeys[message.counter]; + + var messageProtoArray = toArrayBuffer(messageProto); + var macInput = new Uint8Array(messageProtoArray.byteLength + 33*2 + 1); + macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey))); + macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey)), 33); + macInput[33*2] = (3 << 4) | 3; + macInput.set(new Uint8Array(messageProtoArray), 33*2 + 1); + + return verifyMAC(macInput.buffer, keys[1], mac).then(function() { + return window.textsecure.crypto.decrypt(keys[0], toArrayBuffer(message.ciphertext), keys[2].slice(0, 16)) + .then(function(paddedPlaintext) { + + paddedPlaintext = new Uint8Array(paddedPlaintext); + var plaintext; + for (var i = paddedPlaintext.length - 1; i >= 0; i--) { + if (paddedPlaintext[i] == 0x80) { + plaintext = new Uint8Array(i); + plaintext.set(paddedPlaintext.subarray(0, i)); + plaintext = plaintext.buffer; + break; + } else if (paddedPlaintext[i] != 0x00) + throw new Error('Invalid padding'); + } + + delete session['pendingPreKey']; + + var finalMessage = textsecure.protobuf.PushMessageContent.decode(plaintext); + + if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) + == textsecure.protobuf.PushMessageContent.Flags.END_SESSION) + closeSession(session, true); + + removeOldChains(session); + + crypto_storage.saveSession(encodedNumber, session, registrationId); + return finalMessage; + }); + }); + }); + }); + }); + } + + /************************* + *** Public crypto API *** + *************************/ + // Decrypts message into a raw string + self.decryptWebsocketMessage = function(message) { + var signaling_key = textsecure.storage.getEncrypted("signaling_key"); //TODO: in crypto_storage + var aes_key = toArrayBuffer(signaling_key.substring(0, 32)); + var mac_key = toArrayBuffer(signaling_key.substring(32, 32 + 20)); + + var decodedMessage = base64DecToArr(getString(message)); + if (new Uint8Array(decodedMessage)[0] != 1) + throw new Error("Got bad version number: " + decodedMessage[0]); + + var iv = decodedMessage.slice(1, 1 + 16); + var ciphertext = decodedMessage.slice(1 + 16, decodedMessage.byteLength - 10); + var ivAndCiphertext = decodedMessage.slice(0, decodedMessage.byteLength - 10); + var mac = decodedMessage.slice(decodedMessage.byteLength - 10, decodedMessage.byteLength); + + return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() { + return window.textsecure.crypto.decrypt(aes_key, ciphertext, iv); + }); + }; + + self.decryptAttachment = function(encryptedBin, keys) { + var aes_key = keys.slice(0, 32); + var mac_key = keys.slice(32, 64); + + var iv = encryptedBin.slice(0, 16); + var ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); + var ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); + var mac = encryptedBin.slice(encryptedBin.byteLength - 32, encryptedBin.byteLength); + + return verifyMAC(ivAndCiphertext, mac_key, mac).then(function() { + return window.textsecure.crypto.decrypt(aes_key, ciphertext, iv); + }); + }; + + self.encryptAttachment = function(plaintext, keys, iv) { + var aes_key = keys.slice(0, 32); + var mac_key = keys.slice(32, 64); + + return window.textsecure.crypto.encrypt(aes_key, plaintext, iv).then(function(ciphertext) { + var ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); + ivAndCiphertext.set(new Uint8Array(iv)); + ivAndCiphertext.set(new Uint8Array(ciphertext), 16); + + return textsecure.crypto.sign(mac_key, ivAndCiphertext.buffer).then(function(mac) { + var encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32); + encryptedBin.set(ivAndCiphertext); + encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); + return encryptedBin.buffer; + }); + }); + }; + + self.handleIncomingPushMessageProto = function(proto) { + switch(proto.type) { + case textsecure.protobuf.IncomingPushMessageSignal.Type.PLAINTEXT: + return Promise.resolve(textsecure.protobuf.PushMessageContent.decode(proto.message)); + case textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT: + var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice); + return decryptWhisperMessage(from, getString(proto.message)); + case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE: + if (proto.message.readUint8() != ((3 << 4) | 3)) + throw new Error("Bad version byte"); + var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice); + return handlePreKeyWhisperMessage(from, getString(proto.message)); + case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT: + return Promise.resolve(null); + default: + return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); }); + } + } + + // return Promise(encoded [PreKey]WhisperMessage) + self.encryptMessageFor = function(deviceObject, pushMessageContent) { + var session = crypto_storage.getOpenSession(deviceObject.encodedNumber); + + var doEncryptPushMessageContent = function() { + var msg = new textsecure.protobuf.WhisperMessage(); + var plaintext = toArrayBuffer(pushMessageContent.encode()); + + var paddedPlaintext = new Uint8Array(Math.ceil((plaintext.byteLength + 1) / 160.0) * 160); + paddedPlaintext.set(new Uint8Array(plaintext)); + paddedPlaintext[plaintext.byteLength] = 0x80; + + msg.ephemeralKey = toArrayBuffer(session.currentRatchet.ephemeralKeyPair.pubKey); + var chain = session[getString(msg.ephemeralKey)]; + + return fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() { + return HKDF(toArrayBuffer(chain.messageKeys[chain.chainKey.counter]), '', "WhisperMessageKeys").then(function(keys) { + delete chain.messageKeys[chain.chainKey.counter]; + msg.counter = chain.chainKey.counter; + msg.previousCounter = session.currentRatchet.previousCounter; + + return window.textsecure.crypto.encrypt(keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)).then(function(ciphertext) { + msg.ciphertext = ciphertext; + var encodedMsg = toArrayBuffer(msg.encode()); + + var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1); + macInput.set(new Uint8Array(toArrayBuffer(crypto_storage.getIdentityKey().pubKey))); + macInput.set(new Uint8Array(toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33); + macInput[33*2] = (3 << 4) | 3; + macInput.set(new Uint8Array(encodedMsg), 33*2 + 1); + + return textsecure.crypto.sign(keys[1], macInput.buffer).then(function(mac) { + var result = new Uint8Array(encodedMsg.byteLength + 9); + result[0] = (3 << 4) | 3; + result.set(new Uint8Array(encodedMsg), 1); + result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1); + + try { + delete deviceObject['signedKey']; + delete deviceObject['signedKeyId']; + delete deviceObject['preKey']; + delete deviceObject['preKeyId']; + } catch(_) {} + + removeOldChains(session); + + crypto_storage.saveSessionAndDevice(deviceObject, session); + return result; + }); + }); + }); + }); + } + + var preKeyMsg = new textsecure.protobuf.PreKeyWhisperMessage(); + preKeyMsg.identityKey = toArrayBuffer(crypto_storage.getIdentityKey().pubKey); + preKeyMsg.registrationId = textsecure.storage.getUnencrypted("registrationId"); + + if (session === undefined) { + return textsecure.crypto.createKeyPair().then(function(baseKey) { + preKeyMsg.preKeyId = deviceObject.preKeyId; + preKeyMsg.signedPreKeyId = deviceObject.signedKeyId; + preKeyMsg.baseKey = toArrayBuffer(baseKey.pubKey); + return initSession(true, baseKey, undefined, deviceObject.encodedNumber, + toArrayBuffer(deviceObject.identityKey), toArrayBuffer(deviceObject.preKey), toArrayBuffer(deviceObject.signedKey)) + .then(function(new_session) { + session = new_session; + session.pendingPreKey = { preKeyId: deviceObject.preKeyId, signedKeyId: deviceObject.signedKeyId, baseKey: baseKey.pubKey }; + return doEncryptPushMessageContent().then(function(message) { + preKeyMsg.message = message; + var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode()); + return {type: 3, body: result}; + }); + }); + }); + } else + return doEncryptPushMessageContent().then(function(message) { + if (session.pendingPreKey !== undefined) { + preKeyMsg.baseKey = toArrayBuffer(session.pendingPreKey.baseKey); + preKeyMsg.preKeyId = session.pendingPreKey.preKeyId; + preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId; + preKeyMsg.message = message; + + var result = String.fromCharCode((3 << 4) | 3) + getString(preKeyMsg.encode()); + return {type: 3, body: result}; + } else + return {type: 1, body: getString(message)}; + }); + } + + var GENERATE_KEYS_KEYS_GENERATED = 100; + self.generateKeys = function() { + var identityKeyPair = crypto_storage.getIdentityKey(); + var identityKeyCalculated = function(identityKeyPair) { + var firstPreKeyId = textsecure.storage.getEncrypted("maxPreKeyId", 0); + textsecure.storage.putEncrypted("maxPreKeyId", firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED); + + var signedKeyId = textsecure.storage.getEncrypted("signedKeyId", 0); + textsecure.storage.putEncrypted("signedKeyId", signedKeyId + 1); + + var keys = {}; + keys.identityKey = identityKeyPair.pubKey; + keys.preKeys = []; + + var generateKey = function(keyId) { + return crypto_storage.getNewStoredKeyPair("preKey" + keyId, false).then(function(keyPair) { + keys.preKeys[keyId] = {keyId: keyId, publicKey: keyPair.pubKey}; + }); + }; + + var promises = []; + for (var i = firstPreKeyId; i < firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED; i++) + promises[i] = generateKey(i); + + promises[firstPreKeyId + GENERATE_KEYS_KEYS_GENERATED] = crypto_storage.getNewStoredKeyPair("signedKey" + signedKeyId).then(function(keyPair) { + return textsecure.crypto.Ed25519Sign(identityKeyPair.privKey, keyPair.pubKey).then(function(sig) { + keys.signedPreKey = {keyId: signedKeyId, publicKey: keyPair.pubKey, signature: sig}; + }); + }); + + //TODO: Process by date added and agressively call generateKeys when we get near maxPreKeyId in a message + crypto_storage.removeStoredKeyPair("signedKey" + (signedKeyId - 2)); + + return Promise.all(promises).then(function() { + return keys; + }); + } + if (identityKeyPair === undefined) + return crypto_storage.getNewStoredKeyPair("identityKey", true).then(function(keyPair) { return identityKeyCalculated(keyPair); }); + else + return identityKeyCalculated(identityKeyPair); + } + + window.textsecure.registerOnLoadFunction(function() { + //TODO: Dont always update prekeys here + if (textsecure.storage.getEncrypted("lastSignedKeyUpdate", Date.now()) < Date.now() - MESSAGE_LOST_THRESHOLD_MS) + self.generateKeys(); + }); + + + self.prepareTempWebsocket = function() { + var socketInfo = {}; + var keyPair; + + socketInfo.decryptAndHandleDeviceInit = function(deviceInit) { + var masterEphemeral = toArrayBuffer(deviceInit.masterEphemeralPubKey); + var message = toArrayBuffer(deviceInit.identityKeyMessage); + + return textsecure.crypto.ECDHE(masterEphemeral, keyPair.privKey).then(function(ecRes) { + return HKDF(ecRes, masterEphemeral, "WhisperDeviceInit").then(function(keys) { + if (new Uint8Array(message)[0] != (3 << 4) | 3) + throw new Error("Bad version number on IdentityKeyMessage"); + + var iv = message.slice(1, 16 + 1); + var mac = message.slice(message.length - 32, message.length); + var ivAndCiphertext = message.slice(0, message.length - 32); + var ciphertext = message.slice(16 + 1, message.length - 32); + + return verifyMAC(ivAndCiphertext, ecRes[1], mac).then(function() { + window.textsecure.crypto.decrypt(ecRes[0], ciphertext, iv).then(function(plaintext) { + var identityKeyMsg = textsecure.protobuf.IdentityKey.decode(plaintext); + + textsecure.crypto.createKeyPair(toArrayBuffer(identityKeyMsg.identityKey)).then(function(identityKeyPair) { + crypto_storage.putKeyPair("identityKey", identityKeyPair); + identityKeyMsg.identityKey = null; + + return identityKeyMsg; + }); + }); + }); + }); + }); + } + + return textsecure.crypto.createKeyPair().then(function(newKeyPair) { + keyPair = newKeyPair; + socketInfo.pubKey = keyPair.pubKey; + return socketInfo; + }); + } + + return self; +}(); + +})(); diff --git a/js/sendmessage.js b/js/sendmessage.js index 5a1b8c33100..baed6f7a28b 100644 --- a/js/sendmessage.js +++ b/js/sendmessage.js @@ -50,7 +50,7 @@ window.textsecure.messaging = function() { return new Promise(function() { throw new Error("Mismatched relays for number " + number); }); } - return textsecure.crypto.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) { + return textsecure.protocol.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) { jsonData[i] = { type: encryptedMsg.type, destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1], @@ -198,7 +198,7 @@ window.textsecure.messaging = function() { proto.key = textsecure.crypto.getRandomBytes(64); var iv = textsecure.crypto.getRandomBytes(16); - return textsecure.crypto.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) { + return textsecure.protocol.encryptAttachment(attachment.data, proto.key, iv).then(function(encryptedBin) { return textsecure.api.putAttachment(encryptedBin).then(function(id) { proto.id = id; proto.contentType = attachment.contentType; @@ -252,7 +252,7 @@ window.textsecure.messaging = function() { return sendIndividualProto(number, proto).then(function(res) { var devices = textsecure.storage.devices.getDeviceObjectsForNumber(number); for (var i in devices) - textsecure.crypto.closeOpenSessionForDevice(devices[i].encodedNumber); + textsecure.protocol.closeOpenSessionForDevice(devices[i].encodedNumber); return res; }); diff --git a/js/webcrypto.js b/js/webcrypto.js index 174c699c2bd..5ac5531ad0b 100644 --- a/js/webcrypto.js +++ b/js/webcrypto.js @@ -89,22 +89,4 @@ }; })(); } // if !window.crypto.subtle - - window.textsecure.subtle = { - encrypt: function(key, data, iv) { - return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['encrypt']).then(function(key) { - return window.crypto.subtle.encrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); - }); - }, - decrypt: function(key, data, iv) { - return window.crypto.subtle.importKey('raw', key, {name: 'AES-CBC'}, false, ['decrypt']).then(function(key) { - return window.crypto.subtle.decrypt({name: 'AES-CBC', iv: new Uint8Array(iv)}, key, data); - }); - }, - sign: function(key, data) { - return window.crypto.subtle.importKey('raw', key, {name: 'HMAC', hash: {name: 'SHA-256'}}, false, ['sign']).then(function(key) { - return window.crypto.subtle.sign( {name: 'HMAC', hash: 'SHA-256'}, key, data); - }); - }, - }; })(); diff --git a/options.html b/options.html index a405b2b6d4f..8f1eb7f3cbe 100644 --- a/options.html +++ b/options.html @@ -96,6 +96,7 @@ + @@ -103,6 +104,7 @@ + diff --git a/test/_test.js b/test/_test.js index 593c764bb6a..2f646f2476c 100644 --- a/test/_test.js +++ b/test/_test.js @@ -1,2 +1,43 @@ +/* + * 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 . + */ + mocha.setup("bdd"); window.assert = chai.assert; + +/* + * global helpers for tests + */ +function assertEqualArrayBuffers(ab1, ab2) { + assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); +}; + +function arrayBufferToHex(buffer) { + var array = new Uint8Array(buffer); + var s = ''; + for (var i in array) { + var h = array[i].toString(16); + if (h.length < 2) { s += '0'; } + s += h; + } + return s; +}; + +function hexToArrayBuffer(str) { + var ret = new ArrayBuffer(str.length / 2); + var array = new Uint8Array(ret); + for (var i = 0; i < str.length/2; i++) + array[i] = parseInt(str.substr(i*2, 2), 16); + return ret; +}; diff --git a/test/crypto_test.js b/test/crypto_test.js index dea558dc1fe..0f57e315c49 100644 --- a/test/crypto_test.js +++ b/test/crypto_test.js @@ -14,8 +14,7 @@ * along with this program. If not, see . */ -mocha.setup("bdd"); -window.assert = chai.assert; +'use strict'; describe("ArrayBuffer->String conversion", function() { it('works', function() { @@ -35,7 +34,7 @@ describe("Cryptographic primitives", function() { var iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f'); var plaintext = hexToArrayBuffer('6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'); var ciphertext = hexToArrayBuffer('f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644'); - window.textsecure.subtle.encrypt(key, plaintext, iv).then(function(result) { + window.textsecure.crypto.encrypt(key, plaintext, iv).then(function(result) { assert.strictEqual(getString(result), getString(ciphertext)); }).then(done).catch(done); }); @@ -47,7 +46,7 @@ describe("Cryptographic primitives", function() { var iv = hexToArrayBuffer('000102030405060708090a0b0c0d0e0f'); var plaintext = hexToArrayBuffer('6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710'); var ciphertext = hexToArrayBuffer('f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b3f461796d6b0d6b2e0c2a72b4d80e644'); - window.textsecure.subtle.decrypt(key, ciphertext, iv).then(function(result) { + window.textsecure.crypto.decrypt(key, ciphertext, iv).then(function(result) { assert.strictEqual(getString(result), getString(plaintext)); }).then(done).catch(done); }); @@ -58,7 +57,7 @@ describe("Cryptographic primitives", function() { var key = hexToArrayBuffer('6f35628d65813435534b5d67fbdb54cb33403d04e843103e6399f806cb5df95febbdd61236f33245'); var input = hexToArrayBuffer('752cff52e4b90768558e5369e75d97c69643509a5e5904e0a386cbe4d0970ef73f918f675945a9aefe26daea27587e8dc909dd56fd0468805f834039b345f855cfe19c44b55af241fff3ffcd8045cd5c288e6c4e284c3720570b58e4d47b8feeedc52fd1401f698a209fccfa3b4c0d9a797b046a2759f82a54c41ccd7b5f592b'); var mac = getString(hexToArrayBuffer('05d1243e6465ed9620c9aec1c351a186')); - window.textsecure.subtle.sign(key, input).then(function(result) { + window.textsecure.crypto.sign(key, input).then(function(result) { assert.strictEqual(getString(result).substring(0, mac.length), mac); }).then(done).catch(done); }); @@ -79,7 +78,7 @@ describe("Cryptographic primitives", function() { for (var i = 0; i < 10; i++) info[i] = 240 + i; - return textsecure.crypto.testing_only.HKDF(IKM.buffer, salt.buffer, info.buffer).then(function(OKM){ + return textsecure.crypto.HKDF(IKM.buffer, salt.buffer, info.buffer).then(function(OKM){ var T1 = hexToArrayBuffer("3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf"); var T2 = hexToArrayBuffer("34007208d5b887185865"); assert.equal(getString(OKM[0]), getString(T1)); @@ -109,7 +108,7 @@ describe('Unencrypted PushMessageProto "decrypt"', function() { message: text_message.encode() }; - return textsecure.crypto.handleIncomingPushMessageProto(server_message). + return textsecure.protocol.handleIncomingPushMessageProto(server_message). then(function(message) { assert.equal(message.body, text_message.body); assert.equal(message.attachments.length, text_message.attachments.length); @@ -122,8 +121,8 @@ describe("Curve25519", function() { describe("Implementation", function() { // this is a just cute little trick to get a nice-looking note about // which curve25519 impl we're using. - if (window.textsecure.nacl.USE_NACL) { - it("is NACL", function(done) { done(); }); + if (window.textsecure.NATIVE_CLIENT) { + it("is Native Client", function(done) { done(); }); } else { it("is JavaScript", function(done) { done(); }); } @@ -139,26 +138,26 @@ describe("Curve25519", function() { var bob_pub = hexToArrayBuffer("05de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); var shared_sec = hexToArrayBuffer("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"); - return textsecure.crypto.testing_only.privToPub(alice_priv, true).then(function(aliceKeyPair) { + return textsecure.crypto.createKeyPair(alice_priv).then(function(aliceKeyPair) { var target = new Uint8Array(alice_priv.slice(0)); target[0] &= 248; target[31] &= 127; target[31] |= 64; + assert.equal(getString(aliceKeyPair.pubKey), getString(alice_pub)); assert.equal(getString(aliceKeyPair.privKey), getString(target)); - return textsecure.crypto.testing_only.privToPub(bob_priv, true).then(function(bobKeyPair) { + return textsecure.crypto.createKeyPair(bob_priv).then(function(bobKeyPair) { var target = new Uint8Array(bob_priv.slice(0)); target[0] &= 248; target[31] &= 127; target[31] |= 64; assert.equal(getString(bobKeyPair.privKey), getString(target)); - assert.equal(getString(aliceKeyPair.pubKey), getString(alice_pub)); assert.equal(getString(bobKeyPair.pubKey), getString(bob_pub)); - return textsecure.crypto.testing_only.ECDHE(bobKeyPair.pubKey, aliceKeyPair.privKey).then(function(ss) { + return textsecure.crypto.ECDHE(bobKeyPair.pubKey, aliceKeyPair.privKey).then(function(ss) { assert.equal(getString(ss), getString(shared_sec)); - return textsecure.crypto.testing_only.ECDHE(aliceKeyPair.pubKey, bobKeyPair.privKey).then(function(ss) { + return textsecure.crypto.ECDHE(aliceKeyPair.pubKey, bobKeyPair.privKey).then(function(ss) { assert.equal(getString(ss), getString(shared_sec)); }); }); @@ -178,14 +177,14 @@ describe("Curve25519", function() { var sig = hexToArrayBuffer("2bc06c745acb8bae10fbc607ee306084d0c28e2b3bb819133392473431291fd0"+ "dfa9c7f11479996cf520730d2901267387e08d85bbf2af941590e3035a545285"); - return textsecure.crypto.testing_only.privToPub(priv, false).then(function(pubCalc) { + return textsecure.crypto.createKeyPair(priv).then(function(pubCalc) { //if (getString(pub) != getString(pubCalc)) // return false; - return textsecure.crypto.testing_only.Ed25519Sign(priv, msg).then(function(sigCalc) { + return textsecure.crypto.Ed25519Sign(priv, msg).then(function(sigCalc) { assert.equal(getString(sig), getString(sigCalc)); - return textsecure.crypto.testing_only.Ed25519Verify(pub, msg, sig); + return textsecure.crypto.Ed25519Verify(pub, msg, sig); }); }); }).then(done).catch(done); @@ -199,14 +198,14 @@ describe("Curve25519", function() { it ('works', function(done) { localStorage.clear(); return textsecure.registerOnLoadFunction(function() { - return textsecure.crypto.generateKeys().then(function() { + return textsecure.protocol.generateKeys().then(function() { assert.isDefined(textsecure.storage.getEncrypted("25519KeyidentityKey")); assert.isDefined(textsecure.storage.getEncrypted("25519KeysignedKey0")); for (var i = 0; i < 100; i++) { assert.isDefined(textsecure.storage.getEncrypted("25519KeypreKey" + i)); } var origIdentityKey = getString(textsecure.storage.getEncrypted("25519KeyidentityKey").privKey); - return textsecure.crypto.generateKeys().then(function() { + return textsecure.protocol.generateKeys().then(function() { assert.isDefined(textsecure.storage.getEncrypted("25519KeyidentityKey")); assert.equal(getString(textsecure.storage.getEncrypted("25519KeyidentityKey").privKey), origIdentityKey); @@ -217,7 +216,7 @@ describe("Curve25519", function() { assert.isDefined(textsecure.storage.getEncrypted("25519KeypreKey" + i)); } - return textsecure.crypto.generateKeys().then(function() { + return textsecure.protocol.generateKeys().then(function() { assert.isDefined(textsecure.storage.getEncrypted("25519KeyidentityKey")); assert.equal(getString(textsecure.storage.getEncrypted("25519KeyidentityKey").privKey), origIdentityKey); @@ -259,7 +258,7 @@ describe("Axolotl", function() { throw new Error('Out of private keys'); else { var privKey = privKeyQueue.shift(); - return textsecure.crypto.testing_only.privToPub(privKey, false).then(function(keyPair) { + return textsecure.crypto.createKeyPair(privKey).then(function(keyPair) { var a = btoa(getString(keyPair.privKey)); var b = btoa(getString(privKey)); if (getString(keyPair.privKey) != getString(privKey)) throw new Error('Failed to rederive private key!'); @@ -286,7 +285,7 @@ describe("Axolotl", function() { message.sourceDevice = 1; try { var proto = textsecure.protobuf.IncomingPushMessageSignal.decode(message.encode()); - return textsecure.crypto.handleIncomingPushMessageProto(proto).then(function(res) { + return textsecure.protocol.handleIncomingPushMessageProto(proto).then(function(res) { if (data.expectTerminateSession) return res.flags == textsecure.protobuf.PushMessageContent.Flags.END_SESSION; return res.body == data.expectedSmsText; @@ -303,13 +302,13 @@ describe("Axolotl", function() { } if (data.ourIdentityKey !== undefined) - return textsecure.crypto.testing_only.privToPub(data.ourIdentityKey, true).then(function(keyPair) { + return textsecure.crypto.createKeyPair(data.ourIdentityKey).then(function(keyPair) { textsecure.storage.putEncrypted("25519KeyidentityKey", keyPair); - return textsecure.crypto.testing_only.privToPub(data.ourSignedPreKey, false).then(function(keyPair) { + return textsecure.crypto.createKeyPair(data.ourSignedPreKey).then(function(keyPair) { textsecure.storage.putEncrypted("25519KeysignedKey" + data.signedPreKeyId, keyPair); if (data.ourPreKey !== undefined) - return textsecure.crypto.testing_only.privToPub(data.ourPreKey, false).then(function(keyPair) { + return textsecure.crypto.createKeyPair(data.ourPreKey).then(function(keyPair) { textsecure.storage.putEncrypted("25519KeypreKey" + data.preKeyId, keyPair); return postLocalKeySetup(); }); @@ -356,7 +355,7 @@ describe("Axolotl", function() { privKeyQueue.push(data.ourEphemeralKey); if (data.ourIdentityKey !== undefined) - return textsecure.crypto.testing_only.privToPub(data.ourIdentityKey, true).then(function(keyPair) { + return textsecure.crypto.createKeyPair(data.ourIdentityKey).then(function(keyPair) { textsecure.storage.putEncrypted("25519KeyidentityKey", keyPair); return postLocalKeySetup(); }); diff --git a/test/index.html b/test/index.html index 7755dd8e232..875bcf15552 100644 --- a/test/index.html +++ b/test/index.html @@ -127,6 +127,7 @@ + @@ -134,7 +135,8 @@ - + + @@ -155,6 +157,7 @@ + diff --git a/test/nativeclient_test.js b/test/nativeclient_test.js new file mode 100644 index 00000000000..3f6a9b8bbdc --- /dev/null +++ b/test/nativeclient_test.js @@ -0,0 +1,96 @@ +/* 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 . + */ + +'use strict'; + +describe("textsecure.nativeclient", function() { + var alice_bytes = hexToArrayBuffer("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); + var alice_priv = hexToArrayBuffer("70076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c6a"); + var alice_pub = hexToArrayBuffer("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"); + var bob_bytes = hexToArrayBuffer("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"); + var bob_priv = hexToArrayBuffer("58ab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e06b"); + var bob_pub = hexToArrayBuffer("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); + var shared_sec = hexToArrayBuffer("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"); + + describe("privToPub", function() { + it ('converts alice private keys to a keypair', function(done) { + textsecure.nativeclient.privToPub(alice_bytes).then(function(keypair) { + assertEqualArrayBuffers(keypair.privKey, alice_priv); + assertEqualArrayBuffers(keypair.pubKey, alice_pub); + done(); + }).catch(done); + }); + it ('converts bob private keys to a keypair', function(done) { + textsecure.nativeclient.privToPub(bob_bytes).then(function(keypair) { + assertEqualArrayBuffers(keypair.privKey, bob_priv); + assertEqualArrayBuffers(keypair.pubKey, bob_pub); + done(); + }).catch(done); + }); + }); + + describe("ECDHE", function() { + it("computes the shared secret for alice", function(done) { + textsecure.nativeclient.ECDHE(bob_pub, alice_priv).then(function(secret) { + assertEqualArrayBuffers(shared_sec, secret); + done(); + }).catch(done); + }); + it("computes the shared secret for bob", function(done) { + textsecure.nativeclient.ECDHE(alice_pub, bob_priv).then(function(secret) { + assertEqualArrayBuffers(shared_sec, secret); + done(); + }).catch(done); + }); + }); + + var priv = hexToArrayBuffer("48a8892cc4e49124b7b57d94fa15becfce071830d6449004685e387c62409973"); + var pub = hexToArrayBuffer("55f1bfede27b6a03e0dd389478ffb01462e5c52dbbac32cf870f00af1ed9af3a"); + var msg = hexToArrayBuffer("617364666173646661736466"); + var sig = hexToArrayBuffer("2bc06c745acb8bae10fbc607ee306084d0c28e2b3bb819133392473431291fd0dfa9c7f11479996cf520730d2901267387e08d85bbf2af941590e3035a545285"); + describe("Ed25519Sign", function() { + it("computes the signature", function(done) { + textsecure.nativeclient.Ed25519Sign(priv, msg).then(function(signature) { + assertEqualArrayBuffers(sig, signature); + done(); + }).catch(done); + }); + }); + + describe("Ed25519Verify", function() { + it("throws on bad signature", function(done) { + var badsig = sig.slice(0); + new Uint8Array(badsig).set([0], 0); + + textsecure.nativeclient.Ed25519Verify(pub, msg, badsig).catch(function(e) { + if (e.message === 'Invalid signature') { + done(); + } else { throw e; } + }).catch(done); + }); + + it("does not throw on good signature", function(done) { + textsecure.nativeclient.Ed25519Verify(pub, msg, sig).then(done).catch(done); + }); + }); + + describe("registerOnLoadFunction", function() { + it('queues a callback til native client is loaded', function(done) { + textsecure.nativeclient.registerOnLoadFunction(done); + }); + }); + +}); diff --git a/test/test.js b/test/test.js index fe66c706508..c571e39120b 100644 --- a/test/test.js +++ b/test/test.js @@ -10872,5 +10872,46 @@ require.alias("chai/index.js", "chai/index.js");if (typeof exports == "object") } else { this["chai"] = require("chai"); }})(); +/* + * 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 . + */ + mocha.setup("bdd"); window.assert = chai.assert; + +/* + * global helpers for tests + */ +function assertEqualArrayBuffers(ab1, ab2) { + assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); +}; + +function arrayBufferToHex(buffer) { + var array = new Uint8Array(buffer); + var s = ''; + for (var i in array) { + var h = array[i].toString(16); + if (h.length < 2) { s += '0'; } + s += h; + } + return s; +}; + +function hexToArrayBuffer(str) { + var ret = new ArrayBuffer(str.length / 2); + var array = new Uint8Array(ret); + for (var i = 0; i < str.length/2; i++) + array[i] = parseInt(str.substr(i*2, 2), 16); + return ret; +}; diff --git a/test/testvectors.js b/test/testvectors.js index 0c45d6b7b93..71f80da5499 100644 --- a/test/testvectors.js +++ b/test/testvectors.js @@ -2,13 +2,6 @@ // Distributed under the X11 software license, see the accompanying // file MIT -function hexToArrayBuffer(str) { - var ret = new ArrayBuffer(str.length / 2); - var array = new Uint8Array(ret); - for (var i = 0; i < str.length/2; i++) - array[i] = parseInt(str.substr(i*2, 2), 16); - return ret; -} axolotlTestVectors = function() { // We're gonna throw the finalized tests in here: var tests = [];