diff --git a/Gruntfile.js b/Gruntfile.js index 7a05272e7..e17c9afe3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -59,6 +59,7 @@ module.exports = function(grunt) { 'libtextsecure/account_manager.js', 'libtextsecure/message_receiver.js', 'libtextsecure/sendmessage.js', + 'libtextsecure/contacts_parser.js', ], dest: 'js/libtextsecure.js', }, diff --git a/js/background.js b/js/background.js index 8d45ee3de..b7675e140 100644 --- a/js/background.js +++ b/js/background.js @@ -45,28 +45,65 @@ // initialize the socket and start listening for messages messageReceiver = new textsecure.MessageReceiver(window); - window.addEventListener('signal', function(ev) { - var proto = ev.proto; - if (proto.type === textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT) { - onDeliveryReceipt(proto); - } else { - onMessageReceived(proto); - } - }); + window.addEventListener('contact', onContactReceived); + window.addEventListener('receipt', onDeliveryReceipt); + window.addEventListener('message', onMessageReceived); + window.addEventListener('group', onGroupReceived); + window.addEventListener('sent', onSentMessage); + window.addEventListener('error', onError); messageReceiver.connect(); } - function onMessageReceived(pushMessage) { + function onContactReceived(contactInfo) { + new Whisper.Conversation({ + name: contactInfo.name, + id: contactInfo.number, + avatar: contactInfo.avatar, + type: 'private', + active_at: null + }).save(); + } + + function onGroupReceived(group) { + new Whisper.Conversation({ + members: group.members, + name: group.name, + id: group.id, + avatar: group.avatar, + type: 'group', + active_at: null + }).save(); + } + + function onMessageReceived(ev) { + var data = ev.data; + var message = initIncomingMessage(data.source, data.timestamp); + message.handlePushMessageContent(data.message); + } + + function onSentMessage(ev) { var now = new Date().getTime(); - var timestamp = pushMessage.timestamp.toNumber(); + var data = ev.data; var message = new Whisper.Message({ - source : pushMessage.source, - sourceDevice : pushMessage.sourceDevice, - relay : pushMessage.relay, + source : textsecure.storage.user.getNumber(), + sent_at : data.timestamp, + received_at : now, + conversationId : data.destination, + type : 'outgoing' + }); + + message.handlePushMessageContent(data.message); + } + + function initIncomingMessage(source, timestamp) { + var now = new Date().getTime(); + + var message = new Whisper.Message({ + source : source, sent_at : timestamp, received_at : now, - conversationId : pushMessage.source, + conversationId : source, type : 'incoming' }); @@ -74,36 +111,38 @@ storage.put("unreadCount", newUnreadCount); extension.navigator.setBadgeText(newUnreadCount); - message.save().then(function() { - return new Promise(function(resolve) { - resolve(textsecure.protocol_wrapper.handleIncomingPushMessageProto(pushMessage).then( - function(pushMessageContent) { - message.handlePushMessageContent(pushMessageContent); - } - )); - }).catch(function(e) { - if (e.name === 'IncomingIdentityKeyError') { - message.save({ errors : [e] }).then(function() { - extension.trigger('updateInbox'); - notifyConversation(message); - }); - } else if (e.message === 'Bad MAC') { - message.save({ errors : [ _.pick(e, ['name', 'message'])]}).then(function() { - extension.trigger('updateInbox'); - notifyConversation(message); - }); - } else { - console.log(e); - throw e; - } + return message; + } + + function onError(ev) { + var e = ev.error; + if (!ev.proto) { + console.log(e); + throw e; + } + var envelope = ev.proto; + var message = initIncomingMessage(envelope.source, envelope.timestamp.toNumber()); + if (e.name === 'IncomingIdentityKeyError') { + message.save({ errors : [e] }).then(function() { + extension.trigger('updateInbox'); + notifyConversation(message); }); - }); + } else if (e.message !== 'Bad MAC') { + message.save({ errors : [ _.pick(e, ['name', 'message'])]}).then(function() { + extension.trigger('updateInbox'); + notifyConversation(message); + }); + } else { + console.log(e); + throw e; + } } // lazy hack window.receipts = new Backbone.Collection(); - function onDeliveryReceipt(pushMessage) { + function onDeliveryReceipt(ev) { + var pushMessage = ev.proto; var timestamp = pushMessage.timestamp.toNumber(); var messages = new Whisper.MessageCollection(); var groups = new Whisper.ConversationCollection(); diff --git a/js/libtextsecure.js b/js/libtextsecure.js index 7112d193b..e57db3cde 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -37784,17 +37784,6 @@ axolotlInternal.RecipientRecord = function() { textsecure.storage.axolotl = new AxolotlStore(); var axolotlInstance = axolotl.protocol(textsecure.storage.axolotl); - var decodeMessageContents = function(res) { - var finalMessage = textsecure.protobuf.PushMessageContent.decode(res[0]); - - if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) - == textsecure.protobuf.PushMessageContent.Flags.END_SESSION && - finalMessage.sync !== null) - res[1](); - - return finalMessage; - }; - var handlePreKeyWhisperMessage = function(from, message) { try { return axolotlInstance.handlePreKeyWhisperMessage(from, message); @@ -37810,22 +37799,18 @@ axolotlInternal.RecipientRecord = function() { window.textsecure = window.textsecure || {}; window.textsecure.protocol_wrapper = { - 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 axolotlInstance.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents); - case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE: - if (proto.message.readUint8() != ((3 << 4) | 3)) + decrypt: function(source, sourceDevice, type, blob) { + if (sourceDevice === null) { sourceDevice = 0; } + var fromAddress = [source, sourceDevice].join('.'); + switch(type) { + case textsecure.protobuf.Envelope.Type.CIPHERTEXT: + return axolotlInstance.decryptWhisperMessage(fromAddress, getString(blob)); + case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: + if (blob.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)).then(decodeMessageContents); - case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT: - return Promise.resolve(null); + return handlePreKeyWhisperMessage(fromAddress, getString(blob)); default: - return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); }); + return new Promise.reject(new Error("Unknown message type")); } }, closeOpenSessionForDevice: function(encodedNumber) { @@ -37852,8 +37837,18 @@ axolotlInternal.RecipientRecord = function() { }; var tryMessageAgain = function(from, encodedMessage) { - return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents); - } + return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(function(res) { + var finalMessage = textsecure.protobuf.DataMessage.decode(res[0]); + + if ((finalMessage.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION && + finalMessage.sync !== null) + res[1](); + + return processDecrypted(finalMessage); + }); + }; + textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.INIT_SESSION); })(); @@ -38710,8 +38705,7 @@ window.textsecure.utils = function() { return self; }(); - -var handleAttachment = function(attachment) { +function handleAttachment(attachment) { function getAttachment() { return TextSecureServer.getAttachment(attachment.id.toString()); } @@ -38728,11 +38722,11 @@ var handleAttachment = function(attachment) { } return getAttachment(). - then(decryptAttachment). - then(updateAttachment); -}; + then(decryptAttachment). + then(updateAttachment); +} -textsecure.processDecrypted = function(decrypted, source) { +function processDecrypted(decrypted, source) { // 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 @@ -38741,21 +38735,11 @@ textsecure.processDecrypted = function(decrypted, source) { if (decrypted.flags == null) decrypted.flags = 0; - if (decrypted.sync !== null && textsecure.storage.user.getNumber() != source) { - // Ignore erroneous or malicious sync context from different number - decrypted.sync = null; - } - - if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) - == textsecure.protobuf.PushMessageContent.Flags.END_SESSION) { + if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION) { decrypted.body = null; decrypted.attachments = []; decrypted.group = null; - if (decrypted.sync !== null) { - // We didn't actually close the session - see axolotl_wrapper - // so just throw an error since this message makes no sense - throw new Error("Got a sync END_SESSION message"); - } return Promise.resolve(decrypted); } if (decrypted.flags != 0) { @@ -38768,7 +38752,7 @@ textsecure.processDecrypted = function(decrypted, source) { decrypted.group.id = getString(decrypted.group.id); promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) { if (existingGroup === undefined) { - if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) { + if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) { throw new Error("Got message for unknown group"); } if (decrypted.group.avatar !== null) { @@ -38784,7 +38768,7 @@ textsecure.processDecrypted = function(decrypted, source) { } switch(decrypted.group.type) { - case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE: + case textsecure.protobuf.GroupContext.Type.UPDATE: if (decrypted.group.avatar !== null) promises.push(handleAttachment(decrypted.group.avatar)); @@ -38811,11 +38795,11 @@ textsecure.processDecrypted = function(decrypted, source) { }); break; - case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT: + case textsecure.protobuf.GroupContext.Type.QUIT: decrypted.body = null; decrypted.attachments = []; return textsecure.storage.groups.removeNumber(decrypted.group.id, source); - case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER: + case textsecure.protobuf.GroupContext.Type.DELIVER: decrypted.group.name = null; decrypted.group.members = []; decrypted.group.avatar = null; @@ -38834,7 +38818,7 @@ textsecure.processDecrypted = function(decrypted, source) { return Promise.all(promises).then(function() { return decrypted; }); -}; +} /* vim: ts=4:sw=4:expandtab * @@ -39487,28 +39471,36 @@ function generateKeys(count, progressCallback) { connect: function() { // initialize the socket and start listening for messages this.socket = TextSecureServer.getMessageWebsocket(); - var eventTarget = this.target; - new WebSocketResource(this.socket, function(request) { - // TODO: handle different types of requests. for now we only expect - // PUT /messages - textsecure.crypto.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'); - - var ev = new Event('signal'); - ev.proto = proto; - eventTarget.dispatchEvent(ev); + new WebSocketResource(this.socket, this.handleRequest.bind(this)); + }, + handleRequest: function(request) { + // TODO: handle different types of requests. for now we only expect + // PUT /messages + textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) { + var envelope = textsecure.protobuf.Envelope.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 (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) { + this.onDeliveryReceipt(envelope); + } else if (envelope.content) { + this.handleContentMessage(envelope); + } else if (envelope.legacyMessage) { + this.handleLegacyMessage(envelope); + } else { + throw new Error('Received message with no content and no legacyMessage'); + } - }).catch(function(e) { - console.log("Error handling incoming message:", e); - extension.trigger('error', e); - request.respond(500, 'Bad encrypted websocket message'); - }); - }); + }.bind(this)).catch(function(e) { + request.respond(500, 'Bad encrypted websocket message'); + console.log("Error handling incoming message:", e); + var ev = new Event('error'); + ev.error = e; + this.target.dispatchEvent(ev); + }.bind(this)); }, getStatus: function() { if (this.socket) { @@ -39516,11 +39508,120 @@ function generateKeys(count, progressCallback) { } else { return -1; } + }, + onDeliveryReceipt: function (envelope) { + var ev = new Event('receipt'); + ev.proto = envelope; + this.target.dispatchEvent(ev); + }, + decrypt: function(envelope, ciphertext) { + return textsecure.protocol_wrapper.decrypt( + envelope.source, + envelope.sourceDevice, + envelope.type, + ciphertext + ).catch(function(error) { + var ev = new Event('error'); + ev.error = error; + ev.proto = envelope; + this.target.dispatchEvent(ev); + }.bind(this)); + }, + handleSentMessage: function(destination, timestamp, message) { + var source = textsecure.storage.user.getNumber(); + return processDecrypted(message, source).then(function(message) { + var ev = new Event('sent'); + ev.data = { + destination : destination, + timestamp : timestamp.toNumber(), + message : message + }; + this.target.dispatchEvent(ev); + }.bind(this)); + }, + handleDataMessage: function(envelope, message, close_session) { + if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION ) { + close_session(); + } + return processDecrypted(message, envelope.source).then(function(message) { + var ev = new Event('message'); + ev.data = { + source : envelope.source, + timestamp : envelope.timestamp.toNumber(), + message : message + }; + this.target.dispatchEvent(ev); + }.bind(this)); + }, + handleLegacyMessage: function (envelope) { + return this.decrypt(envelope, envelope.legacyMessage).then(function(result) { + var plaintext = result[0]; // array buffer + var close_session = result[1]; // function + var message = textsecure.protobuf.DataMessage.decode(plaintext); + return this.handleDataMessage(envelope, message, close_session); + }.bind(this)); + }, + handleContentMessage: function (envelope) { + return this.decrypt(envelope, envelope.content).then(function(result) { + var plaintext = result[0]; // array buffer + var close_session = result[1]; // function + var content = textsecure.protobuf.Content.decode(plaintext); + if (content.syncMessage) { + return this.handleSyncMessage(envelope, content.syncMessage); + } else if (content.dataMessage) { + return this.handleDataMessage(envelope, content.dataMessage, close_session); + } else { + throw new Error('Got Content message with no dataMessage and no syncMessage'); + } + }.bind(this)); + }, + handleSyncMessage: function(envelope, syncMessage) { + if (envelope.source !== textsecure.storage.user.getNumber()) { + throw new Error('Received sync message from another number'); + } + if (envelope.sourceDevice == textsecure.storage.user.getDeviceId()) { + throw new Error('Received sync message from our own device'); + } + if (syncMessage.sent) { + var sentMessage = syncMessage.sent; + return this.handleSentMessage( + sentMessage.destination, + sentMessage.timestamp, + sentMessage.message + ); + } else if (syncMessage.contacts) { + this.handleContacts(syncMessage.contacts); + } else if (syncMessage.group) { + this.handleGroup(syncMessage.group); + } else { + throw new Error('Got SyncMessage with no sent, contacts, or group'); + } + }, + handleContacts: function(contacts) { + var eventTarget = this.target; + var attachmentPointer = contacts.blob; + return handleAttachment(attachmentPointer).then(function() { + var contactBuffer = new ContactBuffer(attachmentPointer.data); + var contactInfo = contactBuffer.readContact(); + while (contactInfo !== undefined) { + var ev = new Event('contact'); + ev.contactInfo = contactInfo; + eventTarget.dispatchEvent(ev); + contactInfo = contactBuffer.readContact(); + } + }); + }, + handleGroup: function(envelope) { + var ev = new Event('group'); + ev.group = envelope.group; + this.target.dispatchEvent(ev); } }; textsecure.MessageReceiver = MessageReceiver; + }()); /* vim: ts=4:sw=4:expandtab @@ -39638,11 +39739,11 @@ window.textsecure.messaging = function() { if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race return Promise.reject("Tried to refresh group to non-member"); - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(group.id); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.members = group.numbers; proto.group.name = group.name === undefined ? null : group.name; @@ -39659,7 +39760,7 @@ window.textsecure.messaging = function() { } var tryMessageAgain = function(number, encodedMessage, timestamp) { - var proto = textsecure.protobuf.PushMessageContent.decode(encodedMessage); + var proto = textsecure.protobuf.DataMessage.decode(encodedMessage); return new Promise(function(resolve, reject) { sendMessageProto(timestamp, [number], proto, function(res) { if (res.failure.length > 0) @@ -39705,7 +39806,7 @@ window.textsecure.messaging = function() { doSendMessage = function(number, devicesForNumber, recurse) { var groupUpdate = Promise.resolve(true); - if (message.group && message.group.id && message.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT) + if (message.group && message.group.id && message.group.type != textsecure.protobuf.GroupContext.Type.QUIT) groupUpdate = refreshGroup(number, message.group.id, devicesForNumber); return groupUpdate.then(function() { return sendMessageToDevices(timestamp, number, devicesForNumber, message).then(function(result) { @@ -39771,7 +39872,7 @@ window.textsecure.messaging = function() { } makeAttachmentPointer = function(attachment) { - var proto = new textsecure.protobuf.PushMessageContent.AttachmentPointer(); + var proto = new textsecure.protobuf.AttachmentPointer(); proto.key = textsecure.crypto.getRandomBytes(64); var iv = textsecure.crypto.getRandomBytes(16); @@ -39799,13 +39900,18 @@ window.textsecure.messaging = function() { var myNumber = textsecure.storage.user.getNumber(); var myDevice = textsecure.storage.user.getDeviceId(); if (myDevice != 1) { - var sync_message = textsecure.protobuf.PushMessageContent.decode(message.encode()); - sync_message.sync = new textsecure.protobuf.PushMessageContent.SyncMessageContext(); + var sentMessage = new textsecure.protobuf.SyncMessage.Sent(); + sentMessage.timestamp = timestamp; + sentMessage.message = message; if (destination) { - sync_message.sync.destination = destination; + sentMessage.destination = destination; } - sync_message.sync.timestamp = timestamp; - return sendIndividualProto(myNumber, sync_message, Date.now()); + var syncMessage = new textsecure.protobuf.SyncMessage(); + syncMessage.sent = sentMessage; + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + return sendIndividualProto(myNumber, contentMessage, Date.now()); } } @@ -39827,7 +39933,7 @@ window.textsecure.messaging = function() { } self.sendMessageToNumber = function(number, messageText, attachments, timestamp) { - var proto = new textsecure.protobuf.PushMessageContent(); + var proto = new textsecure.protobuf.DataMessage(); proto.body = messageText; var promises = []; @@ -39842,9 +39948,9 @@ window.textsecure.messaging = function() { } self.closeSession = function(number) { - var proto = new textsecure.protobuf.PushMessageContent(); + var proto = new textsecure.protobuf.DataMessage(); proto.body = "TERMINATE"; - proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION; + proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION; return sendIndividualProto(number, proto, Date.now()).then(function(res) { return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devices) { return Promise.all(devices.map(function(device) { @@ -39857,11 +39963,11 @@ window.textsecure.messaging = function() { } self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) { - var proto = new textsecure.protobuf.PushMessageContent(); + var proto = new textsecure.protobuf.DataMessage(); proto.body = messageText; - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER; + proto.group.type = textsecure.protobuf.GroupContext.Type.DELIVER; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { if (numbers === undefined) @@ -39878,14 +39984,14 @@ window.textsecure.messaging = function() { } self.createGroup = function(numbers, name, avatar) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); return textsecure.storage.groups.createNewGroup(numbers).then(function(group) { proto.group.id = toArrayBuffer(group.id); var numbers = group.numbers; - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.members = numbers; proto.group.name = name; @@ -39905,11 +40011,11 @@ window.textsecure.messaging = function() { } self.updateGroup = function(groupId, name, avatar, numbers) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.name = name; return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) { @@ -39934,10 +40040,10 @@ window.textsecure.messaging = function() { } self.addNumberToGroup = function(groupId, number) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) { if (numbers === undefined) @@ -39949,10 +40055,10 @@ window.textsecure.messaging = function() { } self.setGroupName = function(groupId, name) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.name = name; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { @@ -39965,10 +40071,10 @@ window.textsecure.messaging = function() { } self.setGroupAvatar = function(groupId, avatar) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { if (numbers === undefined) @@ -39983,10 +40089,10 @@ window.textsecure.messaging = function() { } self.leaveGroup = function(groupId) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT; + proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { if (numbers === undefined) @@ -39999,4 +40105,30 @@ window.textsecure.messaging = function() { return self; }(); + +/* + * vim: ts=4:sw=4:expandtab + */ +function ContactBuffer(arrayBuffer) { + this.buffer = new dCodeIO.ByteBuffer(arrayBuffer); +} +ContactBuffer.prototype = { + constructor: ContactBuffer, + readContact: function() { + try { + var len = this.buffer.readVarint32(); + this.buffer.skip(len); + var contactInfo = textsecure.protobuf.ContactDetails.decode( + this.buffer.slice(this.buffer.offset, len) + ); + var attachmentLen = contactInfo.avatar.length; + contactInfo.avatar.data = this.buffer.slice(this.buffer.offset, attachmentLen).toArrayBuffer(true /* copy? */); + this.buffer.skip(attachmentLen); + + return contactInfo; + } catch(e) { + console.log(e); + } + } +}; })(); diff --git a/js/models/conversations.js b/js/models/conversations.js index f988d7f4f..06fc24c12 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -136,7 +136,7 @@ type : 'outgoing', sent_at : now, received_at : now, - flags : textsecure.protobuf.PushMessageContent.Flags.END_SESSION + flags : textsecure.protobuf.DataMessage.Flags.END_SESSION }).save(); } diff --git a/js/models/messages.js b/js/models/messages.js index baf542e65..7bdfcaf3b 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -34,7 +34,7 @@ } }, isEndSession: function() { - var flag = textsecure.protobuf.PushMessageContent.Flags.END_SESSION; + var flag = textsecure.protobuf.DataMessage.Flags.END_SESSION; return !!(this.get('flags') & flag); }, isGroupUpdate: function() { @@ -128,113 +128,103 @@ // identity key change. var message = this; var source = message.get('source'); + var type = source === textsecure.storage.user.getNumber() ? 'outgoing' : 'incoming'; var timestamp = message.get('sent_at'); - return textsecure.processDecrypted(pushMessageContent, source).then(function(pushMessageContent) { - var conversationId = source; - if (pushMessageContent.sync) { - conversationId = pushMessageContent.sync.destination; - } + var conversationId = message.get('conversationId'); + if (pushMessageContent.group) { + conversationId = pushMessageContent.group.id; + } + var conversation = new Whisper.Conversation({id: conversationId}); + conversation.fetch().always(function() { + var now = new Date().getTime(); + var attributes = { type: 'private' }; if (pushMessageContent.group) { - conversationId = pushMessageContent.group.id; - } - var conversation = new Whisper.Conversation({id: conversationId}); - conversation.fetch().always(function() { - var now = new Date().getTime(); - var attributes = { type: 'private' }; - if (pushMessageContent.group) { - var group_update = {}; + var group_update = {}; + attributes = { + type: 'group', + groupId: pushMessageContent.group.id, + }; + if (pushMessageContent.group.type === textsecure.protobuf.GroupContext.Type.UPDATE) { attributes = { - type: 'group', - groupId: pushMessageContent.group.id, + type : 'group', + groupId : pushMessageContent.group.id, + name : pushMessageContent.group.name, + avatar : pushMessageContent.group.avatar, + members : pushMessageContent.group.members, }; - if (pushMessageContent.group.type === textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) { - attributes = { - type : 'group', - groupId : pushMessageContent.group.id, - name : pushMessageContent.group.name, - avatar : pushMessageContent.group.avatar, - members : pushMessageContent.group.members, - }; - group_update = conversation.changedAttributes(_.pick(pushMessageContent.group, 'name', 'avatar')); - var difference = _.difference(pushMessageContent.group.members, conversation.get('members')); - if (difference.length > 0) { - group_update.joined = difference; - } - } - else if (pushMessageContent.group.type === textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT) { - group_update = { left: source }; - attributes.members = _.without(conversation.get('members'), source); - } - - if (_.keys(group_update).length > 0) { - message.set({group_update: group_update}); + group_update = conversation.changedAttributes(_.pick(pushMessageContent.group, 'name', 'avatar')); + var difference = _.difference(pushMessageContent.group.members, conversation.get('members')); + if (difference.length > 0) { + group_update.joined = difference; } } - var type = 'incoming'; - if (pushMessageContent.sync) { - type = 'outgoing'; - timestamp = pushMessageContent.sync.timestamp.toNumber(); + else if (pushMessageContent.group.type === textsecure.protobuf.GroupContext.Type.QUIT) { + group_update = { left: source }; + attributes.members = _.without(conversation.get('members'), source); + } - // lazy hack - check for receipts that arrived early. - if (pushMessageContent.sync.destination) { - var receipt = window.receipts.findWhere({ - timestamp: timestamp, - source: pushMessageContent.sync.destination - }); - if (receipt) { - window.receipts.remove(receipt); + if (_.keys(group_update).length > 0) { + message.set({group_update: group_update}); + } + } + if (type === 'outgoing') { + // lazy hack - check for receipts that arrived early. + if (pushMessageContent.group && pushMessageContent.group.id) { // group sync + var members = conversation.get('members') || []; + var receipts = window.receipts.where({ timestamp: timestamp }); + for (var i in receipts) { + if (members.indexOf(receipts[i].get('source')) > -1) { + window.receipts.remove(receipts[i]); message.set({ delivered: (message.get('delivered') || 0) + 1 }); } - } else if (pushMessageContent.group.id) { // group sync - var members = conversation.get('members') || []; - var receipts = window.receipts.where({ timestamp: timestamp }); - for (var i in receipts) { - if (members.indexOf(receipts[i].get('source')) > -1) { - window.receipts.remove(receipts[i]); - message.set({ - delivered: (message.get('delivered') || 0) + 1 - }); - } - } - } else { - throw new Error('Received sync message with no destination and no group id'); + } + } else { + var receipt = window.receipts.findWhere({ + timestamp: timestamp, + source: conversationId + }); + if (receipt) { + window.receipts.remove(receipt); + message.set({ + delivered: (message.get('delivered') || 0) + 1 + }); } } - attributes.active_at = now; - if (type === 'incoming') { - attributes.unreadCount = conversation.get('unreadCount') + 1; - } - conversation.set(attributes); + } + attributes.active_at = now; + if (type === 'incoming') { + attributes.unreadCount = conversation.get('unreadCount') + 1; + } + conversation.set(attributes); - message.set({ - body : pushMessageContent.body, - conversationId : conversation.id, - attachments : pushMessageContent.attachments, - decrypted_at : now, - type : type, - sent_at : timestamp, - flags : pushMessageContent.flags, - errors : [] - }); + message.set({ + body : pushMessageContent.body, + conversationId : conversation.id, + attachments : pushMessageContent.attachments, + decrypted_at : now, + type : type, + sent_at : timestamp, + flags : pushMessageContent.flags, + errors : [] + }); - if (message.get('sent_at') > conversation.get('timestamp')) { - conversation.set({ - timestamp: message.get('sent_at'), - lastMessage: message.get('body') - }); - } + if (message.get('sent_at') > conversation.get('timestamp')) { + conversation.set({ + timestamp: message.get('sent_at'), + lastMessage: message.get('body') + }); + } - conversation.save().then(function() { - message.save().then(function() { - extension.trigger('updateInbox'); // inbox fetch - if (message.isIncoming()) { - notifyConversation(message); - } else { - updateConversation(conversation.id); - } - }); + conversation.save().then(function() { + message.save().then(function() { + extension.trigger('updateInbox'); // inbox fetch + if (message.isIncoming()) { + notifyConversation(message); + } else { + updateConversation(conversation.id); + } }); }); }); diff --git a/libtextsecure/axolotl_wrapper.js b/libtextsecure/axolotl_wrapper.js index 0219315b9..cafb69eb0 100644 --- a/libtextsecure/axolotl_wrapper.js +++ b/libtextsecure/axolotl_wrapper.js @@ -5,17 +5,6 @@ textsecure.storage.axolotl = new AxolotlStore(); var axolotlInstance = axolotl.protocol(textsecure.storage.axolotl); - var decodeMessageContents = function(res) { - var finalMessage = textsecure.protobuf.PushMessageContent.decode(res[0]); - - if ((finalMessage.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) - == textsecure.protobuf.PushMessageContent.Flags.END_SESSION && - finalMessage.sync !== null) - res[1](); - - return finalMessage; - }; - var handlePreKeyWhisperMessage = function(from, message) { try { return axolotlInstance.handlePreKeyWhisperMessage(from, message); @@ -31,22 +20,18 @@ window.textsecure = window.textsecure || {}; window.textsecure.protocol_wrapper = { - 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 axolotlInstance.decryptWhisperMessage(from, getString(proto.message)).then(decodeMessageContents); - case textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE: - if (proto.message.readUint8() != ((3 << 4) | 3)) + decrypt: function(source, sourceDevice, type, blob) { + if (sourceDevice === null) { sourceDevice = 0; } + var fromAddress = [source, sourceDevice].join('.'); + switch(type) { + case textsecure.protobuf.Envelope.Type.CIPHERTEXT: + return axolotlInstance.decryptWhisperMessage(fromAddress, getString(blob)); + case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: + if (blob.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)).then(decodeMessageContents); - case textsecure.protobuf.IncomingPushMessageSignal.Type.RECEIPT: - return Promise.resolve(null); + return handlePreKeyWhisperMessage(fromAddress, getString(blob)); default: - return new Promise(function(resolve, reject) { reject(new Error("Unknown message type")); }); + return new Promise.reject(new Error("Unknown message type")); } }, closeOpenSessionForDevice: function(encodedNumber) { @@ -73,8 +58,18 @@ }; var tryMessageAgain = function(from, encodedMessage) { - return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(decodeMessageContents); - } + return axolotlInstance.handlePreKeyWhisperMessage(from, encodedMessage).then(function(res) { + var finalMessage = textsecure.protobuf.DataMessage.decode(res[0]); + + if ((finalMessage.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION && + finalMessage.sync !== null) + res[1](); + + return processDecrypted(finalMessage); + }); + }; + textsecure.replay.registerFunction(tryMessageAgain, textsecure.replay.Type.INIT_SESSION); })(); diff --git a/libtextsecure/contacts_parser.js b/libtextsecure/contacts_parser.js new file mode 100644 index 000000000..9790e4589 --- /dev/null +++ b/libtextsecure/contacts_parser.js @@ -0,0 +1,25 @@ +/* + * vim: ts=4:sw=4:expandtab + */ +function ContactBuffer(arrayBuffer) { + this.buffer = new dCodeIO.ByteBuffer(arrayBuffer); +} +ContactBuffer.prototype = { + constructor: ContactBuffer, + readContact: function() { + try { + var len = this.buffer.readVarint32(); + this.buffer.skip(len); + var contactInfo = textsecure.protobuf.ContactDetails.decode( + this.buffer.slice(this.buffer.offset, len) + ); + var attachmentLen = contactInfo.avatar.length; + contactInfo.avatar.data = this.buffer.slice(this.buffer.offset, attachmentLen).toArrayBuffer(true /* copy? */); + this.buffer.skip(attachmentLen); + + return contactInfo; + } catch(e) { + console.log(e); + } + } +}; diff --git a/libtextsecure/helpers.js b/libtextsecure/helpers.js index 4e9eddc8f..49431f34a 100644 --- a/libtextsecure/helpers.js +++ b/libtextsecure/helpers.js @@ -125,8 +125,7 @@ window.textsecure.utils = function() { return self; }(); - -var handleAttachment = function(attachment) { +function handleAttachment(attachment) { function getAttachment() { return TextSecureServer.getAttachment(attachment.id.toString()); } @@ -143,11 +142,11 @@ var handleAttachment = function(attachment) { } return getAttachment(). - then(decryptAttachment). - then(updateAttachment); -}; + then(decryptAttachment). + then(updateAttachment); +} -textsecure.processDecrypted = function(decrypted, source) { +function processDecrypted(decrypted, source) { // 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 @@ -156,21 +155,11 @@ textsecure.processDecrypted = function(decrypted, source) { if (decrypted.flags == null) decrypted.flags = 0; - if (decrypted.sync !== null && textsecure.storage.user.getNumber() != source) { - // Ignore erroneous or malicious sync context from different number - decrypted.sync = null; - } - - if ((decrypted.flags & textsecure.protobuf.PushMessageContent.Flags.END_SESSION) - == textsecure.protobuf.PushMessageContent.Flags.END_SESSION) { + if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION) { decrypted.body = null; decrypted.attachments = []; decrypted.group = null; - if (decrypted.sync !== null) { - // We didn't actually close the session - see axolotl_wrapper - // so just throw an error since this message makes no sense - throw new Error("Got a sync END_SESSION message"); - } return Promise.resolve(decrypted); } if (decrypted.flags != 0) { @@ -183,7 +172,7 @@ textsecure.processDecrypted = function(decrypted, source) { decrypted.group.id = getString(decrypted.group.id); promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) { if (existingGroup === undefined) { - if (decrypted.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE) { + if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) { throw new Error("Got message for unknown group"); } if (decrypted.group.avatar !== null) { @@ -199,7 +188,7 @@ textsecure.processDecrypted = function(decrypted, source) { } switch(decrypted.group.type) { - case textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE: + case textsecure.protobuf.GroupContext.Type.UPDATE: if (decrypted.group.avatar !== null) promises.push(handleAttachment(decrypted.group.avatar)); @@ -226,11 +215,11 @@ textsecure.processDecrypted = function(decrypted, source) { }); break; - case textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT: + case textsecure.protobuf.GroupContext.Type.QUIT: decrypted.body = null; decrypted.attachments = []; return textsecure.storage.groups.removeNumber(decrypted.group.id, source); - case textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER: + case textsecure.protobuf.GroupContext.Type.DELIVER: decrypted.group.name = null; decrypted.group.members = []; decrypted.group.avatar = null; @@ -249,4 +238,4 @@ textsecure.processDecrypted = function(decrypted, source) { return Promise.all(promises).then(function() { return decrypted; }); -}; +} diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index ece9e52ce..7913d78d0 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -31,28 +31,36 @@ connect: function() { // initialize the socket and start listening for messages this.socket = TextSecureServer.getMessageWebsocket(); - var eventTarget = this.target; - new WebSocketResource(this.socket, function(request) { - // TODO: handle different types of requests. for now we only expect - // PUT /messages - textsecure.crypto.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'); + new WebSocketResource(this.socket, this.handleRequest.bind(this)); + }, + handleRequest: function(request) { + // TODO: handle different types of requests. for now we only expect + // PUT /messages + textsecure.crypto.decryptWebsocketMessage(request.body).then(function(plaintext) { + var envelope = textsecure.protobuf.Envelope.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'); - var ev = new Event('signal'); - ev.proto = proto; - eventTarget.dispatchEvent(ev); + if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) { + this.onDeliveryReceipt(envelope); + } else if (envelope.content) { + this.handleContentMessage(envelope); + } else if (envelope.legacyMessage) { + this.handleLegacyMessage(envelope); + } else { + throw new Error('Received message with no content and no legacyMessage'); + } - }).catch(function(e) { - console.log("Error handling incoming message:", e); - extension.trigger('error', e); - request.respond(500, 'Bad encrypted websocket message'); - }); - }); + }.bind(this)).catch(function(e) { + request.respond(500, 'Bad encrypted websocket message'); + console.log("Error handling incoming message:", e); + var ev = new Event('error'); + ev.error = e; + this.target.dispatchEvent(ev); + }.bind(this)); }, getStatus: function() { if (this.socket) { @@ -60,9 +68,118 @@ } else { return -1; } + }, + onDeliveryReceipt: function (envelope) { + var ev = new Event('receipt'); + ev.proto = envelope; + this.target.dispatchEvent(ev); + }, + decrypt: function(envelope, ciphertext) { + return textsecure.protocol_wrapper.decrypt( + envelope.source, + envelope.sourceDevice, + envelope.type, + ciphertext + ).catch(function(error) { + var ev = new Event('error'); + ev.error = error; + ev.proto = envelope; + this.target.dispatchEvent(ev); + }.bind(this)); + }, + handleSentMessage: function(destination, timestamp, message) { + var source = textsecure.storage.user.getNumber(); + return processDecrypted(message, source).then(function(message) { + var ev = new Event('sent'); + ev.data = { + destination : destination, + timestamp : timestamp.toNumber(), + message : message + }; + this.target.dispatchEvent(ev); + }.bind(this)); + }, + handleDataMessage: function(envelope, message, close_session) { + if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION ) { + close_session(); + } + return processDecrypted(message, envelope.source).then(function(message) { + var ev = new Event('message'); + ev.data = { + source : envelope.source, + timestamp : envelope.timestamp.toNumber(), + message : message + }; + this.target.dispatchEvent(ev); + }.bind(this)); + }, + handleLegacyMessage: function (envelope) { + return this.decrypt(envelope, envelope.legacyMessage).then(function(result) { + var plaintext = result[0]; // array buffer + var close_session = result[1]; // function + var message = textsecure.protobuf.DataMessage.decode(plaintext); + return this.handleDataMessage(envelope, message, close_session); + }.bind(this)); + }, + handleContentMessage: function (envelope) { + return this.decrypt(envelope, envelope.content).then(function(result) { + var plaintext = result[0]; // array buffer + var close_session = result[1]; // function + var content = textsecure.protobuf.Content.decode(plaintext); + if (content.syncMessage) { + return this.handleSyncMessage(envelope, content.syncMessage); + } else if (content.dataMessage) { + return this.handleDataMessage(envelope, content.dataMessage, close_session); + } else { + throw new Error('Got Content message with no dataMessage and no syncMessage'); + } + }.bind(this)); + }, + handleSyncMessage: function(envelope, syncMessage) { + if (envelope.source !== textsecure.storage.user.getNumber()) { + throw new Error('Received sync message from another number'); + } + if (envelope.sourceDevice == textsecure.storage.user.getDeviceId()) { + throw new Error('Received sync message from our own device'); + } + if (syncMessage.sent) { + var sentMessage = syncMessage.sent; + return this.handleSentMessage( + sentMessage.destination, + sentMessage.timestamp, + sentMessage.message + ); + } else if (syncMessage.contacts) { + this.handleContacts(syncMessage.contacts); + } else if (syncMessage.group) { + this.handleGroup(syncMessage.group); + } else { + throw new Error('Got SyncMessage with no sent, contacts, or group'); + } + }, + handleContacts: function(contacts) { + var eventTarget = this.target; + var attachmentPointer = contacts.blob; + return handleAttachment(attachmentPointer).then(function() { + var contactBuffer = new ContactBuffer(attachmentPointer.data); + var contactInfo = contactBuffer.readContact(); + while (contactInfo !== undefined) { + var ev = new Event('contact'); + ev.contactInfo = contactInfo; + eventTarget.dispatchEvent(ev); + contactInfo = contactBuffer.readContact(); + } + }); + }, + handleGroup: function(envelope) { + var ev = new Event('group'); + ev.group = envelope.group; + this.target.dispatchEvent(ev); } }; textsecure.MessageReceiver = MessageReceiver; + }()); diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index e4b8ad0cd..517a20dde 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -113,11 +113,11 @@ window.textsecure.messaging = function() { if (numberIndex < 0) // This is potentially a multi-message rare racing-AJAX race return Promise.reject("Tried to refresh group to non-member"); - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(group.id); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.members = group.numbers; proto.group.name = group.name === undefined ? null : group.name; @@ -134,7 +134,7 @@ window.textsecure.messaging = function() { } var tryMessageAgain = function(number, encodedMessage, timestamp) { - var proto = textsecure.protobuf.PushMessageContent.decode(encodedMessage); + var proto = textsecure.protobuf.DataMessage.decode(encodedMessage); return new Promise(function(resolve, reject) { sendMessageProto(timestamp, [number], proto, function(res) { if (res.failure.length > 0) @@ -180,7 +180,7 @@ window.textsecure.messaging = function() { doSendMessage = function(number, devicesForNumber, recurse) { var groupUpdate = Promise.resolve(true); - if (message.group && message.group.id && message.group.type != textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT) + if (message.group && message.group.id && message.group.type != textsecure.protobuf.GroupContext.Type.QUIT) groupUpdate = refreshGroup(number, message.group.id, devicesForNumber); return groupUpdate.then(function() { return sendMessageToDevices(timestamp, number, devicesForNumber, message).then(function(result) { @@ -246,7 +246,7 @@ window.textsecure.messaging = function() { } makeAttachmentPointer = function(attachment) { - var proto = new textsecure.protobuf.PushMessageContent.AttachmentPointer(); + var proto = new textsecure.protobuf.AttachmentPointer(); proto.key = textsecure.crypto.getRandomBytes(64); var iv = textsecure.crypto.getRandomBytes(16); @@ -274,13 +274,18 @@ window.textsecure.messaging = function() { var myNumber = textsecure.storage.user.getNumber(); var myDevice = textsecure.storage.user.getDeviceId(); if (myDevice != 1) { - var sync_message = textsecure.protobuf.PushMessageContent.decode(message.encode()); - sync_message.sync = new textsecure.protobuf.PushMessageContent.SyncMessageContext(); + var sentMessage = new textsecure.protobuf.SyncMessage.Sent(); + sentMessage.timestamp = timestamp; + sentMessage.message = message; if (destination) { - sync_message.sync.destination = destination; + sentMessage.destination = destination; } - sync_message.sync.timestamp = timestamp; - return sendIndividualProto(myNumber, sync_message, Date.now()); + var syncMessage = new textsecure.protobuf.SyncMessage(); + syncMessage.sent = sentMessage; + var contentMessage = new textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + return sendIndividualProto(myNumber, contentMessage, Date.now()); } } @@ -302,7 +307,7 @@ window.textsecure.messaging = function() { } self.sendMessageToNumber = function(number, messageText, attachments, timestamp) { - var proto = new textsecure.protobuf.PushMessageContent(); + var proto = new textsecure.protobuf.DataMessage(); proto.body = messageText; var promises = []; @@ -317,9 +322,9 @@ window.textsecure.messaging = function() { } self.closeSession = function(number) { - var proto = new textsecure.protobuf.PushMessageContent(); + var proto = new textsecure.protobuf.DataMessage(); proto.body = "TERMINATE"; - proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION; + proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION; return sendIndividualProto(number, proto, Date.now()).then(function(res) { return textsecure.storage.devices.getDeviceObjectsForNumber(number).then(function(devices) { return Promise.all(devices.map(function(device) { @@ -332,11 +337,11 @@ window.textsecure.messaging = function() { } self.sendMessageToGroup = function(groupId, messageText, attachments, timestamp) { - var proto = new textsecure.protobuf.PushMessageContent(); + var proto = new textsecure.protobuf.DataMessage(); proto.body = messageText; - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.DELIVER; + proto.group.type = textsecure.protobuf.GroupContext.Type.DELIVER; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { if (numbers === undefined) @@ -353,14 +358,14 @@ window.textsecure.messaging = function() { } self.createGroup = function(numbers, name, avatar) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); return textsecure.storage.groups.createNewGroup(numbers).then(function(group) { proto.group.id = toArrayBuffer(group.id); var numbers = group.numbers; - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.members = numbers; proto.group.name = name; @@ -380,11 +385,11 @@ window.textsecure.messaging = function() { } self.updateGroup = function(groupId, name, avatar, numbers) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.name = name; return textsecure.storage.groups.addNumbers(groupId, numbers).then(function(numbers) { @@ -409,10 +414,10 @@ window.textsecure.messaging = function() { } self.addNumberToGroup = function(groupId, number) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; return textsecure.storage.groups.addNumbers(groupId, [number]).then(function(numbers) { if (numbers === undefined) @@ -424,10 +429,10 @@ window.textsecure.messaging = function() { } self.setGroupName = function(groupId, name) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; proto.group.name = name; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { @@ -440,10 +445,10 @@ window.textsecure.messaging = function() { } self.setGroupAvatar = function(groupId, avatar) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.UPDATE; + proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { if (numbers === undefined) @@ -458,10 +463,10 @@ window.textsecure.messaging = function() { } self.leaveGroup = function(groupId) { - var proto = new textsecure.protobuf.PushMessageContent(); - proto.group = new textsecure.protobuf.PushMessageContent.GroupContext(); + var proto = new textsecure.protobuf.DataMessage(); + proto.group = new textsecure.protobuf.GroupContext(); proto.group.id = toArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.PushMessageContent.GroupContext.Type.QUIT; + proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) { if (numbers === undefined) diff --git a/libtextsecure/test/message_receiver_test.js b/libtextsecure/test/message_receiver_test.js new file mode 100644 index 000000000..2d50e47c7 --- /dev/null +++ b/libtextsecure/test/message_receiver_test.js @@ -0,0 +1,50 @@ +/* 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 . + */ + +describe('MessageReceiver', function() { + var WebSocket = window.WebSocket; + before(function() { window.WebSocket = MockSocket; }); + after (function() { window.WebSocket = WebSocket; }); + it('connects', function(done) { + var mockServer = new MockServer('ws://localhost:8080'); + var attrs = { + type: textsecure.protobuf.Envelope.Type.PLAINTEXT, + source: '+19999999999', + sourceDevice: '1', + timestamp: Date.now(), + }; + mockServer.on('connection', function(server) { + var signal = new textsecure.protobuf.Envelope(attrs); + signal.message = new textsecure.protobuf.DataMessage({ body: 'hello' }); + server.send( + new textsecure.protobuf.WebSocketMessage({ + type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, + request: { verb: 'PUT', path: '/messages', body: signal } + }).encode().toArrayBuffer() + ); + }); + + window.addEventListener('signal', function(ev) { + var signal = ev.proto; + for (var key in attrs) { + assert.strictEqual(attrs[key], signal[key]); + } + assert.strictEqual(signal.message.body, 'hello'); + }); + var messageReceiver = new textsecure.MessageReceiver(window); + messageReceiver.connect(); + }); +}); diff --git a/libtextsecure/test/protocol_test.js b/libtextsecure/test/protocol_test.js index 94b8b0744..f19eea34e 100644 --- a/libtextsecure/test/protocol_test.js +++ b/libtextsecure/test/protocol_test.js @@ -22,7 +22,7 @@ describe('Protocol', function() { it('works', function(done) { localStorage.clear(); - var text_message = new textsecure.protobuf.PushMessageContent(); + var text_message = new textsecure.protobuf.DataMessage(); text_message.body = "Hi Mom"; var server_message = { type: 4, // unencrypted @@ -31,8 +31,12 @@ describe('Protocol', function() { message: text_message.encode() }; - return textsecure.protocol_wrapper.handleIncomingPushMessageProto(server_message). - then(function(message) { + return textsecure.protocol_wrapper.handleEncryptedMessage( + server_message.source, + server_message.source_device, + server_message.type, + server_message.message + ).then(function(message) { assert.equal(message.body, text_message.body); assert.equal(message.attachments.length, text_message.attachments.length); assert.equal(text_message.attachments.length, 0); diff --git a/protos/IncomingPushMessageSignal.proto b/protos/IncomingPushMessageSignal.proto index 8ed5f064b..3499196c9 100644 --- a/protos/IncomingPushMessageSignal.proto +++ b/protos/IncomingPushMessageSignal.proto @@ -1,52 +1,32 @@ package textsecure; -option java_package = "org.whispersystems.textsecure.push"; -option java_outer_classname = "PushMessageProtos"; +option java_package = "org.whispersystems.textsecure.internal.push"; +option java_outer_classname = "TextSecureProtos"; -message IncomingPushMessageSignal { +message Envelope { enum Type { UNKNOWN = 0; CIPHERTEXT = 1; KEY_EXCHANGE = 2; PREKEY_BUNDLE = 3; - PLAINTEXT = 4; RECEIPT = 5; } - optional Type type = 1; - optional string source = 2; - optional uint32 sourceDevice = 7; - optional string relay = 3; - optional uint64 timestamp = 5; - optional bytes message = 6; // Contains an encrypted PushMessageContent -// repeated string destinations = 4; // No longer supported -} - -message PushMessageContent { - message AttachmentPointer { - optional fixed64 id = 1; - optional string contentType = 2; - optional bytes key = 3; - } - message GroupContext { - enum Type { - UNKNOWN = 0; - UPDATE = 1; - DELIVER = 2; - QUIT = 3; - } - optional bytes id = 1; - optional Type type = 2; - optional string name = 3; - repeated string members = 4; - optional AttachmentPointer avatar = 5; - } + optional Type type = 1; + optional string source = 2; + optional uint32 sourceDevice = 7; + optional string relay = 3; + optional uint64 timestamp = 5; + optional bytes legacyMessage = 6; // Contains an encrypted DataMessage + optional bytes content = 8; // Contains an encrypted Content +} - message SyncMessageContext { - optional string destination = 1; - optional uint64 timestamp = 2; - } +message Content { + optional DataMessage dataMessage = 1; + optional SyncMessage syncMessage = 2; +} +message DataMessage { enum Flags { END_SESSION = 1; } @@ -55,5 +35,55 @@ message PushMessageContent { repeated AttachmentPointer attachments = 2; optional GroupContext group = 3; optional uint32 flags = 4; - optional SyncMessageContext sync = 5; +} + +message SyncMessage { + message Sent { + optional string destination = 1; + optional uint64 timestamp = 2; + optional DataMessage message = 3; + } + + message Contacts { + optional AttachmentPointer blob = 1; + } + + message Group { + optional GroupContext group = 1; + } + + optional Sent sent = 1; + optional Contacts contacts = 2; + optional Group group = 3; +} + +message AttachmentPointer { + optional fixed64 id = 1; + optional string contentType = 2; + optional bytes key = 3; +} + +message GroupContext { + enum Type { + UNKNOWN = 0; + UPDATE = 1; + DELIVER = 2; + QUIT = 3; + } + optional bytes id = 1; + optional Type type = 2; + optional string name = 3; + repeated string members = 4; + optional AttachmentPointer avatar = 5; +} + +message ContactDetails { + message Avatar { + optional string contentType = 1; + optional uint64 length = 2; + } + + optional string number = 1; + optional string name = 2; + optional Avatar avatar = 3; }