From e69586200a230e96f0b0f316d62d2b96690d5bc6 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 10 Apr 2018 10:23:09 -0700 Subject: [PATCH] Unleash eslint on message_receiver.js - lots of change --- .eslintignore | 3 +- Gruntfile.js | 1 + libtextsecure/message_receiver.js | 1890 +++++++++++++++-------------- test/styleguide/legacy_bridge.js | 2 +- 4 files changed, 986 insertions(+), 910 deletions(-) diff --git a/.eslintignore b/.eslintignore index e03245044..24c008bf9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,9 +2,9 @@ build/** components/** coverage/** dist/** -libtextsecure/** # these aren't ready yet, pulling files in one-by-one +libtextsecure/** js/*.js js/models/**/*.js js/views/**/*.js @@ -30,6 +30,7 @@ ts/**/*.js !js/views/inbox_view.js !js/views/message_view.js !js/views/settings_view.js +!libtextsecure/message_receiver.js !main.js !preload.js !prepare_build.js diff --git a/Gruntfile.js b/Gruntfile.js index b6de06d07..7b070cb19 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -112,6 +112,7 @@ module.exports = function(grunt) { '!js/views/debug_log_view.js', '!js/views/message_view.js', '!js/WebAudioRecorderMp3.js', + '!libtextsecure/message_receiver.js', '_locales/**/*' ], options: { jshintrc: '.jshintrc' }, diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 582084ffd..f5147b6d4 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1,965 +1,1039 @@ -/* - * vim: ts=4:sw=4:expandtab - */ - -function MessageReceiver(url, username, password, signalingKey, options) { - options = options || {}; - - this.count = 0; - - this.url = url; - this.signalingKey = signalingKey; - this.username = username; - this.password = password; - this.server = new TextSecureServer(url, username, password); - - var address = libsignal.SignalProtocolAddress.fromString(username); - this.number = address.getName(); - this.deviceId = address.getDeviceId(); - - this.pending = Promise.resolve(); - - if (options.retryCached) { - this.pending = this.queueAllCached(); - } +/* global window: false */ +/* global textsecure: false */ +/* global TextSecureServer: false */ +/* global libsignal: false */ +/* global WebSocketResource: false */ +/* global WebSocket: false */ +/* global Event: false */ +/* global dcodeIO: false */ +/* global _: false */ +/* global ContactBuffer: false */ +/* global GroupBuffer: false */ + +/* eslint-disable more/no-then */ + +function MessageReceiver(url, username, password, signalingKey, options = {}) { + this.count = 0; + + this.url = url; + this.signalingKey = signalingKey; + this.username = username; + this.password = password; + this.server = new TextSecureServer(url, username, password); + + const address = libsignal.SignalProtocolAddress.fromString(username); + this.number = address.getName(); + this.deviceId = address.getDeviceId(); + + this.pending = Promise.resolve(); + + if (options.retryCached) { + this.pending = this.queueAllCached(); + } } MessageReceiver.prototype = new textsecure.EventTarget(); MessageReceiver.prototype.extend({ - constructor: MessageReceiver, - connect: function() { - if (this.calledClose) { - return; - } + constructor: MessageReceiver, + connect() { + if (this.calledClose) { + return; + } - this.hasConnected = true; + this.hasConnected = true; - if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { - this.socket.close(); - this.wsr.close(); - } - // 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 - } - }); + if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { + this.socket.close(); + this.wsr.close(); + } + // 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, + }, + }); + + // Because sometimes the socket doesn't properly emit its close event + this._onClose = this.onclose.bind(this); + this.wsr.addEventListener('close', this._onClose); + + // Ensures that an immediate 'empty' event from the websocket will fire only after + // all cached envelopes are processed. + this.incoming = [this.pending]; + }, + shutdown() { + if (this.socket) { + this.socket.onclose = null; + this.socket.onerror = null; + this.socket.onopen = null; + this.socket = null; + } - // Because sometimes the socket doesn't properly emit its close event - this._onClose = this.onclose.bind(this) - this.wsr.addEventListener('close', this._onClose); - - // Ensures that an immediate 'empty' event from the websocket will fire only after - // all cached envelopes are processed. - this.incoming = [this.pending]; - }, - shutdown: function() { - if (this.socket) { - this.socket.onclose = null; - this.socket.onerror = null; - this.socket.onopen = null; - this.socket = null; - } + if (this.wsr) { + this.wsr.removeEventListener('close', this._onClose); + this.wsr = null; + } + }, + close() { + console.log('MessageReceiver.close()'); + this.calledClose = true; + + // Our WebSocketResource instance will close the socket and emit a 'close' event + // if the socket doesn't emit one quickly enough. + if (this.wsr) { + this.wsr.close(3000, 'called close'); + } - if (this.wsr) { - this.wsr.removeEventListener('close', this._onClose); - this.wsr = null; - } - }, - close: function() { - console.log('MessageReceiver.close()'); - this.calledClose = true; - - // Our WebSocketResource instance will close the socket and emit a 'close' event - // if the socket doesn't emit one quickly enough. - if (this.wsr) { - this.wsr.close(3000, 'called close'); - } + return this.drain(); + }, + onopen() { + console.log('websocket open'); + }, + onerror() { + console.log('websocket error'); + }, + dispatchAndWait(event) { + return Promise.all(this.dispatchEvent(event)); + }, + onclose(ev) { + console.log( + 'websocket closed', + ev.code, + ev.reason || '', + 'calledClose:', + this.calledClose + ); + + this.shutdown(); + + if (this.calledClose) { + return Promise.resolve(); + } + if (ev.code === 3000) { + return Promise.resolve(); + } + if (ev.code === 3001) { + this.onEmpty(); + } + // possible 403 or network issue. Make an request to confirm + return this.server.getDevices(this.number) + .then(this.connect.bind(this)) // No HTTP error? Reconnect + .catch((e) => { + const event = new Event('error'); + event.error = e; + return this.dispatchAndWait(event); + }); + }, + handleRequest(request) { + this.incoming = this.incoming || []; + // We do the message decryption here, instead of in the ordered pending queue, + // to avoid exposing the time it took us to process messages through the time-to-ack. + + // TODO: handle different types of requests. + if (request.path !== '/api/v1/message') { + console.log('got request', request.verb, request.path); + request.respond(200, 'OK'); + + if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') { + this.onEmpty(); + } + return; + } - return this.drain(); - }, - onopen: function() { - console.log('websocket open'); - }, - onerror: function(error) { - console.log('websocket error'); - }, - dispatchAndWait: function(event) { - return Promise.all(this.dispatchEvent(event)); - }, - onclose: function(ev) { + const promise = textsecure.crypto.decryptWebsocketMessage( + request.body, + this.signalingKey + ).then((plaintext) => { + const 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 + + if (this.isBlocked(envelope.source)) { + return request.respond(200, 'OK'); + } + + return this.addToCache(envelope, plaintext).then(() => { + request.respond(200, 'OK'); + this.queueEnvelope(envelope); + }, (error) => { console.log( - 'websocket closed', - ev.code, - ev.reason || '', - 'calledClose:', - this.calledClose + 'handleRequest error trying to add message to cache:', + error && error.stack ? error.stack : error ); - - this.shutdown(); - - if (this.calledClose) { - return; - } - if (ev.code === 3000) { - return; - } - if (ev.code === 3001) { - this.onEmpty(); + }); + }).catch((e) => { + request.respond(500, 'Bad encrypted websocket message'); + console.log('Error handling incoming message:', e && e.stack ? e.stack : e); + const ev = new Event('error'); + ev.error = e; + return this.dispatchAndWait(ev); + }); + + this.incoming.push(promise); + }, + addToQueue(task) { + this.count += 1; + this.pending = this.pending.then(task, task); + + const { count, pending } = this; + + const cleanup = () => { + this.updateProgress(count); + // We want to clear out the promise chain whenever possible because it could + // lead to large memory usage over time: + // https://github.com/nodejs/node/issues/6673#issuecomment-244331609 + if (this.pending === pending) { + this.pending = Promise.resolve(); + } + }; + + pending.then(cleanup, cleanup); + + return pending; + }, + onEmpty() { + const { incoming } = this; + this.incoming = []; + + const dispatchEmpty = () => { + console.log('MessageReceiver: emitting \'empty\' event'); + const ev = new Event('empty'); + return this.dispatchAndWait(ev); + }; + + const queueDispatch = () => { + // resetting count to zero so everything queued after this starts over again + this.count = 0; + + this.addToQueue(dispatchEmpty); + }; + + // We first wait for all recently-received messages (this.incoming) to be queued, + // then we add a task to emit the 'empty' event to the queue, so all message + // processing is complete by the time it runs. + Promise.all(incoming).then(queueDispatch, queueDispatch); + }, + drain() { + const { incoming } = this; + this.incoming = []; + + const queueDispatch = () => this.addToQueue(() => { + console.log('drained'); + }); + + // This promise will resolve when there are no more messages to be processed. + return Promise.all(incoming).then(queueDispatch, queueDispatch); + }, + updateProgress(count) { + // count by 10s + if (count % 10 !== 0) { + return; + } + const ev = new Event('progress'); + ev.count = count; + this.dispatchEvent(ev); + }, + queueAllCached() { + return this.getAllFromCache().then((items) => { + for (let i = 0, max = items.length; i < max; i += 1) { + this.queueCached(items[i]); + } + }); + }, + queueCached(item) { + try { + let envelopePlaintext = item.envelope; + + // Up until 0.42.6 we stored envelope and decrypted as strings in IndexedDB, + // so we need to be ready for them. + if (typeof envelopePlaintext === 'string') { + envelopePlaintext = this.stringToArrayBuffer(envelopePlaintext); + } + const envelope = textsecure.protobuf.Envelope.decode(envelopePlaintext); + + const { decrypted } = item; + if (decrypted) { + let payloadPlaintext = decrypted; + if (typeof payloadPlaintext === 'string') { + payloadPlaintext = this.stringToArrayBuffer(payloadPlaintext); } - // possible 403 or network issue. Make an request to confirm - return 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; - return this.dispatchAndWait(ev); - }.bind(this)); - }, - handleRequest: function(request) { - this.incoming = this.incoming || []; - // We do the message decryption here, instead of in the ordered pending queue, - // to avoid exposing the time it took us to process messages through the time-to-ack. - - // TODO: handle different types of requests. - if (request.path !== '/api/v1/message') { - console.log('got request', request.verb, request.path); - request.respond(200, 'OK'); - - if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') { - this.onEmpty(); - } - return; + this.queueDecryptedEnvelope(envelope, payloadPlaintext); + } else { + this.queueEnvelope(envelope); + } + } catch (error) { + console.log('queueCached error handling item', item.id); + } + }, + getEnvelopeId(envelope) { + return `${envelope.source}.${envelope.sourceDevice} ${envelope.timestamp.toNumber()}`; + }, + stringToArrayBuffer(string) { + // eslint-disable-next-line new-cap + return new dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer(); + }, + getAllFromCache() { + console.log('getAllFromCache'); + return textsecure.storage.unprocessed.getAll().then((items) => { + console.log('getAllFromCache loaded', items.length, 'saved envelopes'); + + return Promise.all(_.map(items, (item) => { + const attempts = 1 + (item.attempts || 0); + if (attempts >= 5) { + console.log('getAllFromCache final attempt for envelope', item.id); + return textsecure.storage.unprocessed.remove(item.id); } + return textsecure.storage.unprocessed.update(item.id, { attempts }); + })).then(() => items, (error) => { + console.log( + 'getAllFromCache error updating items after load:', + error && error.stack ? error.stack : error + ); + return items; + }); + }); + }, + addToCache(envelope, plaintext) { + const id = this.getEnvelopeId(envelope); + const data = { + id, + envelope: plaintext, + timestamp: Date.now(), + attempts: 1, + }; + return textsecure.storage.unprocessed.add(data); + }, + updateCache(envelope, plaintext) { + const id = this.getEnvelopeId(envelope); + const data = { + decrypted: plaintext, + }; + return textsecure.storage.unprocessed.update(id, data); + }, + removeFromCache(envelope) { + const id = this.getEnvelopeId(envelope); + return textsecure.storage.unprocessed.remove(id); + }, + queueDecryptedEnvelope(envelope, plaintext) { + const id = this.getEnvelopeId(envelope); + console.log('queueing decrypted envelope', id); + + const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext); + const taskWithTimeout = textsecure.createTaskWithTimeout( + task, + `queueEncryptedEnvelope ${id}` + ); + const promise = this.addToQueue(taskWithTimeout); + + return promise.catch((error) => { + console.log( + 'queueDecryptedEnvelope error handling envelope', + id, + ':', + error && error.stack ? error.stack : error + ); + }); + }, + queueEnvelope(envelope) { + const id = this.getEnvelopeId(envelope); + console.log('queueing envelope', id); + + const task = this.handleEnvelope.bind(this, envelope); + const taskWithTimeout = textsecure.createTaskWithTimeout(task, `queueEnvelope ${id}`); + const promise = this.addToQueue(taskWithTimeout); + + return promise.catch((error) => { + console.log( + 'queueEnvelope error handling envelope', + id, + ':', + error && error.stack ? error.stack : error + ); + }); + }, + // Same as handleEnvelope, just without the decryption step. Necessary for handling + // messages which were successfully decrypted, but application logic didn't finish + // processing. + handleDecryptedEnvelope(envelope, plaintext) { + // No decryption is required for delivery receipts, so the decrypted field of + // the Unprocessed model will never be set + + if (envelope.content) { + return this.innerHandleContentMessage(envelope, plaintext); + } else if (envelope.legacyMessage) { + return this.innerHandleLegacyMessage(envelope, plaintext); + } + this.removeFromCache(envelope); + throw new Error('Received message with no content and no legacyMessage'); + }, + handleEnvelope(envelope) { + if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) { + return this.onDeliveryReceipt(envelope); + } - this.incoming.push(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 + if (envelope.content) { + return this.handleContentMessage(envelope); + } else if (envelope.legacyMessage) { + return this.handleLegacyMessage(envelope); + } + this.removeFromCache(envelope); + throw new Error('Received message with no content and no legacyMessage'); + }, + getStatus() { + if (this.socket) { + return this.socket.readyState; + } else if (this.hasConnected) { + return WebSocket.CLOSED; + } + return -1; + }, + onDeliveryReceipt(envelope) { + return new Promise(((resolve, reject) => { + const ev = new Event('delivery'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.deliveryReceipt = { + timestamp: envelope.timestamp.toNumber(), + source: envelope.source, + sourceDevice: envelope.sourceDevice, + }; + this.dispatchAndWait(ev).then(resolve, reject); + })); + }, + unpad(paddedData) { + const paddedPlaintext = new Uint8Array(paddedData); + let plaintext; + + for (let i = paddedPlaintext.length - 1; i >= 0; i -= 1) { + if (paddedPlaintext[i] === 0x80) { + plaintext = new Uint8Array(i); + plaintext.set(paddedPlaintext.subarray(0, i)); + plaintext = plaintext.buffer; + break; + } else if (paddedPlaintext[i] !== 0x00) { + throw new Error('Invalid padding'); + } + } - if (this.isBlocked(envelope.source)) { - return request.respond(200, 'OK'); - } + return plaintext; + }, + decrypt(envelope, ciphertext) { + let promise; + const address = new libsignal.SignalProtocolAddress( + envelope.source, + envelope.sourceDevice + ); + + const ourNumber = textsecure.storage.user.getNumber(); + const number = address.toString().split('.')[0]; + const options = {}; + + // No limit on message keys if we're communicating with our other devices + if (ourNumber === number) { + options.messageKeysLimit = false; + } - return this.addToCache(envelope, plaintext).then(function() { - request.respond(200, 'OK'); - this.queueEnvelope(envelope); - }.bind(this), function(error) { - console.log( - 'handleRequest error trying to add message to cache:', - error && error.stack ? error.stack : error - ); - }); - }.bind(this)).catch(function(e) { - request.respond(500, 'Bad encrypted websocket message'); - console.log("Error handling incoming message:", e && e.stack ? e.stack : e); - var ev = new Event('error'); - ev.error = e; - return this.dispatchAndWait(ev); - }.bind(this))); - }, - addToQueue: function(task) { - var count = this.count += 1; - var current = this.pending = this.pending.then(task, task); - - var cleanup = function() { - this.updateProgress(count); - // We want to clear out the promise chain whenever possible because it could - // lead to large memory usage over time: - // https://github.com/nodejs/node/issues/6673#issuecomment-244331609 - if (this.pending === current) { - this.pending = Promise.resolve(); - } - }.bind(this); - - current.then(cleanup, cleanup); - - return current; - }, - onEmpty: function() { - var incoming = this.incoming; - this.incoming = []; - - var dispatchEmpty = function() { - console.log('MessageReceiver: emitting \'empty\' event'); - var ev = new Event('empty'); - return this.dispatchAndWait(ev); - }.bind(this); - - var queueDispatch = function() { - // resetting count to zero so everything queued after this starts over again - this.count = 0; - - this.addToQueue(dispatchEmpty); - }.bind(this); - - // We first wait for all recently-received messages (this.incoming) to be queued, - // then we add a task to emit the 'empty' event to the queue, so all message - // processing is complete by the time it runs. - Promise.all(incoming).then(queueDispatch, queueDispatch); - }, - drain: function() { - var incoming = this.incoming; - this.incoming = []; - - var queueDispatch = function() { - return this.addToQueue(function() { - console.log('drained'); - }); - }.bind(this); - - // This promise will resolve when there are no more messages to be processed. - return Promise.all(incoming).then(queueDispatch, queueDispatch); - }, - updateProgress: function(count) { - // count by 10s - if (count % 10 !== 0) { - return; - } - var ev = new Event('progress'); - ev.count = count; - this.dispatchEvent(ev); - }, - queueAllCached: function() { - return this.getAllFromCache().then(function(items) { - for (var i = 0, max = items.length; i < max; i += 1) { - this.queueCached(items[i]); - } - }.bind(this)); - }, - queueCached: function(item) { - try { - var envelopePlaintext = item.envelope; - - // Up until 0.42.6 we stored envelope and decrypted as strings in IndexedDB, - // so we need to be ready for them. - if (typeof envelopePlaintext === 'string') { - envelopePlaintext = this.stringToArrayBuffer(envelopePlaintext); - } - var envelope = textsecure.protobuf.Envelope.decode(envelopePlaintext); + const sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address, + options + ); + + switch (envelope.type) { + case textsecure.protobuf.Envelope.Type.CIPHERTEXT: + console.log('message from', this.getEnvelopeId(envelope)); + promise = sessionCipher.decryptWhisperMessage(ciphertext).then(this.unpad); + break; + case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: + console.log('prekey message from', this.getEnvelopeId(envelope)); + promise = this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address); + break; + default: + promise = Promise.reject(new Error('Unknown message type')); + } - var decrypted = item.decrypted; - if (decrypted) { - var payloadPlaintext = decrypted; - if (typeof payloadPlaintext === 'string') { - payloadPlaintext = this.stringToArrayBuffer(payloadPlaintext); - } - this.queueDecryptedEnvelope(envelope, payloadPlaintext); - } else { - this.queueEnvelope(envelope); - } - } - catch (error) { - console.log('queueCached error handling item', item.id); - } - }, - getEnvelopeId: function(envelope) { - return envelope.source + '.' + envelope.sourceDevice + ' ' + envelope.timestamp.toNumber(); - }, - stringToArrayBuffer: function(string) { - return new dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer(); - }, - getAllFromCache: function() { - console.log('getAllFromCache'); - return textsecure.storage.unprocessed.getAll().then(function(items) { - console.log('getAllFromCache loaded', items.length, 'saved envelopes'); - - return Promise.all(_.map(items, function(item) { - var attempts = 1 + (item.attempts || 0); - if (attempts >= 5) { - console.log('getAllFromCache final attempt for envelope', item.id); - return textsecure.storage.unprocessed.remove(item.id); - } else { - return textsecure.storage.unprocessed.update(item.id, {attempts: attempts}); + return promise.then(plaintext => this.updateCache( + envelope, + plaintext + ).then(() => plaintext, (error) => { + console.log( + 'decrypt failed to save decrypted message contents to cache:', + error && error.stack ? error.stack : error + ); + return plaintext; + })).catch((error) => { + let errorToThrow = error; + + if (error.message === 'Unknown identity key') { + // create an error that the UI will pick up and ask the + // user if they want to re-negotiate + const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); + errorToThrow = new textsecure.IncomingIdentityKeyError( + address.toString(), + buffer.toArrayBuffer(), + error.identityKey + ); + } + const ev = new Event('error'); + ev.error = errorToThrow; + ev.proto = envelope; + ev.confirm = this.removeFromCache.bind(this, envelope); + + const returnError = () => Promise.reject(errorToThrow); + return this.dispatchAndWait(ev).then(returnError, returnError); + }); + }, + async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) { + const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext); + + try { + return this.unpad(padded); + } catch (e) { + if (e.message === 'Unknown identity key') { + // create an error that the UI will pick up and ask the + // user if they want to re-negotiate + const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); + throw new textsecure.IncomingIdentityKeyError( + address.toString(), + buffer.toArrayBuffer(), + e.identityKey + ); + } + throw e; + } + }, + handleSentMessage(envelope, destination, timestamp, msg, expirationStartTimestamp) { + let p = Promise.resolve(); + // eslint-disable-next-line no-bitwise + if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { + p = this.handleEndSession(destination); + } + return p.then(() => this.processDecrypted( + envelope, + msg, + this.number + ).then((message) => { + const ev = new Event('sent'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.data = { + destination, + timestamp: timestamp.toNumber(), + device: envelope.sourceDevice, + message, + }; + if (expirationStartTimestamp) { + ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); + } + return this.dispatchAndWait(ev); + })); + }, + handleDataMessage(envelope, msg) { + console.log('data message from', this.getEnvelopeId(envelope)); + let p = Promise.resolve(); + // eslint-disable-next-line no-bitwise + if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { + p = this.handleEndSession(envelope.source); + } + return p.then(() => this.processDecrypted( + envelope, + msg, + envelope.source + ).then((message) => { + const ev = new Event('message'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.data = { + source: envelope.source, + sourceDevice: envelope.sourceDevice, + timestamp: envelope.timestamp.toNumber(), + receivedAt: envelope.receivedAt, + message, + }; + return this.dispatchAndWait(ev); + })); + }, + handleLegacyMessage(envelope) { + return this.decrypt( + envelope, + envelope.legacyMessage + ).then(plaintext => this.innerHandleLegacyMessage(envelope, plaintext)); + }, + innerHandleLegacyMessage(envelope, plaintext) { + const message = textsecure.protobuf.DataMessage.decode(plaintext); + return this.handleDataMessage(envelope, message); + }, + handleContentMessage(envelope) { + return this.decrypt( + envelope, + envelope.content + ).then(plaintext => this.innerHandleContentMessage(envelope, plaintext)); + }, + innerHandleContentMessage(envelope, plaintext) { + const 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); + } else if (content.nullMessage) { + return this.handleNullMessage(envelope, content.nullMessage); + } else if (content.callMessage) { + return this.handleCallMessage(envelope, content.callMessage); + } else if (content.receiptMessage) { + return this.handleReceiptMessage(envelope, content.receiptMessage); + } + this.removeFromCache(envelope); + throw new Error('Unsupported content message'); + }, + handleCallMessage(envelope) { + console.log('call message from', this.getEnvelopeId(envelope)); + this.removeFromCache(envelope); + }, + handleReceiptMessage(envelope, receiptMessage) { + const results = []; + if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) { + for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { + const ev = new Event('delivery'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.deliveryReceipt = { + timestamp: receiptMessage.timestamp[i].toNumber(), + source: envelope.source, + sourceDevice: envelope.sourceDevice, + }; + results.push(this.dispatchAndWait(ev)); + } + } else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) { + for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { + const ev = new Event('read'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.timestamp = envelope.timestamp.toNumber(); + ev.read = { + timestamp: receiptMessage.timestamp[i].toNumber(), + reader: envelope.source, + }; + results.push(this.dispatchAndWait(ev)); + } + } + return Promise.all(results); + }, + handleNullMessage(envelope) { + console.log('null message from', this.getEnvelopeId(envelope)); + this.removeFromCache(envelope); + }, + handleSyncMessage(envelope, syncMessage) { + if (envelope.source !== this.number) { + throw new Error('Received sync message from another number'); + } + // eslint-disable-next-line eqeqeq + if (envelope.sourceDevice == this.deviceId) { + throw new Error('Received sync message from our own device'); + } + if (syncMessage.sent) { + const sentMessage = syncMessage.sent; + const to = sentMessage.message.group + ? `group(${sentMessage.message.group.id.toBinary()})` + : sentMessage.destination; + + console.log( + 'sent message to', + to, + sentMessage.timestamp.toNumber(), + 'from', + this.getEnvelopeId(envelope) + ); + return this.handleSentMessage( + envelope, + sentMessage.destination, + sentMessage.timestamp, + sentMessage.message, + sentMessage.expirationStartTimestamp + ); + } else if (syncMessage.contacts) { + return this.handleContacts(envelope, syncMessage.contacts); + } else if (syncMessage.groups) { + return this.handleGroups(envelope, syncMessage.groups); + } else if (syncMessage.blocked) { + return this.handleBlocked(envelope, syncMessage.blocked); + } else if (syncMessage.request) { + console.log('Got SyncMessage Request'); + return this.removeFromCache(envelope); + } else if (syncMessage.read && syncMessage.read.length) { + console.log('read messages from', this.getEnvelopeId(envelope)); + return this.handleRead(envelope, syncMessage.read); + } else if (syncMessage.verified) { + return this.handleVerified(envelope, syncMessage.verified); + } else if (syncMessage.configuration) { + return this.handleConfiguration(envelope, syncMessage.configuration); + } + throw new Error('Got empty SyncMessage'); + }, + handleConfiguration(envelope, configuration) { + const ev = new Event('configuration'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.configuration = { + readReceipts: configuration.readReceipts, + }; + return this.dispatchAndWait(ev); + }, + handleVerified(envelope, verified) { + const ev = new Event('verified'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.verified = { + state: verified.state, + destination: verified.destination, + identityKey: verified.identityKey.toArrayBuffer(), + }; + return this.dispatchAndWait(ev); + }, + handleRead(envelope, read) { + const results = []; + for (let i = 0; i < read.length; i += 1) { + const ev = new Event('readSync'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.timestamp = envelope.timestamp.toNumber(); + ev.read = { + timestamp: read[i].timestamp.toNumber(), + sender: read[i].sender, + }; + results.push(this.dispatchAndWait(ev)); + } + return Promise.all(results); + }, + handleContacts(envelope, contacts) { + console.log('contact sync'); + const attachmentPointer = contacts.blob; + return this.handleAttachment(attachmentPointer).then(() => { + const results = []; + const contactBuffer = new ContactBuffer(attachmentPointer.data); + let contactDetails = contactBuffer.next(); + while (contactDetails !== undefined) { + const ev = new Event('contact'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.contactDetails = contactDetails; + results.push(this.dispatchAndWait(ev)); + + contactDetails = contactBuffer.next(); + } + + const ev = new Event('contactsync'); + ev.confirm = this.removeFromCache.bind(this, envelope); + results.push(this.dispatchAndWait(ev)); + + return Promise.all(results); + }); + }, + handleGroups(envelope, groups) { + console.log('group sync'); + const attachmentPointer = groups.blob; + return this.handleAttachment(attachmentPointer).then(() => { + const groupBuffer = new GroupBuffer(attachmentPointer.data); + let groupDetails = groupBuffer.next(); + const promises = []; + while (groupDetails !== undefined) { + const getGroupDetails = (details) => { + // eslint-disable-next-line no-param-reassign + details.id = details.id.toBinary(); + if (details.active) { + return textsecure.storage.groups.getGroup(details.id) + .then((existingGroup) => { + if (existingGroup === undefined) { + return textsecure.storage.groups.createNewGroup( + details.members, + details.id + ); } - }.bind(this))).then(function() { - return items; - }, function(error) { - console.log( - 'getAllFromCache error updating items after load:', - error && error.stack ? error.stack : error + return textsecure.storage.groups.updateNumbers( + details.id, + details.members ); - return items; - }); - }.bind(this)); - }, - addToCache: function(envelope, plaintext) { - var id = this.getEnvelopeId(envelope); - var data = { - id: id, - envelope: plaintext, - timestamp: Date.now(), - attempts: 1 - }; - return textsecure.storage.unprocessed.add(data); - }, - updateCache: function(envelope, plaintext) { - var id = this.getEnvelopeId(envelope); - var data = { - decrypted: plaintext + }).then(() => details); + } + return Promise.resolve(details); }; - return textsecure.storage.unprocessed.update(id, data); - }, - removeFromCache: function(envelope) { - var id = this.getEnvelopeId(envelope); - return textsecure.storage.unprocessed.remove(id); - }, - queueDecryptedEnvelope: function(envelope, plaintext) { - var id = this.getEnvelopeId(envelope); - console.log('queueing decrypted envelope', id); - - var task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext); - var taskWithTimeout = textsecure.createTaskWithTimeout(task, 'queueEncryptedEnvelope ' + id); - var promise = this.addToQueue(taskWithTimeout); - - return promise.catch(function(error) { - console.log('queueDecryptedEnvelope error handling envelope', id, ':', error && error.stack ? error.stack : error); - }); - }, - queueEnvelope: function(envelope) { - var id = this.getEnvelopeId(envelope); - console.log('queueing envelope', id); - - var task = this.handleEnvelope.bind(this, envelope); - var taskWithTimeout = textsecure.createTaskWithTimeout(task, 'queueEnvelope ' + id); - var promise = this.addToQueue(taskWithTimeout); - return promise.catch(function(error) { - console.log('queueEnvelope error handling envelope', id, ':', error && error.stack ? error.stack : error); + const promise = getGroupDetails(groupDetails).then((details) => { + const ev = new Event('group'); + ev.confirm = this.removeFromCache.bind(this, envelope); + ev.groupDetails = details; + return this.dispatchAndWait(ev); + }).catch((e) => { + console.log('error processing group', e); }); - }, - // Same as handleEnvelope, just without the decryption step. Necessary for handling - // messages which were successfully decrypted, but application logic didn't finish - // processing. - handleDecryptedEnvelope: function(envelope, plaintext) { - // No decryption is required for delivery receipts, so the decrypted field of - // the Unprocessed model will never be set - - if (envelope.content) { - return this.innerHandleContentMessage(envelope, plaintext); - } else if (envelope.legacyMessage) { - return this.innerHandleLegacyMessage(envelope, plaintext); - } else { - this.removeFromCache(envelope); - throw new Error('Received message with no content and no legacyMessage'); - } - }, - handleEnvelope: function(envelope) { - if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) { - return this.onDeliveryReceipt(envelope); - } + groupDetails = groupBuffer.next(); + promises.push(promise); + } - if (envelope.content) { - return this.handleContentMessage(envelope); - } else if (envelope.legacyMessage) { - return this.handleLegacyMessage(envelope); - } else { - this.removeFromCache(envelope); - throw new Error('Received message with no content and no legacyMessage'); - } - }, - getStatus: function() { - if (this.socket) { - return this.socket.readyState; - } else if (this.hasConnected) { - return WebSocket.CLOSED; - } else { - return -1; - } - }, - onDeliveryReceipt: function (envelope) { - return new Promise(function(resolve, reject) { - var ev = new Event('delivery'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.deliveryReceipt = { - timestamp : envelope.timestamp.toNumber(), - source : envelope.source, - sourceDevice : envelope.sourceDevice - }; - this.dispatchAndWait(ev).then(resolve, reject); - }.bind(this)); - }, - unpad: function(paddedPlaintext) { - paddedPlaintext = new Uint8Array(paddedPlaintext); - var plaintext; - for (var i = paddedPlaintext.length - 1; i >= 0; i--) { - if (paddedPlaintext[i] == 0x80) { - plaintext = new Uint8Array(i); - plaintext.set(paddedPlaintext.subarray(0, i)); - plaintext = plaintext.buffer; - break; - } else if (paddedPlaintext[i] !== 0x00) { - throw new Error('Invalid padding'); - } - } - - return plaintext; - }, - decrypt: function(envelope, ciphertext) { - var promise; - var address = new libsignal.SignalProtocolAddress(envelope.source, envelope.sourceDevice); - - var ourNumber = textsecure.storage.user.getNumber(); - var number = address.toString().split('.')[0]; - var options = {}; - - // No limit on message keys if we're communicating with our other devices - if (ourNumber === number) { - options.messageKeysLimit = false; - } - - var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address, options); - switch(envelope.type) { - case textsecure.protobuf.Envelope.Type.CIPHERTEXT: - console.log('message from', this.getEnvelopeId(envelope)); - promise = sessionCipher.decryptWhisperMessage(ciphertext).then(this.unpad); - break; - case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: - console.log('prekey message from', this.getEnvelopeId(envelope)); - promise = this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address); - break; - default: - promise = Promise.reject(new Error("Unknown message type")); - } - return promise.then(function(plaintext) { - return this.updateCache(envelope, plaintext).then(function() { - return plaintext; - }, function(error) { - console.log( - 'decrypt failed to save decrypted message contents to cache:', - error && error.stack ? error.stack : error - ); - return plaintext; - }); - }.bind(this)).catch(function(error) { - if (error.message === 'Unknown identity key') { - // create an error that the UI will pick up and ask the - // user if they want to re-negotiate - var buffer = dcodeIO.ByteBuffer.wrap(ciphertext); - error = new textsecure.IncomingIdentityKeyError( - address.toString(), - buffer.toArrayBuffer(), - error.identityKey - ); - } - var ev = new Event('error'); - ev.error = error; - ev.proto = envelope; - ev.confirm = this.removeFromCache.bind(this, envelope); - - var returnError = function() { - return Promise.reject(error); - }; - return this.dispatchAndWait(ev).then(returnError, returnError); - }.bind(this)); - }, - decryptPreKeyWhisperMessage: function(ciphertext, sessionCipher, address) { - return sessionCipher.decryptPreKeyWhisperMessage(ciphertext).then(this.unpad).catch(function(e) { - if (e.message === 'Unknown identity key') { - // create an error that the UI will pick up and ask the - // user if they want to re-negotiate - var buffer = dcodeIO.ByteBuffer.wrap(ciphertext); - throw new textsecure.IncomingIdentityKeyError( - address.toString(), - buffer.toArrayBuffer(), - e.identityKey - ); - } - throw e; - }); - }, - handleSentMessage: function(envelope, destination, timestamp, message, expirationStartTimestamp) { - var p = Promise.resolve(); - if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) == - textsecure.protobuf.DataMessage.Flags.END_SESSION ) { - p = this.handleEndSession(destination); - } - return p.then(function() { - return this.processDecrypted(envelope, message, this.number).then(function(message) { - var ev = new Event('sent'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { - destination : destination, - timestamp : timestamp.toNumber(), - device : envelope.sourceDevice, - message : message - }; - if (expirationStartTimestamp) { - ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); - } - return this.dispatchAndWait(ev); - }.bind(this)); - }.bind(this)); - }, - handleDataMessage: function(envelope, message) { - console.log('data message from', this.getEnvelopeId(envelope)); - var p = Promise.resolve(); - if ((message.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) == - textsecure.protobuf.DataMessage.Flags.END_SESSION ) { - p = this.handleEndSession(envelope.source); - } - return p.then(function() { - return this.processDecrypted(envelope, message, envelope.source).then(function(message) { - var ev = new Event('message'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { - source : envelope.source, - sourceDevice : envelope.sourceDevice, - timestamp : envelope.timestamp.toNumber(), - receivedAt : envelope.receivedAt, - message : message - }; - return this.dispatchAndWait(ev); - }.bind(this)); - }.bind(this)); - }, - handleLegacyMessage: function (envelope) { - return this.decrypt(envelope, envelope.legacyMessage).then(function(plaintext) { - return this.innerHandleLegacyMessage(envelope, plaintext); - }.bind(this)); - }, - innerHandleLegacyMessage: function (envelope, plaintext) { - var message = textsecure.protobuf.DataMessage.decode(plaintext); - return this.handleDataMessage(envelope, message); - }, - handleContentMessage: function (envelope) { - return this.decrypt(envelope, envelope.content).then(function(plaintext) { - return this.innerHandleContentMessage(envelope, plaintext); - }.bind(this)); - }, - innerHandleContentMessage: function(envelope, plaintext) { - 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); - } else if (content.nullMessage) { - return this.handleNullMessage(envelope, content.nullMessage); - } else if (content.callMessage) { - return this.handleCallMessage(envelope, content.callMessage); - } else if (content.receiptMessage) { - return this.handleReceiptMessage(envelope, content.receiptMessage); - } else { - this.removeFromCache(envelope); - throw new Error('Unsupported content message'); - } - }, - handleCallMessage: function(envelope, nullMessage) { - console.log('call message from', this.getEnvelopeId(envelope)); - this.removeFromCache(envelope); - }, - handleReceiptMessage: function(envelope, receiptMessage) { - var results = []; - if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY) { - for (var i = 0; i < receiptMessage.timestamp.length; ++i) { - var ev = new Event('delivery'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.deliveryReceipt = { - timestamp : receiptMessage.timestamp[i].toNumber(), - source : envelope.source, - sourceDevice : envelope.sourceDevice - }; - results.push(this.dispatchAndWait(ev)); - } - } else if (receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ) { - for (var i = 0; i < receiptMessage.timestamp.length; ++i) { - var ev = new Event('read'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.timestamp = envelope.timestamp.toNumber(); - ev.read = { - timestamp : receiptMessage.timestamp[i].toNumber(), - reader : envelope.source - } - results.push(this.dispatchAndWait(ev)); - } - } - return Promise.all(results); - }, - handleNullMessage: function(envelope, nullMessage) { - console.log('null message from', this.getEnvelopeId(envelope)); - this.removeFromCache(envelope); - }, - 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; - var to = sentMessage.message.group - ? 'group(' + sentMessage.message.group.id.toBinary() + ')' - : sentMessage.destination; - - console.log('sent message to', - to, - sentMessage.timestamp.toNumber(), - 'from', - this.getEnvelopeId(envelope) - ); - return this.handleSentMessage( - envelope, - sentMessage.destination, - sentMessage.timestamp, - sentMessage.message, - sentMessage.expirationStartTimestamp - ); - } else if (syncMessage.contacts) { - return this.handleContacts(envelope, syncMessage.contacts); - } else if (syncMessage.groups) { - return this.handleGroups(envelope, syncMessage.groups); - } else if (syncMessage.blocked) { - return this.handleBlocked(envelope, syncMessage.blocked); - } else if (syncMessage.request) { - console.log('Got SyncMessage Request'); - return this.removeFromCache(envelope); - } else if (syncMessage.read && syncMessage.read.length) { - console.log('read messages from', this.getEnvelopeId(envelope)); - return this.handleRead(envelope, syncMessage.read); - } else if (syncMessage.verified) { - return this.handleVerified(envelope, syncMessage.verified); - } else if (syncMessage.configuration) { - return this.handleConfiguration(envelope, syncMessage.configuration); - } else { - throw new Error('Got empty SyncMessage'); - } - }, - handleConfiguration: function(envelope, configuration) { - var ev = new Event('configuration'); + Promise.all(promises).then(() => { + const ev = new Event('groupsync'); ev.confirm = this.removeFromCache.bind(this, envelope); - ev.configuration = { - readReceipts: configuration.readReceipts - }; return this.dispatchAndWait(ev); - }, - handleVerified: function(envelope, verified) { - var ev = new Event('verified'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.verified = { - state: verified.state, - destination: verified.destination, - identityKey: verified.identityKey.toArrayBuffer() - }; - return this.dispatchAndWait(ev); - }, - handleRead: function(envelope, read) { - var results = []; - for (var i = 0; i < read.length; ++i) { - var ev = new Event('readSync'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.timestamp = envelope.timestamp.toNumber(); - ev.read = { - timestamp : read[i].timestamp.toNumber(), - sender : read[i].sender - } - results.push(this.dispatchAndWait(ev)); - } - return Promise.all(results); - }, - handleContacts: function(envelope, contacts) { - console.log('contact sync'); - var attachmentPointer = contacts.blob; - return this.handleAttachment(attachmentPointer).then(function() { - var results = []; - var contactBuffer = new ContactBuffer(attachmentPointer.data); - var contactDetails = contactBuffer.next(); - while (contactDetails !== undefined) { - var ev = new Event('contact'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.contactDetails = contactDetails; - results.push(this.dispatchAndWait(ev)); - - contactDetails = contactBuffer.next(); - } + }); + }); + }, + handleBlocked(envelope, blocked) { + console.log('Setting these numbers as blocked:', blocked.numbers); + textsecure.storage.put('blocked', blocked.numbers); + }, + isBlocked(number) { + return textsecure.storage.get('blocked', []).indexOf(number) >= 0; + }, + handleAttachment(attachment) { + // eslint-disable-next-line no-param-reassign + attachment.id = attachment.id.toString(); + // eslint-disable-next-line no-param-reassign + attachment.key = attachment.key.toArrayBuffer(); + if (attachment.digest) { + // eslint-disable-next-line no-param-reassign + attachment.digest = attachment.digest.toArrayBuffer(); + } + function decryptAttachment(encrypted) { + return textsecure.crypto.decryptAttachment( + encrypted, + attachment.key, + attachment.digest + ); + } - var ev = new Event('contactsync'); - ev.confirm = this.removeFromCache.bind(this, envelope); - results.push(this.dispatchAndWait(ev)); - - return Promise.all(results); - }.bind(this)); - }, - handleGroups: function(envelope, groups) { - console.log('group sync'); - var attachmentPointer = groups.blob; - return this.handleAttachment(attachmentPointer).then(function() { - var groupBuffer = new GroupBuffer(attachmentPointer.data); - var groupDetails = groupBuffer.next(); - var promises = []; - while (groupDetails !== undefined) { - var promise = (function(groupDetails) { - groupDetails.id = groupDetails.id.toBinary(); - if (groupDetails.active) { - return 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() { return groupDetails }); - } else { - return Promise.resolve(groupDetails); - } - })(groupDetails).then(function(groupDetails) { - var ev = new Event('group'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.groupDetails = groupDetails; - return this.dispatchAndWait(ev); - }.bind(this)).catch(function(e) { - console.log('error processing group', e); - }); - groupDetails = groupBuffer.next(); - promises.push(promise); - } + function updateAttachment(data) { + // eslint-disable-next-line no-param-reassign + attachment.data = data; + } - Promise.all(promises).then(function() { - var ev = new Event('groupsync'); - ev.confirm = this.removeFromCache.bind(this, envelope); - return this.dispatchAndWait(ev); - }.bind(this)); - }.bind(this)); - }, - handleBlocked: function(envelope, blocked) { - console.log('Setting these numbers as blocked:', blocked.numbers); - textsecure.storage.put('blocked', blocked.numbers); - }, - isBlocked: function(number) { - return textsecure.storage.get('blocked', []).indexOf(number) >= 0; - }, - handleAttachment: function(attachment) { - attachment.id = attachment.id.toString(); - attachment.key = attachment.key.toArrayBuffer(); - if (attachment.digest) { - attachment.digest = attachment.digest.toArrayBuffer(); - } - function decryptAttachment(encrypted) { - return textsecure.crypto.decryptAttachment( - encrypted, - attachment.key, - attachment.digest - ); - } + return this.server.getAttachment(attachment.id) + .then(decryptAttachment) + .then(updateAttachment); + }, + validateRetryContentMessage(content) { + // Today this is only called for incoming identity key errors, so it can't be a sync + // message. + if (content.syncMessage) { + return false; + } - function updateAttachment(data) { - attachment.data = data; - } + // We want at least one field set, but not more than one + let count = 0; + count += content.dataMessage ? 1 : 0; + count += content.callMessage ? 1 : 0; + count += content.nullMessage ? 1 : 0; + if (count !== 1) { + return false; + } - return this.server.getAttachment(attachment.id) - .then(decryptAttachment) - .then(updateAttachment); - }, - validateRetryContentMessage: function(content) { - // Today this is only called for incoming identity key errors. So it can't be a sync message. - if (content.syncMessage) { - return false; - } + // It's most likely that dataMessage will be populated, so we look at it in detail + const data = content.dataMessage; + if (data && !data.attachments.length && !data.body && !data.expireTimer && + !data.flags && !data.group) { + return false; + } - // We want at least one field set, but not more than one - var count = 0; - count += content.dataMessage ? 1 : 0; - count += content.callMessage ? 1 : 0; - count += content.nullMessage ? 1 : 0; - if (count !== 1) { - return false; - } + return true; + }, + tryMessageAgain(from, ciphertext, message) { + const address = libsignal.SignalProtocolAddress.fromString(from); + const sentAt = message.sent_at || Date.now(); + const receivedAt = message.received_at || Date.now(); + + const ourNumber = textsecure.storage.user.getNumber(); + const number = address.getName(); + const device = address.getDeviceId(); + const options = {}; + + // No limit on message keys if we're communicating with our other devices + if (ourNumber === number) { + options.messageKeysLimit = false; + } - // It's most likely that dataMessage will be populated, so we look at it in detail - var data = content.dataMessage; - if (data && !data.attachments.length && !data.body && !data.expireTimer && !data.flags && !data.group) { - return false; + const sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address, + options + ); + console.log('retrying prekey whisper message'); + return this.decryptPreKeyWhisperMessage( + ciphertext, + sessionCipher, + address + ).then((plaintext) => { + const envelope = { + source: number, + sourceDevice: device, + receivedAt, + timestamp: { + toNumber() { + return sentAt; + }, + }, + }; + + // Before June, all incoming messages were still DataMessage: + // - iOS: Michael Kirk says that they were sending Legacy messages until June + // - Desktop: https://github.com/signalapp/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f + // - Android: https://github.com/signalapp/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958 + // + // var d = new Date('2017-06-01T07:00:00.000Z'); + // d.getTime(); + const startOfJune = 1496300400000; + if (sentAt < startOfJune) { + return this.innerHandleLegacyMessage(envelope, plaintext); + } + + // This is ugly. But we don't know what kind of proto we need to decode... + try { + // Simply decoding as a Content message may throw + const content = textsecure.protobuf.Content.decode(plaintext); + + // But it might also result in an invalid object, so we try to detect that + if (this.validateRetryContentMessage(content)) { + return this.innerHandleContentMessage(envelope, plaintext); } + } catch (e) { + return this.innerHandleLegacyMessage(envelope, plaintext); + } + + return this.innerHandleLegacyMessage(envelope, plaintext); + }); + }, + async handleEndSession(number) { + console.log('got end session'); + const deviceIds = await textsecure.storage.protocol.getDeviceIds(number); + + return Promise.all(deviceIds.map((deviceId) => { + const address = new libsignal.SignalProtocolAddress(number, deviceId); + const sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); + + console.log('deleting sessions for', address.toString()); + return sessionCipher.deleteAllSessionsForDevice(); + })); + }, + processDecrypted(envelope, decrypted, source) { + /* eslint-disable no-bitwise, no-param-reassign */ + const FLAGS = textsecure.protobuf.DataMessage.Flags; + + // 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.expireTimer == null) { + decrypted.expireTimer = 0; + } - return true; - }, - tryMessageAgain: function(from, ciphertext, message) { - var address = libsignal.SignalProtocolAddress.fromString(from); - var sentAt = message.sent_at || Date.now(); - var receivedAt = message.received_at || Date.now(); - - var ourNumber = textsecure.storage.user.getNumber(); - var number = address.getName(); - var device = address.getDeviceId(); - var options = {}; - - // No limit on message keys if we're communicating with our other devices - if (ourNumber === number) { - options.messageKeysLimit = false; - } - var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address, options); - console.log('retrying prekey whisper message'); - return this.decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address).then(function(plaintext) { - var envelope = { - source: number, - sourceDevice: device, - receivedAt: receivedAt, - timestamp: { - toNumber: function() { - return sentAt; - } - } - }; - - // Before June, all incoming messages were still DataMessage: - // - iOS: Michael Kirk says that they were sending Legacy messages until June - // - Desktop: https://github.com/signalapp/Signal-Desktop/commit/e8548879db405d9bcd78b82a456ad8d655592c0f - // - Android: https://github.com/signalapp/libsignal-service-java/commit/61a75d023fba950ff9b4c75a249d1a3408e12958 - // - // var d = new Date('2017-06-01T07:00:00.000Z'); - // d.getTime(); - var startOfJune = 1496300400000; - if (sentAt < startOfJune) { - return this.innerHandleLegacyMessage(envelope, plaintext); - } + if (decrypted.flags & FLAGS.END_SESSION) { + decrypted.body = null; + decrypted.attachments = []; + decrypted.group = null; + return Promise.resolve(decrypted); + } else if (decrypted.flags & FLAGS.EXPIRATION_TIMER_UPDATE) { + decrypted.body = null; + decrypted.attachments = []; + } else if (decrypted.flags & FLAGS.PROFILE_KEY_UPDATE) { + decrypted.body = null; + decrypted.attachments = []; + } else if (decrypted.flags !== 0) { + throw new Error('Unknown flags in message'); + } - // This is ugly. But we don't know what kind of proto we need to decode... - try { - // Simply decoding as a Content message may throw - var content = textsecure.protobuf.Content.decode(plaintext); + const promises = []; - // But it might also result in an invalid object, so we try to detect that - if (this.validateRetryContentMessage(content)) { - return this.innerHandleContentMessage(envelope, plaintext); - } - } catch(e) { - return this.innerHandleLegacyMessage(envelope, plaintext); - } + if (decrypted.group !== null) { + decrypted.group.id = decrypted.group.id.toBinary(); - return this.innerHandleLegacyMessage(envelope, plaintext); - }.bind(this)); - }, - handleEndSession: function(number) { - console.log('got end session'); - return textsecure.storage.protocol.getDeviceIds(number).then(function(deviceIds) { - return Promise.all(deviceIds.map(function(deviceId) { - var address = new libsignal.SignalProtocolAddress(number, deviceId); - var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address); - - console.log('deleting sessions for', address.toString()); - return sessionCipher.deleteAllSessionsForDevice(); - })); - }); - }, - processDecrypted: function(envelope, 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.group.type === textsecure.protobuf.GroupContext.Type.UPDATE) { + if (decrypted.group.avatar !== null) { + promises.push(this.handleAttachment(decrypted.group.avatar)); } - if (decrypted.expireTimer == null) { - decrypted.expireTimer = 0; + } + + const storageGroups = textsecure.storage.groups; + + promises.push(storageGroups.getNumbers(decrypted.group.id).then((existingGroup) => { + if (existingGroup === undefined) { + if (decrypted.group.type !== textsecure.protobuf.GroupContext.Type.UPDATE) { + decrypted.group.members = [source]; + console.log('Got message for unknown group'); + } + return textsecure.storage.groups.createNewGroup( + decrypted.group.members, + decrypted.group.id + ); } + const fromIndex = existingGroup.indexOf(source); - if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { - decrypted.body = null; - decrypted.attachments = []; - decrypted.group = null; - return Promise.resolve(decrypted); - } else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ) { + if (fromIndex < 0) { + // TODO: This could be indication of a race... + console.log('Sender was not a member of the group they were sending from'); + } + + switch (decrypted.group.type) { + case textsecure.protobuf.GroupContext.Type.UPDATE: decrypted.body = null; decrypted.attachments = []; - } else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) { + return textsecure.storage.groups.updateNumbers( + decrypted.group.id, + decrypted.group.members + ); + case textsecure.protobuf.GroupContext.Type.QUIT: decrypted.body = null; decrypted.attachments = []; - } else if (decrypted.flags != 0) { - throw new Error("Unknown flags in message"); - } - - var promises = []; - - if (decrypted.group !== null) { - decrypted.group.id = decrypted.group.id.toBinary(); - - if (decrypted.group.type == textsecure.protobuf.GroupContext.Type.UPDATE) { - if (decrypted.group.avatar !== null) { - promises.push(this.handleAttachment(decrypted.group.avatar)); - } + if (source === this.number) { + return textsecure.storage.groups.deleteGroup(decrypted.group.id); } - - promises.push(textsecure.storage.groups.getNumbers(decrypted.group.id).then(function(existingGroup) { - if (existingGroup === undefined) { - if (decrypted.group.type != textsecure.protobuf.GroupContext.Type.UPDATE) { - decrypted.group.members = [source]; - console.log("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... - console.log("Sender was not a member of the group they were sending from"); - } - - switch(decrypted.group.type) { - case textsecure.protobuf.GroupContext.Type.UPDATE: - decrypted.body = null; - decrypted.attachments = []; - return textsecure.storage.groups.updateNumbers( - decrypted.group.id, decrypted.group.members - ); - - break; - case textsecure.protobuf.GroupContext.Type.QUIT: - decrypted.body = null; - decrypted.attachments = []; - if (source === this.number) { - return textsecure.storage.groups.deleteGroup(decrypted.group.id); - } else { - 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: - this.removeFromCache(envelope); - throw new Error("Unknown group message type"); - } - } - }.bind(this))); + 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; + return Promise.resolve(); + default: + this.removeFromCache(envelope); + 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; - }); + for (let i = 0, max = decrypted.attachments.length; i < max; i += 1) { + const attachment = decrypted.attachments[i]; + promises.push(this.handleAttachment(attachment)); } + return Promise.all(promises).then(() => decrypted); + /* eslint-enable no-bitwise, no-param-reassign */ + }, }); window.textsecure = window.textsecure || {}; -textsecure.MessageReceiver = function(url, username, password, signalingKey, options) { - var messageReceiver = new MessageReceiver(url, username, password, signalingKey, options); - 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.replay.registerFunction(messageReceiver.tryMessageAgain.bind(messageReceiver), textsecure.replay.Type.INIT_SESSION); +textsecure.MessageReceiver = (url, username, password, signalingKey, options) => { + const messageReceiver = new MessageReceiver( + url, + username, + password, + signalingKey, + options + ); + 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.replay.registerFunction( + messageReceiver.tryMessageAgain.bind(messageReceiver), + textsecure.replay.Type.INIT_SESSION + ); }; textsecure.MessageReceiver.prototype = { - constructor: textsecure.MessageReceiver + constructor: textsecure.MessageReceiver, }; diff --git a/test/styleguide/legacy_bridge.js b/test/styleguide/legacy_bridge.js index 12e8db827..26d7c2f5d 100644 --- a/test/styleguide/legacy_bridge.js +++ b/test/styleguide/legacy_bridge.js @@ -34,7 +34,7 @@ window.Signal.Migrations = { transaction.db.createObjectStore('conversations'); next(); }, - version: 1 + version: 1, }], loadAttachmentData: attachment => Promise.resolve(attachment), };