diff --git a/js/background.js b/js/background.js index b650b27b1..f36d255b1 100644 --- a/js/background.js +++ b/js/background.js @@ -16,28 +16,148 @@ ;(function() { 'use strict'; + var conversations = new Whisper.ConversationCollection(); + var messages = new Whisper.MessageCollection(); + + if (!localStorage.getItem('first_install_ran')) { + localStorage.setItem('first_install_ran', 1); + extension.navigator.tabs.create("options.html"); + } + + if (textsecure.registration.isDone()) { + init(); + } else { + extension.on('registration_done', init); + } function init() { - if (!localStorage.getItem('first_install_ran')) { - localStorage.setItem('first_install_ran', 1); - extension.navigator.tabs.create("options.html"); - } else { - if (textsecure.registration.isDone()) { - var conversations = new Whisper.ConversationCollection(); - textsecure.subscribeToPush(function(message) { - conversations.addIncomingMessage(message).then(function(message) { - extension.trigger('message', message); + if (!textsecure.registration.isDone()) { return; } + + // initialize the socket and start listening for messages + var socket = textsecure.api.getMessageWebsocket(); + new WebSocketResource(socket, function(request) { + // TODO: handle different types of requests. for now we only expect + // PUT /messages + textsecure.protocol.decryptWebsocketMessage(request.body).then(function(plaintext) { + var proto = textsecure.protobuf.IncomingPushMessageSignal.decode(plaintext); + // After this point, decoding errors are not the server's + // fault, and we should handle them gracefully and tell the + // user they received an invalid message + request.respond(200, 'OK'); + + if (proto.type === textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT) { + onDeliveryReceipt(proto); + } else { + onMessageReceived(proto); + } + + }).catch(function(e) { + console.log("Error handling incoming message:", e); + extension.trigger('error', e); + request.respond(500, 'Bad encrypted websocket message'); + }); + }); + }; + + function onMessageReceived(pushMessage) { + var now = new Date().getTime(); + var timestamp = pushMessage.timestamp.toNumber(); + + var conversation = conversations.add({ + id : pushMessage.source, + type : 'private' + }, { merge : true } ); + + var message = messages.add({ + source : pushMessage.source, + sourceDevice : pushMessage.sourceDevice, + relay : pushMessage.relay, + sent_at : timestamp, + received_at : now, + conversationId : pushMessage.source, + type : 'incoming' + }); + + var newUnreadCount = textsecure.storage.getUnencrypted("unreadCount", 0) + 1; + textsecure.storage.putUnencrypted("unreadCount", newUnreadCount); + extension.navigator.setBadgeText(newUnreadCount); + + conversation.save().then(function() { + message.save().then(function() { + return new Promise(function(resolve) { + resolve(textsecure.protocol.handleIncomingPushMessageProto(pushMessage).then( + function(pushMessageContent) { + handlePushMessageContent(pushMessageContent, message); + } + )); + }).catch(function(e) { + if (e.name === 'IncomingIdentityKeyError') { + e.args.push(message.id); + message.save({ errors : [e] }).then(function() { + extension.trigger('message', message); // notify frontend listeners + }); + } else { + throw e; + } + }); + }); + }); + }; + + extension.on('message:decrypted', function(options) { + var message = messages.add({id: options.message_id}); + message.fetch().then(function() { + var pushMessageContent = handlePushMessageContent( + new textsecure.protobuf.PushMessageContent(options.data), + message + ); + }); + }); + + function handlePushMessageContent(pushMessageContent, message) { + // This function can be called from the background script on an + // incoming message or from the frontend after the user accepts an + // identity key change. + return textsecure.processDecrypted(pushMessageContent).then(function(pushMessageContent) { + var now = new Date().getTime(); + var source = message.get('source'); + var conversationId = pushMessageContent.group ? pushMessageContent.group.id : source; + var conversation = conversations.add({id: conversationId}, {merge: true}); + conversation.fetch().always(function() { + var attributes = { active_at: now }; + if (pushMessageContent.group) { + attributes = { + groupId : pushMessageContent.group.id, + name : pushMessageContent.group.name, + type : 'group', + }; + } else { + attributes = { + name : source, + type : 'private' + }; + } + conversation.set(attributes); + + message.set({ + body : pushMessageContent.body, + conversationId : conversation.id, + attachments : pushMessageContent.attachments, + decrypted_at : now, + errors : [] + }); + + conversation.save().then(function() { + message.save().then(function() { + extension.trigger('message', message); // notify frontend listeners }); - console.log("Got message from " + message.pushMessage.source + "." + message.pushMessage.sourceDevice + - ': "' + getString(message.message.body) + '"'); - var newUnreadCount = textsecure.storage.getUnencrypted("unreadCount", 0) + 1; - textsecure.storage.putUnencrypted("unreadCount", newUnreadCount); - extension.navigator.setBadgeText(newUnreadCount); }); - } - } + }); + }); + } + + function onDeliveryReceipt(pushMessage) { + console.log('delivery receipt', pushMessage.source, timestamp); }; - extension.on('registration_done', init); - init(); })(); diff --git a/js/helpers.js b/js/helpers.js index dc1c6a39c..cf9f4284e 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -126,115 +126,112 @@ window.textsecure.throwHumanError = function(error, type, humanError) { throw e; } -// message_callback({message: decryptedMessage, pushMessage: server-providedPushMessage}) -window.textsecure.subscribeToPush = function(message_callback) { - var socket = textsecure.api.getMessageWebsocket(); - - var resource = new WebSocketResource(socket, function(request) { - // TODO: handle different types of requests. for now we only receive - // PUT /messages - textsecure.protocol.decryptWebsocketMessage(request.body).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 - request.respond(200, 'OK'); - - return textsecure.protocol.handleIncomingPushMessageProto(proto).then(function(decrypted) { - // Delivery receipt - if (decrypted === null) - //TODO: Pass to UI - return; +var handleAttachment = function(attachment) { + function getAttachment() { + return textsecure.api.getAttachment(attachment.id.toString()); + } + + function decryptAttachment(encrypted) { + return textsecure.protocol.decryptAttachment( + encrypted, + attachment.key.toArrayBuffer() + ); + } + + function updateAttachment(data) { + attachment.data = data; + } + + return getAttachment(). + then(decryptAttachment). + then(updateAttachment); +}; + +textsecure.processDecrypted = function(decrypted) { - // Now that its decrypted, validate the message and clean it up for consumer processing - // Note that messages may (generally) only perform one action and we ignore remaining fields - // after the first action. + // Now that its decrypted, validate the message and clean it up for consumer processing + // Note that messages may (generally) only perform one action and we ignore remaining fields + // after the first action. - if (decrypted.flags == null) - decrypted.flags = 0; + if (decrypted.flags == null) + decrypted.flags = 0; + + if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) + == textsecure.protobuf.PushMessageContent.Flags.END_SESSION) + return; + if (decrypted.flags != 0) { + throw new Error("Unknown flags in message"); + } - if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) - == textsecure.protobuf.PushMessageContent.Flags.END_SESSION) + var promises = []; + + if (decrypted.group !== null) { + decrypted.group.id = getString(decrypted.group.id); + var existingGroup = textsecure.storage.groups.getNumbers(decrypted.group.id); + if (existingGroup === undefined) { + if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) { + throw new Error("Got message for unknown group"); + } + textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id); + } else { + var fromIndex = existingGroup.indexOf(proto.source); + + if (fromIndex < 0) { + //TODO: This could be indication of a race... + throw new Error("Sender was not a member of the group they were sending from"); + } + + switch(decrypted.group.type) { + case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE: + if (decrypted.group.avatar !== null) + promises.push(handleAttachment(decrypted.group.avatar)); + + if (existingGroup.filter(function(number) { decrypted.group.members.indexOf(number) < 0 }).length != 0) { + throw new Error("Attempted to remove numbers from group with an UPDATE"); + } + decrypted.group.added = decrypted.group.members.filter(function(number) { return existingGroup.indexOf(number) < 0; }); + + var newGroup = textsecure.storage.groups.addNumbers(decrypted.group.id, decrypted.group.added); + if (newGroup.length != decrypted.group.members.length || + newGroup.filter(function(number) { return decrypted.group.members.indexOf(number) < 0; }).length != 0) { + throw new Error("Error calculating group member difference"); + } + + //TODO: Also follow this path if avatar + name haven't changed (ie we should start storing those) + if (decrypted.group.avatar === null && decrypted.group.added.length == 0 && decrypted.group.name === null) { return; - if (decrypted.flags != 0) - throw new Error("Unknown flags in message"); - - var handleAttachment = function(attachment) { - return textsecure.api.getAttachment(attachment.id.toString()).then(function(encryptedBin) { - return textsecure.protocol.decryptAttachment(encryptedBin, attachment.key.toArrayBuffer()).then(function(decryptedBin) { - attachment.data = decryptedBin; - }); - }); - }; - - var promises = []; - - if (decrypted.group !== null) { - decrypted.group.id = getString(decrypted.group.id); - var existingGroup = textsecure.storage.groups.getNumbers(decrypted.group.id); - if (existingGroup === undefined) { - if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) - throw new Error("Got message for unknown group"); - textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id); - } else { - var fromIndex = existingGroup.indexOf(proto.source); - - if (fromIndex < 0) //TODO: This could be indication of a race... - throw new Error("Sender was not a member of the group they were sending from"); - - switch(decrypted.group.type) { - case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE: - if (decrypted.group.avatar !== null) - promises.push(handleAttachment(decrypted.group.avatar)); - - if (existingGroup.filter(function(number) { decrypted.group.members.indexOf(number) < 0 }).length != 0) - throw new Error("Attempted to remove numbers from group with an UPDATE"); - decrypted.group.added = decrypted.group.members.filter(function(number) { return existingGroup.indexOf(number) < 0; }); - - var newGroup = textsecure.storage.groups.addNumbers(decrypted.group.id, decrypted.group.added); - if (newGroup.length != decrypted.group.members.length || - newGroup.filter(function(number) { return decrypted.group.members.indexOf(number) < 0; }).length != 0) - throw new Error("Error calculating group member difference"); - - //TODO: Also follow this path if avatar + name haven't changed (ie we should start storing those) - if (decrypted.group.avatar === null && decrypted.group.added.length == 0 && decrypted.group.name === null) - return; - - //TODO: Strictly verify all numbers (ie dont let verifyNumber do any user-magic tweaking) - - decrypted.body = null; - decrypted.attachments = []; - - break; - case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT: - textsecure.storage.groups.removeNumber(decrypted.group.id, proto.source); - - decrypted.body = null; - decrypted.attachments = []; - case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER: - decrypted.group.name = null; - decrypted.group.members = []; - decrypted.group.avatar = null; - - break; - default: - throw new Error("Unknown group message type"); - } - } } - for (var i in decrypted.attachments) - promises.push(handleAttachment(decrypted.attachments[i])); - return Promise.all(promises).then(function() { - message_callback({pushMessage: proto, message: decrypted}); - }); - }) - }).catch(function(e) { - // TODO: Show "Invalid message" messages? - console.log("Error handling incoming message: "); - console.log(e); - }); + //TODO: Strictly verify all numbers (ie dont let verifyNumber do any user-magic tweaking) + + decrypted.body = null; + decrypted.attachments = []; + + break; + case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT: + textsecure.storage.groups.removeNumber(decrypted.group.id, proto.source); + + decrypted.body = null; + decrypted.attachments = []; + case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER: + decrypted.group.name = null; + decrypted.group.members = []; + decrypted.group.avatar = null; + + break; + default: + throw new Error("Unknown group message type"); + } + } + } + + for (var i in decrypted.attachments) { + promises.push(handleAttachment(decrypted.attachments[i])); + } + return Promise.all(promises).then(function() { + return decrypted; }); -}; +} window.textsecure.registerSingleDevice = function(number, verificationCode, stepDone) { var signalingKey = textsecure.crypto.getRandomBytes(32 + 20); diff --git a/js/protocol.js b/js/protocol.js index 8d5d9813f..a0d2f067b 100644 --- a/js/protocol.js +++ b/js/protocol.js @@ -362,9 +362,17 @@ window.textsecure.protocol = function() { }); } - var wipeIdentityAndTryMessageAgain = function(from, encodedMessage) { - //TODO: Wipe identity key! - return handlePreKeyWhisperMessage(from, encodedMessage); + var wipeIdentityAndTryMessageAgain = function(from, encodedMessage, message_id) { + // Wipe identity key! + textsecure.storage.removeEncrypted("devices" + from.split('.')[0]); + return handlePreKeyWhisperMessage(from, encodedMessage).then( + function(pushMessageContent) { + extension.trigger('message:decrypted', { + message_id : message_id, + data : pushMessageContent + }); + } + ); } textsecure.replay.registerFunction(wipeIdentityAndTryMessageAgain, textsecure.replay.Type.INIT_SESSION);