diff --git a/js/crypto.js b/js/crypto.js index 223099ca2..71f0337d8 100644 --- a/js/crypto.js +++ b/js/crypto.js @@ -143,14 +143,21 @@ window.textsecure.crypto = function() { return this.getStoredKeyPair("identityKey").privKey; } - crypto_storage.saveSession = function(encodedNumber, session) { - var sessions = textsecure.storage.getEncrypted("session" + encodedNumber); - if (sessions === undefined) - sessions = {}; + crypto_storage.saveSession = function(encodedNumber, session, registrationId) { + var device = textsecure.storage.devices.getDeviceObject(encodedNumber); + if (device === undefined) + device = { sessions: {}, encodedNumber: encodedNumber }; + if (device.sessions === undefined) + device.sessions = {}; + + if (registrationId !== undefined) + device.registrationId = registrationId; + + var sessions = device.sessions; var doDeleteSession = false; if (session.indexInfo.closed == -1) - sessions.identityKey = session.indexInfo.remoteIdentityKey; + device.identityKey = session.indexInfo.remoteIdentityKey; else { doDeleteSession = (session.indexInfo.closed < (new Date().getTime() - MESSAGE_LOST_THRESHOLD_MS)); @@ -171,26 +178,29 @@ window.textsecure.crypto = function() { else sessions[getString(session.indexInfo.baseKey)] = session; - textsecure.storage.putEncrypted("session" + encodedNumber, sessions); + 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 = textsecure.storage.getEncrypted("session" + encodedNumber); + var sessions = getSessions(encodedNumber); if (sessions === undefined) return undefined; - for (key in sessions) { - if (key == "identityKey") - continue; - + for (key in sessions) if (sessions[key].indexInfo.closed == -1) return sessions[key]; - } return undefined; } crypto_storage.getSessionByRemoteEphemeralKey = function(encodedNumber, remoteEphemeralKey) { - var sessions = textsecure.storage.getEncrypted("session" + encodedNumber); + var sessions = getSessions(encodedNumber); if (sessions === undefined) return undefined; @@ -198,9 +208,6 @@ window.textsecure.crypto = function() { var openSession = undefined; for (key in sessions) { - if (key == "identityKey") - continue; - if (sessions[key].indexInfo.closed == -1) { if (openSession !== undefined) throw new Error("Datastore inconsistensy: multiple open sessions for " + encodedNumber); @@ -215,20 +222,20 @@ window.textsecure.crypto = function() { return undefined; } - crypto_storage.getSessionOrIdentityKeyByBaseKey = function(encodedNumber, baseKey) { - var sessions = textsecure.storage.getEncrypted("session" + encodedNumber); - if (sessions === undefined) + var sessions = getSessions(encodedNumber); + var device = textsecure.storage.devices.getDeviceObject(encodedNumber); + if (device === undefined) return undefined; - var preferredSession = sessions[getString(baseKey)]; + var preferredSession = device.sessions && device.sessions[getString(baseKey)]; if (preferredSession !== undefined) return preferredSession; - if (sessions.identityKey !== undefined) - return { indexInfo: { remoteIdentityKey: sessions.identityKey } }; + if (device.identityKey !== undefined) + return { indexInfo: { remoteIdentityKey: device.identityKey } }; - throw new Error("Datastore inconsistency: session was stored without identity key"); + throw new Error("Datastore inconsistency: device was stored without identity key"); } // Used when device keys change - we assume key compromise so refuse all new messages @@ -518,7 +525,7 @@ window.textsecure.crypto = function() { } // returns decrypted protobuf - var decryptWhisperMessage = function(encodedNumber, messageBytes, session) { + var decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) { if (messageBytes[0] != String.fromCharCode((2 << 4) | 2)) throw new Error("Bad version number on WhisperMessage"); @@ -554,7 +561,7 @@ window.textsecure.crypto = function() { if ((finalMessage.flags & 1) == 1) // END_SESSION closeSession(session); - crypto_storage.saveSession(encodedNumber, session); + crypto_storage.saveSession(encodedNumber, session, registrationId); return finalMessage; }); }); @@ -613,7 +620,7 @@ window.textsecure.crypto = function() { var from = proto.source + "." + (proto.sourceDevice == null ? 0 : proto.sourceDevice); var preKeyProto = textsecure.protos.decodePreKeyWhisperMessageProtobuf(getString(proto.message)); return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) { - return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0]).then(function(result) { + return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) { if (sessions[1] !== undefined) crypto_storage.saveSession(proto.source, sessions[1]); return result; diff --git a/js/fake_api.js b/js/fake_api.js index 182cf8ce7..3c783a0a4 100644 --- a/js/fake_api.js +++ b/js/fake_api.js @@ -14,30 +14,28 @@ * along with this program. If not, see . */ -//TODO: Redo this (API has changed to textsecure.api and changed) -var FakeWhisperAPI = function() { - var doAjax = function(param) { - if (param.success_callback) { - setTimeout(param.success_callback, 100, param.response); - } - } +var getKeysForNumberMap = {}; +textsecure.api.getKeysForNumber = function(number) { + var res = getKeysForNumberMap[number]; + if (res !== undefined) { + delete getKeysForNumberMap[number]; + return Promise.resolve(res); + } else + throw new Error("getKeysForNumber of unknown/used number"); +} - this.getKeysForNumber = function(number, success_callback, error_callback) { - doAjax({ success_callback : success_callback, - response : [{ identityKey : 1, - deviceId : 1, - publicKey : 1, - keyId : 1 }] - }); - } +var messagesSentMap = {}; +textsecure.api.sendMessages = function(destination, messageArray) { + for (i in messageArray) { + var msg = messageArray[i]; + if ((msg.type != 1 && msg.type != 3) || + msg.destinationDeviceId === undefined || + msg.destinationRegistrationId === undefined || + msg.body === undefined || + msg.timestamp == undefined || + msg.relay !== undefined) + throw new Error("Invalid message"); - this.sendMessages = function(jsonData, success_callback, error_callback) { - doAjax({ success_callback : success_callback, - response : { missingDeviceIds: [] } - }); + messagesSentMap[destination + "." + messageArray[i].destinationDeviceId] = msg; } -}; - -FakeWhisperAPI.prototype = API; -API = new FakeWhisperAPI(); - +} diff --git a/js/helpers.js b/js/helpers.js index e7e7d3fda..a4f196e6f 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -245,10 +245,9 @@ window.textsecure.utils = function() { } self.unencodeNumber = function(number) { - return number.split(".")[0]; + return number.split("."); } - /************************** *** JSON'ing Utilities *** **************************/ @@ -337,13 +336,28 @@ window.textsecure.storage = function() { var self = {}; self.saveDeviceObject = function(deviceObject) { - var number = textsecure.utils.unencodeNumber(deviceObject.encodedNumber); + if (deviceObject.identityKey === undefined || deviceObject.registrationId === undefined || deviceObject.encodedNumber === undefined) + throw new Error("Tried to store invalid deviceObject"); + + var number = textsecure.utils.unencodeNumber(deviceObject.encodedNumber)[0]; var map = textsecure.storage.getEncrypted("devices" + number); if (map === undefined) map = { devices: [deviceObject], identityKey: deviceObject.identityKey }; else if (map.identityKey != getString(deviceObject.identityKey)) throw new Error("Identity key changed"); + else { + var updated = false; + for (i in map.devices) { + if (map.devices[i].encodedNumber == deviceObject.encodedNumber) { + map.devices[i] = deviceObject; + updated = true; + } + } + + if (!updated) + map.devices.push(deviceObject); + } textsecure.storage.putEncrypted("devices" + number, map); } @@ -353,6 +367,19 @@ window.textsecure.storage = function() { return map === undefined ? [] : map.devices; } + self.getDeviceObject = function(encodedNumber) { + var number = textsecure.utils.unencodeNumber(encodedNumber); + var devices = self.getDeviceObjectsForNumber(number[0]); + if (devices === undefined) + return undefined; + + for (i in devices) + if (devices[i].encodedNumber == encodedNumber) + return devices[i]; + + return undefined; + } + self.removeDeviceIdsForNumber = function(number, deviceIdsToRemove) { var map = textsecure.storage.getEncrypted("devices" + number); if (map === undefined) @@ -363,7 +390,7 @@ window.textsecure.storage = function() { for (i in map.devices) { var keep = true; for (idToRemove in deviceIdsToRemove) - if (map.devices[i].deviceId == idToRemove) + if (map.devices[i].encodedNumber == number + "." + idToRemove) keep = false; if (keep) @@ -556,7 +583,8 @@ window.textsecure.sendMessage = function() { return textsecure.crypto.encryptMessageFor(deviceObjectList[i], message).then(function(encryptedMsg) { jsonData[i] = { type: encryptedMsg.type, - destination: deviceObjectList[i].encodedNumber, + destination: number, + destinationDeviceId: textsecure.utils.unencodeNumber(deviceObjectList[i].encodedNumber)[1], destinationRegistrationId: deviceObjectList[i].registrationId, body: encryptedMsg.body, timestamp: new Date().getTime() diff --git a/js/test.js b/js/test.js index 180890058..7caf66a1d 100644 --- a/js/test.js +++ b/js/test.js @@ -213,10 +213,13 @@ textsecure.registerOnLoadFunction(function() { ourBaseKey: hexToArrayBuffer('192b4892aa2e4cff1293999dc7c367874456c4d920aae7d9d42e5e62c965546c'), ourEphemeralKey: hexToArrayBuffer('f12704787bab04a3cf544ebd9d421b6fe36147519eb5afa7c90e3fb67c141e64'), ourIdentityKey: hexToArrayBuffer('a05fd14abb42ff393004eee526e3167441ee51021c6d801b784720c15637747c'), - theirPreKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e'), - theirPreKeyId: 13845842, registrationId: 11593, - theirIdentityKey: hexToArrayBuffer('05276e4df34557386f67df38b708eeddb1a8924e0428b9eefdc9213c3e8927cc7d'), + getKeys: [{deviceId: 0, + keyId: 13845842, + publicKey: hexToArrayBuffer('05fee424a5b6ccb717d85ef2207e2057ab1144c40afe89cdc80e9c424dd90c146e'), + identityKey: hexToArrayBuffer('05276e4df34557386f67df38b708eeddb1a8924e0428b9eefdc9213c3e8927cc7d'), + registrationId: 42} + ], //expectedPlaintext: hexToArrayBuffer('0a0e4120202020202020202020202020'), //expectedCounter: 0, expectedCiphertext: hexToArrayBuffer('2208d28acd061221059ab4e844771bfeb96382edac5f80e757a1109b5611c770b2ba9f28b363d7c2541a2105bd61aea7fa5304f4dc914892bc3795812cda8bb90b73de9920e22c609cf0ec4e2242220a21058c0c357a3a25e6da46b0186d93fec31d5b86a4ac4973742012d8e9de2346be161000180022104bd27ab87ee151d71cdfe89828050ef4b05bddfb56da491728c95a'), @@ -294,13 +297,13 @@ textsecure.registerOnLoadFunction(function() { }], ]; - var axolotlTestVectors = function(v, remoteDevice) { + var axolotlTestVectors = function(v, remoteNumber) { var origCreateNewKeyPair = textsecure.crypto.testing_only.createNewKeyPair; var doStep; var stepDone; stepDone = function(res) { - if (!res || privKeyQueue.length != 0) { + if (!res || privKeyQueue.length != 0 || Object.keys(getKeysForNumberMap).length != 0 || Object.keys(messagesSentMap).length != 0) { textsecure.crypto.testing_only.createNewKeyPair = origCreateNewKeyPair; return false; } else if (step == v.length) { @@ -338,7 +341,7 @@ textsecure.registerOnLoadFunction(function() { var message = new textsecure.protos.IncomingPushMessageProtobuf(); message.type = data.type; - message.source = textsecure.utils.unencodeNumber(remoteDevice.encodedNumber); + message.source = remoteNumber; message.message = data.message; return textsecure.crypto.handleIncomingPushMessageProto(textsecure.protos.decodeIncomingPushMessageProtobuf(getString(message.encode()))).then(function(res) { return res.body == data.expectedSmsText; @@ -358,29 +361,34 @@ textsecure.registerOnLoadFunction(function() { case "sendMessage": var postLocalKeySetup = function() { - if (data.theirIdentityKey !== undefined) - remoteDevice.identityKey = data.theirIdentityKey; - if (data.theirPreKey !== undefined) { - remoteDevice.publicKey = data.theirPreKey; - remoteDevice.preKeyId = data.theirPreKeyId; + if (data.registrationId !== undefined) textsecure.storage.putUnencrypted("registrationId", data.registrationId); - } + + if (data.getKeys !== undefined) + getKeysForNumberMap[remoteNumber] = data.getKeys; var message = new textsecure.protos.PushMessageContentProtobuf(); message.body = data.smsText; - return textsecure.crypto.encryptMessageFor(remoteDevice, message).then(function(res) { - //XXX: This should be all we do: stepDone(getString(data.expectedCiphertext) == getString(res.body)); - if (res.type == 1) { //XXX: This should be used for everything... - var expectedString = getString(data.expectedCiphertext); - var decoded = textsecure.protos.decodeWhisperMessageProtobuf(expectedString.substring(1, expectedString.length - 8)); - var result = getString(res.body); - return getString(decoded.encode()) == result.substring(1, result.length - 8); - } else { - var decoded = textsecure.protos.decodePreKeyWhisperMessageProtobuf(getString(data.expectedCiphertext).substr(1)); - var result = getString(res.body).substring(1); - return getString(decoded.encode()) == result; - } + return new Promise(function(resolve) { + textsecure.sendMessage([remoteNumber], message, function(res) { + if (res.failure.length != 0 || res.success.length != 1 || res.success[0] != remoteNumber) + return resolve(false); + + var msg = messagesSentMap[remoteNumber + "." + 0]; + delete messagesSentMap[remoteNumber + "." + 0]; + //XXX: This should be all we do: stepDone(getString(data.expectedCiphertext) == getString(res.body)); + if (msg.type == 1) { + var expectedString = getString(data.expectedCiphertext); + var decoded = textsecure.protos.decodeWhisperMessageProtobuf(expectedString.substring(1, expectedString.length - 8)); + var result = getString(msg.body); + resolve(getString(decoded.encode()) == result.substring(1, result.length - 8)); + } else { + var decoded = textsecure.protos.decodePreKeyWhisperMessageProtobuf(getString(data.expectedCiphertext).substr(1)); + var result = getString(msg.body).substring(1); + resolve(getString(decoded.encode()) == result); + } + }); }); } @@ -405,7 +413,7 @@ textsecure.registerOnLoadFunction(function() { } TEST(function() { - return axolotlTestVectors(axolotlTwoPartyTestVectorsAlice, { encodedNumber: "BOB.0" }); + return axolotlTestVectors(axolotlTwoPartyTestVectorsAlice, "BOB"); }, "Standard Axolotl Test Vectors as Alice", true); TEST(function() { @@ -414,11 +422,11 @@ textsecure.registerOnLoadFunction(function() { axolotlTwoPartyTestVectorsAlice[2][1].newEphemeralKey = t.newEphemeralKey; axolotlTwoPartyTestVectorsAlice[3][1] = t; delete axolotlTwoPartyTestVectorsAlice[3][1]['newEphemeralKey']; - return axolotlTestVectors(axolotlTwoPartyTestVectorsAlice, { encodedNumber: "BOB.0" }); + return axolotlTestVectors(axolotlTwoPartyTestVectorsAlice, "BOB"); }, "Shuffled Axolotl Test Vectors as Alice", true); TEST(function() { - return axolotlTestVectors(axolotlTwoPartyTestVectorsBob, { encodedNumber: "ALICE.0" }); + return axolotlTestVectors(axolotlTwoPartyTestVectorsBob, "ALICE"); }, "Standard Axolotl Test Vectors as Bob", true); TEST(function() { @@ -440,7 +448,7 @@ textsecure.registerOnLoadFunction(function() { v[0][1].newEphemeralKey = orig[0][1].newEphemeralKey; v[1][1] = { message: orig[0][1].message, type: orig[0][1].type, expectedSmsText: orig[0][1].expectedSmsText }; - return axolotlTestVectors(v, { encodedNumber: "ALICE.0" }); + return axolotlTestVectors(v, "ALICE"); }, "Shuffled Axolotl Test Vectors as Bob I", true); TEST(function() { @@ -457,7 +465,7 @@ textsecure.registerOnLoadFunction(function() { v[1] = orig[2]; v[2] = orig[1]; - return axolotlTestVectors(v, { encodedNumber: "ALICE.0" }); + return axolotlTestVectors(v, "ALICE"); }, "Shuffled Axolotl Test Vectors as Bob II", true); TEST(function() { @@ -476,7 +484,7 @@ textsecure.registerOnLoadFunction(function() { v[2] = orig[3]; v[3] = orig[4]; - return axolotlTestVectors(v, { encodedNumber: "ALICE.0" }); + return axolotlTestVectors(v, "ALICE"); }, "Shuffled Axolotl Test Vectors as Bob III", true); TEST(function() { @@ -506,7 +514,7 @@ textsecure.registerOnLoadFunction(function() { v[2] = orig[3]; v[3] = orig[4]; - return axolotlTestVectors(v, { encodedNumber: "ALICE.0" }); + return axolotlTestVectors(v, "ALICE"); }, "Shuffled Axolotl Test Vectors as Bob IV", true); TEST(function() { diff --git a/test.html b/test.html index e2433a8e5..957e57ddd 100644 --- a/test.html +++ b/test.html @@ -43,8 +43,7 @@ - - +