diff --git a/background.html b/background.html index bd98708944e..16b537e779a 100644 --- a/background.html +++ b/background.html @@ -14,8 +14,6 @@ - - @@ -33,9 +31,12 @@ + + + + - diff --git a/js/api.js b/js/api.js index 41226af0856..018dc883fd0 100644 --- a/js/api.js +++ b/js/api.js @@ -104,6 +104,8 @@ window.textsecure.api = function() { textsecure.throwHumanError(code, "HTTPError", "The server rejected our query, please file a bug report."); } } catch (e) { + if (jqXHR.responseJSON) + e.response = jqXHR.responseJSON; reject(e); } } diff --git a/js/crypto.js b/js/crypto.js index 71f0337d820..75a07b20d8c 100644 --- a/js/crypto.js +++ b/js/crypto.js @@ -238,12 +238,6 @@ window.textsecure.crypto = function() { 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 - self.forceRemoveAllSessions = function(encodedNumber) { - textsecure.storage.removeEncrypted("session" + encodedNumber); - } - - /***************************** *** Internal Crypto stuff *** *****************************/ @@ -407,7 +401,26 @@ window.textsecure.crypto = function() { session.indexInfo.closed = new Date().getTime(); } - var initSessionFromPreKeyWhisperMessage = function(encodedNumber, message) { + var initSessionFromPreKeyWhisperMessage; + var decryptWhisperMessage; + var handlePreKeyWhisperMessage = function(from, encodedMessage) { + var preKeyProto = textsecure.protos.decodePreKeyWhisperMessageProtobuf(encodedMessage); + return initSessionFromPreKeyWhisperMessage(from, preKeyProto).then(function(sessions) { + return decryptWhisperMessage(from, getString(preKeyProto.message), sessions[0], preKeyProto.registrationId).then(function(result) { + if (sessions[1] !== undefined) + crypto_storage.saveSession(from, 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.getAndRemovePreKeyPair(message.preKeyId); var session = crypto_storage.getSessionOrIdentityKeyByBaseKey(encodedNumber, toArrayBuffer(message.baseKey)); @@ -428,8 +441,7 @@ window.textsecure.crypto = function() { 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 - // TODO: Save the message for possible later renegotiation - textsecure.throwHumanError("Received message with unknown identity key", "WarnTryAgainError", "The identity of the sender has changed. This may be malicious, or the sender may have simply reinstalled TextSecure."); + throw textsecure.createTryAgainError("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, encodedNumber, toArrayBuffer(message.identityKey), toArrayBuffer(message.baseKey)) @@ -525,7 +537,7 @@ window.textsecure.crypto = function() { } // returns decrypted protobuf - var decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) { + decryptWhisperMessage = function(encodedNumber, messageBytes, session, registrationId) { if (messageBytes[0] != String.fromCharCode((2 << 4) | 2)) throw new Error("Bad version number on WhisperMessage"); @@ -618,14 +630,7 @@ window.textsecure.crypto = function() { if (proto.message.readUint8() != (2 << 4 | 2)) throw new Error("Bad version byte"); 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], preKeyProto.registrationId).then(function(result) { - if (sessions[1] !== undefined) - crypto_storage.saveSession(proto.source, sessions[1]); - return result; - }); - }); + return handlePreKeyWhisperMessage(from, getString(proto.message)); } } diff --git a/js/helpers.js b/js/helpers.js index d17075a300d..421c3650b33 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -389,8 +389,8 @@ window.textsecure.storage = function() { var devicesRemoved = 0; for (i in map.devices) { var keep = true; - for (idToRemove in deviceIdsToRemove) - if (map.devices[i].encodedNumber == number + "." + idToRemove) + for (j in deviceIdsToRemove) + if (map.devices[i].encodedNumber == number + "." + deviceIdsToRemove[j]) keep = false; if (keep) @@ -459,6 +459,41 @@ window.textsecure.nacl = function() { //TODO: Some kind of textsecure.init(use_nacl) window.textsecure.registerOnLoadFunction = window.textsecure.nacl.registerOnLoadFunction; +window.textsecure.replay = function() { + var self = {}; + + self.REPLAY_FUNCS = { + SEND_MESSAGE: 1, + INIT_SESSION: 2, + } + + var functions = {}; + + self.registerReplayFunction = function(func, functionCode) { + functions[functionCode] = func; + } + + self.replayError = function(replayData) { + var args = Array.prototype.slice.call(arguments); + args.shift(); + args = replayData.args.concat(args); + functions[replayData.replayFunction].apply(window, args); + } + + self.createReplayableError = function(shortMsg, longMsg, replayFunction, args) { + var e = new Error(shortMsg); + e.name = "ReplayableError"; + e.humanError = e.longMessage = longMsg; + e.replayData = { replayFunction: replayFunction, args: args }; + e.replay = function() { + self.replayError(e.replayData); + } + return e; + } + + return self; +}(); + // message_callback({message: decryptedMessage, pushMessage: server-providedPushMessage}) window.textsecure.subscribeToPush = function() { var subscribeToPushMessageSemaphore = 0; @@ -539,13 +574,13 @@ window.textsecure.sendMessage = function() { var identityKey = getString(response[0].identityKey); for (i in response) if (getString(response[i].identityKey) != identityKey) - throw new Error("Identity key changed"); + throw new Error("Identity key not consistent"); for (i in response) { var updateDevice = (updateDevices === undefined); if (!updateDevice) - for (deviceId in updateDevices) - if (deviceId == response[i].deviceId) + for (j in updateDevices) + if (updateDevices[j] == response[i].deviceId) updateDevice = true; if (updateDevice) @@ -602,6 +637,13 @@ window.textsecure.sendMessage = function() { }); } + var tryMessageAgain = function(number, encodedMessage, callback) { + //TODO: Wipe identity key! + var message = textsecure.protos.decodePushMessageContentProtobuf(encodedMessage); + textsecure.sendMessage([number], message, callback); + } + textsecure.replay.registerReplayFunction(tryMessageAgain, textsecure.replay.SEND_MESSAGE); + return function(numbers, message, callback) { var numbersCompleted = 0; var errors = []; @@ -620,30 +662,41 @@ window.textsecure.sendMessage = function() { numberCompleted(); } - var doSendMessage = function(number, devicesForNumber, message) { + var doSendMessage; + var reloadDevicesAndSend = function(number, recurse) { + return function() { + var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number); + if (devicesForNumber.length == 0) + registerError(number, "Go empty device list when loading device keys", null); + else + doSendMessage(number, devicesForNumber, recurse); + } + } + + doSendMessage = function(number, devicesForNumber, recurse) { return sendMessageToDevices(number, devicesForNumber, message).then(function(result) { successfulNumbers[successfulNumbers.length] = number; numberCompleted(); }).catch(function(error) { if (error instanceof Error && error.name == "HTTPError" && (error.message == 410 || error.message == 409)) { + if (!recurse) + return registerError(number, "Hit retry limit attempting to reload device list", error); + + if (error.message == 409) + textsecure.storage.devices.removeDeviceIdsForNumber(number, error.response.extraDevices); + var resetDevices = ((error.message == 410) ? error.response.staleDevices : error.response.missingDevices); - getKeysForNumber(number, resetDevices).then(function() { - if (error.message == 409) - resetDevices = resetDevices.concat(error.response.extraDevices); - - textsecure.storage.devices.removeDeviceIdsForNumber(number, resetDevices); - for (i in resetDevices) - textsecure.crypto.forceRemoveAllSessions(number + "." + resetDevices[i]); - - //TODO: Try again - }).catch(function(error) { - if (error.message !== "Identity key changed") - registerError(number, "Failed to reload device keys", error); - else { - // TODO: Identity key changed, check which devices it changed for and get upset - registerError(number, "Identity key changed!!!!", error); - } - }); + getKeysForNumber(number, resetDevices) + .then(reloadDevicesAndSend(number, false)) + .catch(function(error) { + if (error.message !== "Identity key changed") + registerError(number, "Failed to reload device keys", error); + else { + error = textsecure.replay.createReplayableError("The destination's identity key has changed", "The identity of the destination has changed. This may be malicious, or the destination may have simply reinstalled TextSecure.", + textsecure.replay.SEND_MESSAGE, [number, getString(message.encode())]); + registerError(number, "Identity key changed", error); + } + }); } else registerError(number, "Failed to create or send message", error); }); @@ -654,17 +707,13 @@ window.textsecure.sendMessage = function() { var devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number); if (devicesForNumber.length == 0) { - getKeysForNumber(number).then(function() { - devicesForNumber = textsecure.storage.devices.getDeviceObjectsForNumber(number); - if (devicesForNumber.length == 0) - registerError(number, "Failed to retreive new device keys for number " + number, null); - else - doSendMessage(number, devicesForNumber, message); - }).catch(function(error) { - registerError(number, "Failed to retreive new device keys for number " + number, error); - }); + getKeysForNumber(number) + .then(reloadDevicesAndSend(number, true)) + .catch(function(error) { + registerError(number, "Failed to retreive new device keys for number " + number, error); + }); } else - doSendMessage(number, devicesForNumber, message); + doSendMessage(number, devicesForNumber, true); } } }(); diff --git a/options.html b/options.html index 13d928edf46..7be44e47b98 100644 --- a/options.html +++ b/options.html @@ -38,7 +38,6 @@ - @@ -53,9 +52,16 @@ - - + + + + + + + + + diff --git a/popup.html b/popup.html index 6f45898b053..26e033cd4d0 100644 --- a/popup.html +++ b/popup.html @@ -44,19 +44,8 @@ - - - - - - - - - - - @@ -69,8 +58,22 @@ + + + + + + + + + + + + + + diff --git a/test.html b/test.html index 957e57dddf3..81b94cbd7a2 100644 --- a/test.html +++ b/test.html @@ -29,20 +29,27 @@ - + + + + + - + + + +