diff --git a/js/libtextsecure.js b/js/libtextsecure.js index cbae0710..9468c764 100644 --- a/js/libtextsecure.js +++ b/js/libtextsecure.js @@ -38628,114 +38628,6 @@ window.textsecure.utils = function() { return self; }(); -function handleAttachment(attachment) { - function getAttachment() { - return TextSecureServer.getAttachment(attachment.id.toString()); - } - - function decryptAttachment(encrypted) { - return textsecure.crypto.decryptAttachment( - encrypted, - attachment.key.toArrayBuffer() - ); - } - - function updateAttachment(data) { - attachment.data = data; - } - - return getAttachment(). - then(decryptAttachment). - then(updateAttachment); -} - -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 - // after the first action. - - if (decrypted.flags == null) - decrypted.flags = 0; - - if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) - == textsecure.protobuf.DataMessage.Flags.END_SESSION) { - decrypted.body = null; - decrypted.attachments = []; - decrypted.group = null; - return Promise.resolve(decrypted); - } - if (decrypted.flags != 0) { - throw new Error("Unknown flags in message"); - } - - var promises = []; - - if (decrypted.group !== null) { - decrypted.group.id = getString(decrypted.group.id); - - if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) { - if (decrypted.group.avatar !== null) { - promises.push(handleAttachment(decrypted.group.avatar)); - } - } - - promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) { - if (existingGroup === undefined) { - if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) { - throw new Error("Got message for unknown group"); - } - return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id); - } else { - var fromIndex = existingGroup.indexOf(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.GroupContext.Type.UPDATE: - return textsecure.storage.groups.updateNumbers( - decrypted.group.id, decrypted.group.members - ).then(function(added) { - decrypted.group.added = added; - - if (decrypted.group.avatar === null && - decrypted.group.added.length == 0 && - decrypted.group.name === null) { - return; - } - - decrypted.body = null; - decrypted.attachments = []; - }); - - break; - case textsecure.protobuf.GroupContext.Type.QUIT: - decrypted.body = null; - decrypted.attachments = []; - return textsecure.storage.groups.removeNumber(decrypted.group.id, source); - case textsecure.protobuf.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; - }); -} /* * vim: ts=4:sw=4:expandtab @@ -39289,303 +39181,400 @@ var TextSecureServer = (function() { * vim: ts=4:sw=4:expandtab */ -;(function () { - 'use strict'; - window.textsecure = window.textsecure || {}; +function MessageReceiver(url, username, password, signalingKey) { + this.url = url; + this.signalingKey = signalingKey; + this.username = username; + this.password = password; + this.server = new TextSecureServer(url, username, password); - function MessageReceiver(url, username, password, signalingKey) { - this.url = url; - this.signalingKey = signalingKey; - this.username = username; - this.password = password; - this.server = new TextSecureServer(url, username, password); - - var unencoded = textsecure.utils.unencodeNumber(username); - this.number = unencoded[0]; - this.deviceId = unencoded[1]; - this.connect(); - } - - MessageReceiver.prototype = { - constructor: MessageReceiver, - connect: function() { - if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { - this.socket.close(); - } - console.log('opening websocket'); - // initialize the socket and start listening for messages - this.socket = this.server.getMessageSocket(); - this.socket.onclose = this.onclose.bind(this); - this.socket.onerror = this.onerror.bind(this); - this.socket.onopen = this.onopen.bind(this); - this.wsr = new WebSocketResource(this.socket, { - handleRequest: this.handleRequest.bind(this), - keepalive: { path: '/v1/keepalive', disconnect: true } - }); - }, - close: function() { - this.wsr.close(); - delete this.listeners; - }, - onopen: function() { - console.log('websocket open'); - }, - onerror: function(error) { - console.log('websocket error'); - }, - onclose: function(ev) { - var eventTarget = this; - console.log('websocket closed', ev.code); - // possible 403 or network issue. Make an request to confirm - this.server.getDevices(this.number). - then(this.connect.bind(this)). // No HTTP error? Reconnect - catch(function(e) { - var ev = new Event('error'); - ev.error = e; - eventTarget.dispatchEvent(ev); - }); - }, - handleRequest: function(request) { - this.wsr.resetKeepAliveTimer(); - // TODO: handle different types of requests. for now we only expect - // PUT /messages - textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).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'); - } - - }.bind(this)).catch(function(e) { - request.respond(500, 'Bad encrypted websocket message'); - console.log("Error handling incoming message:", e); + var unencoded = textsecure.utils.unencodeNumber(username); + this.number = unencoded[0]; + this.deviceId = unencoded[1]; +} +MessageReceiver.prototype = { + constructor: MessageReceiver, + connect: function() { + if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { + this.socket.close(); + } + console.log('opening websocket'); + // initialize the socket and start listening for messages + this.socket = this.server.getMessageSocket(); + this.socket.onclose = this.onclose.bind(this); + this.socket.onerror = this.onerror.bind(this); + this.socket.onopen = this.onopen.bind(this); + this.wsr = new WebSocketResource(this.socket, { + handleRequest: this.handleRequest.bind(this), + keepalive: { path: '/v1/keepalive', disconnect: true } + }); + }, + close: function() { + this.wsr.close(); + delete this.listeners; + }, + onopen: function() { + console.log('websocket open'); + }, + onerror: function(error) { + console.log('websocket error'); + }, + onclose: function(ev) { + var eventTarget = this; + console.log('websocket closed', ev.code); + // possible 403 or network issue. Make an request to confirm + this.server.getDevices(this.number). + then(this.connect.bind(this)). // No HTTP error? Reconnect + catch(function(e) { var ev = new Event('error'); ev.error = e; - this.dispatchEvent(ev); - }.bind(this)); - }, - getStatus: function() { - if (this.socket) { - return this.socket.readyState; + eventTarget.dispatchEvent(ev); + }); + }, + handleRequest: function(request) { + this.wsr.resetKeepAliveTimer(); + // TODO: handle different types of requests. for now we only expect + // PUT /messages + textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).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 { - return -1; + throw new Error('Received message with no content and no legacyMessage'); } - }, - onDeliveryReceipt: function (envelope) { - var ev = new Event('receipt'); + + }.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.dispatchEvent(ev); + }.bind(this)); + }, + getStatus: function() { + if (this.socket) { + return this.socket.readyState; + } else { + return -1; + } + }, + onDeliveryReceipt: function (envelope) { + var ev = new Event('receipt'); + ev.proto = envelope; + this.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.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.dispatchEvent(ev); - throw error; // reject this promise - }.bind(this)); - }, - handleSentMessage: function(destination, timestamp, message) { - return processDecrypted(message, this.number).then(function(message) { - var ev = new Event('sent'); - ev.data = { - destination : destination, - timestamp : timestamp.toNumber(), - message : message - }; - this.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.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 !== this.number) { - throw new Error('Received sync message from another number'); - } - if (envelope.sourceDevice == this.deviceId) { - 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.groups) { - this.handleGroups(syncMessage.groups); + throw error; // reject this promise + }.bind(this)); + }, + handleSentMessage: function(destination, timestamp, message) { + return this.processDecrypted(message, this.number).then(function(message) { + var ev = new Event('sent'); + ev.data = { + destination : destination, + timestamp : timestamp.toNumber(), + message : message + }; + this.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 this.processDecrypted(message, envelope.source).then(function(message) { + var ev = new Event('message'); + ev.data = { + source : envelope.source, + timestamp : envelope.timestamp.toNumber(), + message : message + }; + this.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 SyncMessage with no sent, contacts, or groups'); + throw new Error('Got Content message with no dataMessage and no syncMessage'); } - }, - handleContacts: function(contacts) { - var eventTarget = this; - var attachmentPointer = contacts.blob; - return handleAttachment(attachmentPointer).then(function() { - var contactBuffer = new ContactBuffer(attachmentPointer.data); - var contactDetails = contactBuffer.next(); - while (contactDetails !== undefined) { - var ev = new Event('contact'); - ev.contactDetails = contactDetails; - eventTarget.dispatchEvent(ev); - contactDetails = contactBuffer.next(); - } - eventTarget.dispatchEvent(new Event('contactsync')); - }); - }, - handleGroups: function(groups) { - var eventTarget = this; - var attachmentPointer = groups.blob; - return handleAttachment(attachmentPointer).then(function() { - var groupBuffer = new GroupBuffer(attachmentPointer.data); - var groupDetails = groupBuffer.next(); - while (groupDetails !== undefined) { - (function(groupDetails) { - groupDetails.id = getString(groupDetails.id); - textsecure.storage.groups.getGroup(groupDetails.id). - then(function(existingGroup) { - if (existingGroup === undefined) { - return textsecure.storage.groups.createNewGroup( - groupDetails.members, groupDetails.id - ); - } else { - return textsecure.storage.groups.updateNumbers( - groupDetails.id, groupDetails.members - ); - } - }).then(function() { - var ev = new Event('group'); - ev.groupDetails = groupDetails; - eventTarget.dispatchEvent(ev); - }); - })(groupDetails); - groupDetails = groupBuffer.next(); - } - }); - }, - - /* Implements EventTarget */ - dispatchEvent: function(ev) { - if (!(ev instanceof Event)) { - throw new Error('Expects an event'); + }.bind(this)); + }, + handleSyncMessage: function(envelope, syncMessage) { + if (envelope.source !== this.number) { + throw new Error('Received sync message from another number'); + } + if (envelope.sourceDevice == this.deviceId) { + 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.groups) { + this.handleGroups(syncMessage.groups); + } else { + throw new Error('Got SyncMessage with no sent, contacts, or groups'); + } + }, + handleContacts: function(contacts) { + var eventTarget = this; + var attachmentPointer = contacts.blob; + return this.handleAttachment(attachmentPointer).then(function() { + var contactBuffer = new ContactBuffer(attachmentPointer.data); + var contactDetails = contactBuffer.next(); + while (contactDetails !== undefined) { + var ev = new Event('contact'); + ev.contactDetails = contactDetails; + eventTarget.dispatchEvent(ev); + contactDetails = contactBuffer.next(); } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; + eventTarget.dispatchEvent(new Event('contactsync')); + }); + }, + handleGroups: function(groups) { + var eventTarget = this; + var attachmentPointer = groups.blob; + return this.handleAttachment(attachmentPointer).then(function() { + var groupBuffer = new GroupBuffer(attachmentPointer.data); + var groupDetails = groupBuffer.next(); + while (groupDetails !== undefined) { + (function(groupDetails) { + groupDetails.id = getString(groupDetails.id); + textsecure.storage.groups.getGroup(groupDetails.id). + then(function(existingGroup) { + if (existingGroup === undefined) { + return textsecure.storage.groups.createNewGroup( + groupDetails.members, groupDetails.id + ); + } else { + return textsecure.storage.groups.updateNumbers( + groupDetails.id, groupDetails.members + ); + } + }).then(function() { + var ev = new Event('group'); + ev.groupDetails = groupDetails; + eventTarget.dispatchEvent(ev); + }); + })(groupDetails); + groupDetails = groupBuffer.next(); } - var listeners = this.listeners[ev.type]; - if (typeof listeners === 'object') { - for (var i=0; i < listeners.length; ++i) { - if (typeof listeners[i] === 'function') { - listeners[i].call(null, ev); - } - } - } - }, - addEventListener: function(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - var listeners = this.listeners[eventName]; - if (typeof listeners !== 'object') { - listeners = []; - } - listeners.push(callback); - this.listeners[eventName] = listeners; - }, - removeEventListener: function(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - var listeners = this.listeners[eventName]; - for (var i=0; i < listeners.length; ++ i) { - if (listeners[i] === callback) { - listeners.splice(i, 1); - return; - } - } - this.listeners[eventName] = listeners; + }); + }, + handleAttachment: function(attachment) { + function decryptAttachment(encrypted) { + return textsecure.crypto.decryptAttachment( + encrypted, + attachment.key.toArrayBuffer() + ); } - }; + function updateAttachment(data) { + attachment.data = data; + } - textsecure.MessageReceiver = function(url, username, password, signalingKey) { - var messageReceiver = new MessageReceiver(url, username, password, signalingKey); + return this.server.getAttachment(attachment.id.toString()). + then(decryptAttachment). + then(updateAttachment); + }, + processDecrypted: function(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 + // after the first action. - this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); - this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); - this.getStatus = messageReceiver.getStatus.bind(messageReceiver); - this.close = messageReceiver.close.bind(messageReceiver); - }; + if (decrypted.flags == null) + decrypted.flags = 0; - textsecure.MessageReceiver.prototype = { - constructor: textsecure.MessageReceiver - }; + if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION) { + decrypted.body = null; + decrypted.attachments = []; + decrypted.group = null; + return Promise.resolve(decrypted); + } + if (decrypted.flags != 0) { + throw new Error("Unknown flags in message"); + } + + var promises = []; + + if (decrypted.group !== null) { + decrypted.group.id = getString(decrypted.group.id); + + if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) { + if (decrypted.group.avatar !== null) { + promises.push(this.handleAttachment(decrypted.group.avatar)); + } + } + + promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) { + if (existingGroup === undefined) { + if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) { + throw new Error("Got message for unknown group"); + } + return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id); + } else { + var fromIndex = existingGroup.indexOf(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.GroupContext.Type.UPDATE: + return textsecure.storage.groups.updateNumbers( + decrypted.group.id, decrypted.group.members + ).then(function(added) { + decrypted.group.added = added; + + if (decrypted.group.avatar === null && + decrypted.group.added.length == 0 && + decrypted.group.name === null) { + return; + } + + decrypted.body = null; + decrypted.attachments = []; + }); + + break; + case textsecure.protobuf.GroupContext.Type.QUIT: + decrypted.body = null; + decrypted.attachments = []; + return textsecure.storage.groups.removeNumber(decrypted.group.id, source); + case textsecure.protobuf.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(this.handleAttachment(decrypted.attachments[i])); + } + return Promise.all(promises).then(function() { + return decrypted; + }); + }, + + /* Implements EventTarget */ + dispatchEvent: function(ev) { + if (!(ev instanceof Event)) { + throw new Error('Expects an event'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[ev.type]; + if (typeof listeners === 'object') { + for (var i=0; i < listeners.length; ++i) { + if (typeof listeners[i] === 'function') { + listeners[i].call(null, ev); + } + } + } + }, + addEventListener: function(eventName, callback) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[eventName]; + if (typeof listeners !== 'object') { + listeners = []; + } + listeners.push(callback); + this.listeners[eventName] = listeners; + }, + removeEventListener: function(eventName, callback) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[eventName]; + for (var i=0; i < listeners.length; ++ i) { + if (listeners[i] === callback) { + listeners.splice(i, 1); + return; + } + } + this.listeners[eventName] = listeners; + } + +}; + +window.textsecure = window.textsecure || {}; + +textsecure.MessageReceiver = function(url, username, password, signalingKey) { + var messageReceiver = new MessageReceiver(url, username, password, signalingKey); + this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); + this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); + this.getStatus = messageReceiver.getStatus.bind(messageReceiver); + this.close = messageReceiver.close.bind(messageReceiver); + messageReceiver.connect(); +}; + +textsecure.MessageReceiver.prototype = { + constructor: textsecure.MessageReceiver +}; -}()); /* * vim: ts=4:sw=4:expandtab diff --git a/libtextsecure/helpers.js b/libtextsecure/helpers.js index 46a14f1f..49dad8d4 100644 --- a/libtextsecure/helpers.js +++ b/libtextsecure/helpers.js @@ -113,111 +113,3 @@ window.textsecure.utils = function() { return self; }(); -function handleAttachment(attachment) { - function getAttachment() { - return TextSecureServer.getAttachment(attachment.id.toString()); - } - - function decryptAttachment(encrypted) { - return textsecure.crypto.decryptAttachment( - encrypted, - attachment.key.toArrayBuffer() - ); - } - - function updateAttachment(data) { - attachment.data = data; - } - - return getAttachment(). - then(decryptAttachment). - then(updateAttachment); -} - -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 - // after the first action. - - if (decrypted.flags == null) - decrypted.flags = 0; - - if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) - == textsecure.protobuf.DataMessage.Flags.END_SESSION) { - decrypted.body = null; - decrypted.attachments = []; - decrypted.group = null; - return Promise.resolve(decrypted); - } - if (decrypted.flags != 0) { - throw new Error("Unknown flags in message"); - } - - var promises = []; - - if (decrypted.group !== null) { - decrypted.group.id = getString(decrypted.group.id); - - if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) { - if (decrypted.group.avatar !== null) { - promises.push(handleAttachment(decrypted.group.avatar)); - } - } - - promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) { - if (existingGroup === undefined) { - if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) { - throw new Error("Got message for unknown group"); - } - return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id); - } else { - var fromIndex = existingGroup.indexOf(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.GroupContext.Type.UPDATE: - return textsecure.storage.groups.updateNumbers( - decrypted.group.id, decrypted.group.members - ).then(function(added) { - decrypted.group.added = added; - - if (decrypted.group.avatar === null && - decrypted.group.added.length == 0 && - decrypted.group.name === null) { - return; - } - - decrypted.body = null; - decrypted.attachments = []; - }); - - break; - case textsecure.protobuf.GroupContext.Type.QUIT: - decrypted.body = null; - decrypted.attachments = []; - return textsecure.storage.groups.removeNumber(decrypted.group.id, source); - case textsecure.protobuf.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; - }); -} diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index eb60d19f..521e6d7f 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -2,300 +2,397 @@ * vim: ts=4:sw=4:expandtab */ -;(function () { - 'use strict'; - window.textsecure = window.textsecure || {}; +function MessageReceiver(url, username, password, signalingKey) { + this.url = url; + this.signalingKey = signalingKey; + this.username = username; + this.password = password; + this.server = new TextSecureServer(url, username, password); - function MessageReceiver(url, username, password, signalingKey) { - this.url = url; - this.signalingKey = signalingKey; - this.username = username; - this.password = password; - this.server = new TextSecureServer(url, username, password); - - var unencoded = textsecure.utils.unencodeNumber(username); - this.number = unencoded[0]; - this.deviceId = unencoded[1]; - this.connect(); - } - - MessageReceiver.prototype = { - constructor: MessageReceiver, - connect: function() { - if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { - this.socket.close(); - } - console.log('opening websocket'); - // initialize the socket and start listening for messages - this.socket = this.server.getMessageSocket(); - this.socket.onclose = this.onclose.bind(this); - this.socket.onerror = this.onerror.bind(this); - this.socket.onopen = this.onopen.bind(this); - this.wsr = new WebSocketResource(this.socket, { - handleRequest: this.handleRequest.bind(this), - keepalive: { path: '/v1/keepalive', disconnect: true } - }); - }, - close: function() { - this.wsr.close(); - delete this.listeners; - }, - onopen: function() { - console.log('websocket open'); - }, - onerror: function(error) { - console.log('websocket error'); - }, - onclose: function(ev) { - var eventTarget = this; - console.log('websocket closed', ev.code); - // possible 403 or network issue. Make an request to confirm - this.server.getDevices(this.number). - then(this.connect.bind(this)). // No HTTP error? Reconnect - catch(function(e) { - var ev = new Event('error'); - ev.error = e; - eventTarget.dispatchEvent(ev); - }); - }, - handleRequest: function(request) { - this.wsr.resetKeepAliveTimer(); - // TODO: handle different types of requests. for now we only expect - // PUT /messages - textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).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'); - } - - }.bind(this)).catch(function(e) { - request.respond(500, 'Bad encrypted websocket message'); - console.log("Error handling incoming message:", e); + var unencoded = textsecure.utils.unencodeNumber(username); + this.number = unencoded[0]; + this.deviceId = unencoded[1]; +} +MessageReceiver.prototype = { + constructor: MessageReceiver, + connect: function() { + if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { + this.socket.close(); + } + console.log('opening websocket'); + // initialize the socket and start listening for messages + this.socket = this.server.getMessageSocket(); + this.socket.onclose = this.onclose.bind(this); + this.socket.onerror = this.onerror.bind(this); + this.socket.onopen = this.onopen.bind(this); + this.wsr = new WebSocketResource(this.socket, { + handleRequest: this.handleRequest.bind(this), + keepalive: { path: '/v1/keepalive', disconnect: true } + }); + }, + close: function() { + this.wsr.close(); + delete this.listeners; + }, + onopen: function() { + console.log('websocket open'); + }, + onerror: function(error) { + console.log('websocket error'); + }, + onclose: function(ev) { + var eventTarget = this; + console.log('websocket closed', ev.code); + // possible 403 or network issue. Make an request to confirm + this.server.getDevices(this.number). + then(this.connect.bind(this)). // No HTTP error? Reconnect + catch(function(e) { var ev = new Event('error'); ev.error = e; - this.dispatchEvent(ev); - }.bind(this)); - }, - getStatus: function() { - if (this.socket) { - return this.socket.readyState; + eventTarget.dispatchEvent(ev); + }); + }, + handleRequest: function(request) { + this.wsr.resetKeepAliveTimer(); + // TODO: handle different types of requests. for now we only expect + // PUT /messages + textsecure.crypto.decryptWebsocketMessage(request.body, this.signalingKey).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 { - return -1; + throw new Error('Received message with no content and no legacyMessage'); } - }, - onDeliveryReceipt: function (envelope) { - var ev = new Event('receipt'); + + }.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.dispatchEvent(ev); + }.bind(this)); + }, + getStatus: function() { + if (this.socket) { + return this.socket.readyState; + } else { + return -1; + } + }, + onDeliveryReceipt: function (envelope) { + var ev = new Event('receipt'); + ev.proto = envelope; + this.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.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.dispatchEvent(ev); - throw error; // reject this promise - }.bind(this)); - }, - handleSentMessage: function(destination, timestamp, message) { - return processDecrypted(message, this.number).then(function(message) { - var ev = new Event('sent'); - ev.data = { - destination : destination, - timestamp : timestamp.toNumber(), - message : message - }; - this.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.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 !== this.number) { - throw new Error('Received sync message from another number'); - } - if (envelope.sourceDevice == this.deviceId) { - 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.groups) { - this.handleGroups(syncMessage.groups); + throw error; // reject this promise + }.bind(this)); + }, + handleSentMessage: function(destination, timestamp, message) { + return this.processDecrypted(message, this.number).then(function(message) { + var ev = new Event('sent'); + ev.data = { + destination : destination, + timestamp : timestamp.toNumber(), + message : message + }; + this.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 this.processDecrypted(message, envelope.source).then(function(message) { + var ev = new Event('message'); + ev.data = { + source : envelope.source, + timestamp : envelope.timestamp.toNumber(), + message : message + }; + this.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 SyncMessage with no sent, contacts, or groups'); + throw new Error('Got Content message with no dataMessage and no syncMessage'); } - }, - handleContacts: function(contacts) { - var eventTarget = this; - var attachmentPointer = contacts.blob; - return handleAttachment(attachmentPointer).then(function() { - var contactBuffer = new ContactBuffer(attachmentPointer.data); - var contactDetails = contactBuffer.next(); - while (contactDetails !== undefined) { - var ev = new Event('contact'); - ev.contactDetails = contactDetails; - eventTarget.dispatchEvent(ev); - contactDetails = contactBuffer.next(); - } - eventTarget.dispatchEvent(new Event('contactsync')); - }); - }, - handleGroups: function(groups) { - var eventTarget = this; - var attachmentPointer = groups.blob; - return handleAttachment(attachmentPointer).then(function() { - var groupBuffer = new GroupBuffer(attachmentPointer.data); - var groupDetails = groupBuffer.next(); - while (groupDetails !== undefined) { - (function(groupDetails) { - groupDetails.id = getString(groupDetails.id); - textsecure.storage.groups.getGroup(groupDetails.id). - then(function(existingGroup) { - if (existingGroup === undefined) { - return textsecure.storage.groups.createNewGroup( - groupDetails.members, groupDetails.id - ); - } else { - return textsecure.storage.groups.updateNumbers( - groupDetails.id, groupDetails.members - ); - } - }).then(function() { - var ev = new Event('group'); - ev.groupDetails = groupDetails; - eventTarget.dispatchEvent(ev); - }); - })(groupDetails); - groupDetails = groupBuffer.next(); - } - }); - }, - - /* Implements EventTarget */ - dispatchEvent: function(ev) { - if (!(ev instanceof Event)) { - throw new Error('Expects an event'); + }.bind(this)); + }, + handleSyncMessage: function(envelope, syncMessage) { + if (envelope.source !== this.number) { + throw new Error('Received sync message from another number'); + } + if (envelope.sourceDevice == this.deviceId) { + 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.groups) { + this.handleGroups(syncMessage.groups); + } else { + throw new Error('Got SyncMessage with no sent, contacts, or groups'); + } + }, + handleContacts: function(contacts) { + var eventTarget = this; + var attachmentPointer = contacts.blob; + return this.handleAttachment(attachmentPointer).then(function() { + var contactBuffer = new ContactBuffer(attachmentPointer.data); + var contactDetails = contactBuffer.next(); + while (contactDetails !== undefined) { + var ev = new Event('contact'); + ev.contactDetails = contactDetails; + eventTarget.dispatchEvent(ev); + contactDetails = contactBuffer.next(); } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; + eventTarget.dispatchEvent(new Event('contactsync')); + }); + }, + handleGroups: function(groups) { + var eventTarget = this; + var attachmentPointer = groups.blob; + return this.handleAttachment(attachmentPointer).then(function() { + var groupBuffer = new GroupBuffer(attachmentPointer.data); + var groupDetails = groupBuffer.next(); + while (groupDetails !== undefined) { + (function(groupDetails) { + groupDetails.id = getString(groupDetails.id); + textsecure.storage.groups.getGroup(groupDetails.id). + then(function(existingGroup) { + if (existingGroup === undefined) { + return textsecure.storage.groups.createNewGroup( + groupDetails.members, groupDetails.id + ); + } else { + return textsecure.storage.groups.updateNumbers( + groupDetails.id, groupDetails.members + ); + } + }).then(function() { + var ev = new Event('group'); + ev.groupDetails = groupDetails; + eventTarget.dispatchEvent(ev); + }); + })(groupDetails); + groupDetails = groupBuffer.next(); } - var listeners = this.listeners[ev.type]; - if (typeof listeners === 'object') { - for (var i=0; i < listeners.length; ++i) { - if (typeof listeners[i] === 'function') { - listeners[i].call(null, ev); - } - } - } - }, - addEventListener: function(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - var listeners = this.listeners[eventName]; - if (typeof listeners !== 'object') { - listeners = []; - } - listeners.push(callback); - this.listeners[eventName] = listeners; - }, - removeEventListener: function(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - var listeners = this.listeners[eventName]; - for (var i=0; i < listeners.length; ++ i) { - if (listeners[i] === callback) { - listeners.splice(i, 1); - return; - } - } - this.listeners[eventName] = listeners; + }); + }, + handleAttachment: function(attachment) { + function decryptAttachment(encrypted) { + return textsecure.crypto.decryptAttachment( + encrypted, + attachment.key.toArrayBuffer() + ); } - }; + function updateAttachment(data) { + attachment.data = data; + } - textsecure.MessageReceiver = function(url, username, password, signalingKey) { - var messageReceiver = new MessageReceiver(url, username, password, signalingKey); + return this.server.getAttachment(attachment.id.toString()). + then(decryptAttachment). + then(updateAttachment); + }, + processDecrypted: function(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 + // after the first action. - this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); - this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); - this.getStatus = messageReceiver.getStatus.bind(messageReceiver); - this.close = messageReceiver.close.bind(messageReceiver); - }; + if (decrypted.flags == null) + decrypted.flags = 0; - textsecure.MessageReceiver.prototype = { - constructor: textsecure.MessageReceiver - }; + if ((decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) + == textsecure.protobuf.DataMessage.Flags.END_SESSION) { + decrypted.body = null; + decrypted.attachments = []; + decrypted.group = null; + return Promise.resolve(decrypted); + } + if (decrypted.flags != 0) { + throw new Error("Unknown flags in message"); + } + + var promises = []; + + if (decrypted.group !== null) { + decrypted.group.id = getString(decrypted.group.id); + + if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) { + if (decrypted.group.avatar !== null) { + promises.push(this.handleAttachment(decrypted.group.avatar)); + } + } + + promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) { + if (existingGroup === undefined) { + if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) { + throw new Error("Got message for unknown group"); + } + return textsecure.storage.groups.createNewGroup(decrypted.group.members, decrypted.group.id); + } else { + var fromIndex = existingGroup.indexOf(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.GroupContext.Type.UPDATE: + return textsecure.storage.groups.updateNumbers( + decrypted.group.id, decrypted.group.members + ).then(function(added) { + decrypted.group.added = added; + + if (decrypted.group.avatar === null && + decrypted.group.added.length == 0 && + decrypted.group.name === null) { + return; + } + + decrypted.body = null; + decrypted.attachments = []; + }); + + break; + case textsecure.protobuf.GroupContext.Type.QUIT: + decrypted.body = null; + decrypted.attachments = []; + return textsecure.storage.groups.removeNumber(decrypted.group.id, source); + case textsecure.protobuf.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(this.handleAttachment(decrypted.attachments[i])); + } + return Promise.all(promises).then(function() { + return decrypted; + }); + }, + + /* Implements EventTarget */ + dispatchEvent: function(ev) { + if (!(ev instanceof Event)) { + throw new Error('Expects an event'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[ev.type]; + if (typeof listeners === 'object') { + for (var i=0; i < listeners.length; ++i) { + if (typeof listeners[i] === 'function') { + listeners[i].call(null, ev); + } + } + } + }, + addEventListener: function(eventName, callback) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[eventName]; + if (typeof listeners !== 'object') { + listeners = []; + } + listeners.push(callback); + this.listeners[eventName] = listeners; + }, + removeEventListener: function(eventName, callback) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + var listeners = this.listeners[eventName]; + for (var i=0; i < listeners.length; ++ i) { + if (listeners[i] === callback) { + listeners.splice(i, 1); + return; + } + } + this.listeners[eventName] = listeners; + } + +}; + +window.textsecure = window.textsecure || {}; + +textsecure.MessageReceiver = function(url, username, password, signalingKey) { + var messageReceiver = new MessageReceiver(url, username, password, signalingKey); + this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); + this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); + this.getStatus = messageReceiver.getStatus.bind(messageReceiver); + this.close = messageReceiver.close.bind(messageReceiver); + messageReceiver.connect(); +}; + +textsecure.MessageReceiver.prototype = { + constructor: textsecure.MessageReceiver +}; -}());