diff --git a/Gruntfile.js b/Gruntfile.js index acb828fc8..0ac83113e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -52,7 +52,6 @@ module.exports = grunt => { libtextsecuretest: { src: [ 'node_modules/jquery/dist/jquery.js', - 'components/mock-socket/dist/mock-socket.js', 'node_modules/mocha/mocha.js', 'node_modules/chai/chai.js', 'libtextsecure/test/_test.js', diff --git a/bower.json b/bower.json index cd72635b0..c874301fc 100644 --- a/bower.json +++ b/bower.json @@ -11,7 +11,6 @@ "webaudiorecorder": "https://github.com/higuma/web-audio-recorder-js.git" }, "devDependencies": { - "mock-socket": "~0.3.2" }, "preen": { "bytebuffer": [ @@ -20,9 +19,6 @@ "long": [ "dist/Long.js" ], - "mock-socket": [ - "dist/mock-socket.js" - ], "mp3lameencoder": [ "lib/Mp3LameEncoder.js" ], diff --git a/components/mock-socket/dist/mock-socket.js b/components/mock-socket/dist/mock-socket.js deleted file mode 100644 index 9ff39d924..000000000 --- a/components/mock-socket/dist/mock-socket.js +++ /dev/null @@ -1,635 +0,0 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 3 ? '/' : '') + url.slice(3, url.length).join('/').split('?')[0].split('#')[0]); - var _p = _l.pathname; - - if (_p.charAt(_p.length-1) === '/') { _p=_p.substring(0, _p.length-1); } - var _h = _l.hostname, _hs = _h.split('.'), _ps = _p.split('/'); - - if (arg === 'hostname') { return _h; } - else if (arg === 'domain') { - if (/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(_h)) { return _h; } - return _hs.slice(-2).join('.'); - } - //else if (arg === 'tld') { return _hs.slice(-1).join('.'); } - else if (arg === 'sub') { return _hs.slice(0, _hs.length - 2).join('.'); } - else if (arg === 'port') { return _l.port; } - else if (arg === 'protocol') { return _l.protocol.split(':')[0]; } - else if (arg === 'auth') { return _l.auth; } - else if (arg === 'user') { return _l.auth.split(':')[0]; } - else if (arg === 'pass') { return _l.auth.split(':')[1] || ''; } - else if (arg === 'path') { return _l.pathname; } - else if (arg.charAt(0) === '.') { - arg = arg.substring(1); - if(isNumeric(arg)) {arg = parseInt(arg, 10); return _hs[arg < 0 ? _hs.length + arg : arg-1] || ''; } - } - else if (isNumeric(arg)) { arg = parseInt(arg, 10); return _ps[arg < 0 ? _ps.length + arg : arg] || ''; } - else if (arg === 'file') { return _ps.slice(-1)[0]; } - else if (arg === 'filename') { return _ps.slice(-1)[0].split('.')[0]; } - else if (arg === 'fileext') { return _ps.slice(-1)[0].split('.')[1] || ''; } - else if (arg.charAt(0) === '?' || arg.charAt(0) === '#') { - var params = _ls, param = null; - - if(arg.charAt(0) === '?') { params = (params.split('?')[1] || '').split('#')[0]; } - else if(arg.charAt(0) === '#') { params = (params.split('#')[1] || ''); } - - if(!arg.charAt(1)) { return params; } - - arg = arg.substring(1); - params = params.split('&'); - - for(var i=0,ii=params.length; i= 0 && newReadyState <= 4) { - this.readyState = newReadyState; - } - } -}; - -module.exports = MockSocket; - -},{"./helpers/delay":2,"./helpers/global-context":3,"./helpers/message-event":4,"./helpers/url-transform":5,"./helpers/websocket-properties":6}],9:[function(require,module,exports){ -var socketMessageEvent = require('./helpers/message-event'); -var globalContext = require('./helpers/global-context'); - -function SocketService() { - this.list = {}; -} - -SocketService.prototype = { - server: null, - - /* - * This notifies the mock server that a client is connecting and also sets up - * the ready state observer. - * - * @param {client: object} the context of the client - * @param {readyStateFunction: function} the function that will be invoked on a ready state change - */ - clientIsConnecting: function(client, readyStateFunction) { - this.observe('updateReadyState', readyStateFunction, client); - - // if the server has not been set then we notify the onclose method of this client - if(!this.server) { - this.notify(client, 'updateReadyState', globalContext.MockSocket.CLOSED); - this.notifyOnlyFor(client, 'clientOnError'); - return false; - } - - this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.OPEN); - this.notify('clientHasJoined', this.server); - this.notifyOnlyFor(client, 'clientOnOpen', socketMessageEvent('open', null, this.server.url)); - }, - - /* - * Closes a connection from the server's perspective. This should - * close all clients. - * - * @param {messageEvent: object} the mock message event. - */ - closeConnectionFromServer: function(messageEvent) { - this.notify('updateReadyState', globalContext.MockSocket.CLOSING); - this.notify('clientOnclose', messageEvent); - this.notify('updateReadyState', globalContext.MockSocket.CLOSED); - this.notify('clientHasLeft'); - }, - - /* - * Closes a connection from the clients perspective. This - * should only close the client who initiated the close and not - * all of the other clients. - * - * @param {messageEvent: object} the mock message event. - * @param {client: object} the context of the client - */ - closeConnectionFromClient: function(messageEvent, client) { - if(client.readyState === globalContext.MockSocket.OPEN) { - this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSING); - this.notifyOnlyFor(client, 'clientOnclose', messageEvent); - this.notifyOnlyFor(client, 'updateReadyState', globalContext.MockSocket.CLOSED); - this.notify('clientHasLeft'); - } - }, - - - /* - * Notifies the mock server that a client has sent a message. - * - * @param {messageEvent: object} the mock message event. - */ - sendMessageToServer: function(messageEvent) { - this.notify('clientHasSentMessage', messageEvent.data, messageEvent); - }, - - /* - * Notifies all clients that the server has sent a message - * - * @param {messageEvent: object} the mock message event. - */ - sendMessageToClients: function(messageEvent) { - this.notify('clientOnMessage', messageEvent); - }, - - /* - * Setup the callback function observers for both the server and client. - * - * @param {observerKey: string} either: connection, message or close - * @param {callback: function} the callback to be invoked - * @param {server: object} the context of the server - */ - setCallbackObserver: function(observerKey, callback, server) { - this.observe(observerKey, callback, server); - }, - - /* - * Binds a callback to a namespace. If notify is called on a namespace all "observers" will be - * fired with the context that is passed in. - * - * @param {namespace: string} - * @param {callback: function} - * @param {context: object} - */ - observe: function(namespace, callback, context) { - - // Make sure the arguments are of the correct type - if( typeof namespace !== 'string' || typeof callback !== 'function' || (context && typeof context !== 'object')) { - return false; - } - - // If a namespace has not been created before then we need to "initialize" the namespace - if(!this.list[namespace]) { - this.list[namespace] = []; - } - - this.list[namespace].push({callback: callback, context: context}); - }, - - /* - * Remove all observers from a given namespace. - * - * @param {namespace: string} The namespace to clear. - */ - clearAll: function(namespace) { - - if(!this.verifyNamespaceArg(namespace)) { - return false; - } - - this.list[namespace] = []; - }, - - /* - * Notify all callbacks that have been bound to the given namespace. - * - * @param {namespace: string} The namespace to notify observers on. - * @param {namespace: url} The url to notify observers on. - */ - notify: function(namespace) { - - // This strips the namespace from the list of args as we dont want to pass that into the callback. - var argumentsForCallback = Array.prototype.slice.call(arguments, 1); - - if(!this.verifyNamespaceArg(namespace)) { - return false; - } - - // Loop over all of the observers and fire the callback function with the context. - for(var i = 0, len = this.list[namespace].length; i < len; i++) { - this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback); - } - }, - - /* - * Notify only the callback of the given context and namespace. - * - * @param {context: object} the context to match against. - * @param {namespace: string} The namespace to notify observers on. - */ - notifyOnlyFor: function(context, namespace) { - - // This strips the namespace from the list of args as we dont want to pass that into the callback. - var argumentsForCallback = Array.prototype.slice.call(arguments, 2); - - if(!this.verifyNamespaceArg(namespace)) { - return false; - } - - // Loop over all of the observers and fire the callback function with the context. - for(var i = 0, len = this.list[namespace].length; i < len; i++) { - if(this.list[namespace][i].context === context) { - this.list[namespace][i].callback.apply(this.list[namespace][i].context, argumentsForCallback); - } - } - }, - - /* - * Verifies that the namespace is valid. - * - * @param {namespace: string} The namespace to verify. - */ - verifyNamespaceArg: function(namespace) { - if(typeof namespace !== 'string' || !this.list[namespace]) { - return false; - } - - return true; - } -}; - -module.exports = SocketService; - -},{"./helpers/global-context":3,"./helpers/message-event":4}]},{},[1]); diff --git a/js/modules/signal.js b/js/modules/signal.js index 2d385f568..31276df3a 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -17,7 +17,7 @@ const GroupChange = require('../../ts/groupChange'); const IndexedDB = require('./indexeddb'); const Notifications = require('../../ts/notifications'); const OS = require('../../ts/OS'); -const Stickers = require('./stickers'); +const Stickers = require('../../ts/types/Stickers'); const Settings = require('./settings'); const RemoteConfig = require('../../ts/RemoteConfig'); const Util = require('../../ts/util'); diff --git a/js/modules/stickers.d.ts b/js/modules/stickers.d.ts deleted file mode 100644 index fe48e699b..000000000 --- a/js/modules/stickers.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -export function maybeDeletePack(packId: string): Promise; - -export function downloadStickerPack( - packId: string, - packKey: string, - options?: { - finalStatus?: 'installed' | 'downloaded'; - messageId?: string; - fromSync?: boolean; - } -): Promise; - -export function isPackIdValid(packId: unknown): packId is string; - -export function redactPackId(packId: string): string; diff --git a/libtextsecure/test/.eslintrc.js b/libtextsecure/test/.eslintrc.js index e350d68a5..1e3a1f5e0 100644 --- a/libtextsecure/test/.eslintrc.js +++ b/libtextsecure/test/.eslintrc.js @@ -20,8 +20,6 @@ module.exports = { dcodeIO: true, getString: true, hexToArrayBuffer: true, - MockServer: true, - MockSocket: true, PROTO_ROOT: true, stringToArrayBuffer: true, }, diff --git a/libtextsecure/test/_test.js b/libtextsecure/test/_test.js index ebb57b030..615242455 100644 --- a/libtextsecure/test/_test.js +++ b/libtextsecure/test/_test.js @@ -60,8 +60,6 @@ window.hexToArrayBuffer = str => { return ret; }; -window.MockSocket.prototype.addEventListener = () => null; - window.Whisper = window.Whisper || {}; window.Whisper.events = { on() {}, diff --git a/libtextsecure/test/contacts_parser_test.js b/libtextsecure/test/contacts_parser_test.js deleted file mode 100644 index 887dd8f65..000000000 --- a/libtextsecure/test/contacts_parser_test.js +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -describe('ContactBuffer', () => { - function getTestBuffer() { - const buffer = new dcodeIO.ByteBuffer(); - const avatarBuffer = new dcodeIO.ByteBuffer(); - const avatarLen = 255; - for (let i = 0; i < avatarLen; i += 1) { - avatarBuffer.writeUint8(i); - } - avatarBuffer.limit = avatarBuffer.offset; - avatarBuffer.offset = 0; - const contactInfo = new window.textsecure.protobuf.ContactDetails({ - name: 'Zero Cool', - number: '+10000000000', - uuid: '7198E1BD-1293-452A-A098-F982FF201902', - avatar: { contentType: 'image/jpeg', length: avatarLen }, - }); - const contactInfoBuffer = contactInfo.encode().toArrayBuffer(); - - for (let i = 0; i < 3; i += 1) { - buffer.writeVarint32(contactInfoBuffer.byteLength); - buffer.append(contactInfoBuffer); - buffer.append(avatarBuffer.clone()); - } - - buffer.limit = buffer.offset; - buffer.offset = 0; - return buffer.toArrayBuffer(); - } - - it('parses an array buffer of contacts', () => { - const arrayBuffer = getTestBuffer(); - const contactBuffer = new window.textsecure.ContactBuffer(arrayBuffer); - let contact = contactBuffer.next(); - let count = 0; - while (contact !== undefined) { - count += 1; - assert.strictEqual(contact.name, 'Zero Cool'); - assert.strictEqual(contact.number, '+10000000000'); - assert.strictEqual(contact.uuid, '7198e1bd-1293-452a-a098-f982ff201902'); - assert.strictEqual(contact.avatar.contentType, 'image/jpeg'); - assert.strictEqual(contact.avatar.length, 255); - assert.strictEqual(contact.avatar.data.byteLength, 255); - const avatarBytes = new Uint8Array(contact.avatar.data); - for (let j = 0; j < 255; j += 1) { - assert.strictEqual(avatarBytes[j], j); - } - contact = contactBuffer.next(); - } - assert.strictEqual(count, 3); - }); -}); - -describe('GroupBuffer', () => { - function getTestBuffer() { - const buffer = new dcodeIO.ByteBuffer(); - const avatarBuffer = new dcodeIO.ByteBuffer(); - const avatarLen = 255; - for (let i = 0; i < avatarLen; i += 1) { - avatarBuffer.writeUint8(i); - } - avatarBuffer.limit = avatarBuffer.offset; - avatarBuffer.offset = 0; - const groupInfo = new window.textsecure.protobuf.GroupDetails({ - id: window.Signal.Crypto.typedArrayToArrayBuffer( - new Uint8Array([1, 3, 3, 7]) - ), - name: 'Hackers', - membersE164: ['cereal', 'burn', 'phreak', 'joey'], - avatar: { contentType: 'image/jpeg', length: avatarLen }, - }); - const groupInfoBuffer = groupInfo.encode().toArrayBuffer(); - - for (let i = 0; i < 3; i += 1) { - buffer.writeVarint32(groupInfoBuffer.byteLength); - buffer.append(groupInfoBuffer); - buffer.append(avatarBuffer.clone()); - } - - buffer.limit = buffer.offset; - buffer.offset = 0; - return buffer.toArrayBuffer(); - } - - it('parses an array buffer of groups', () => { - const arrayBuffer = getTestBuffer(); - const groupBuffer = new window.textsecure.GroupBuffer(arrayBuffer); - let group = groupBuffer.next(); - let count = 0; - while (group !== undefined) { - count += 1; - assert.strictEqual(group.name, 'Hackers'); - assertEqualArrayBuffers( - group.id.toArrayBuffer(), - window.Signal.Crypto.typedArrayToArrayBuffer( - new Uint8Array([1, 3, 3, 7]) - ) - ); - assert.sameMembers(group.membersE164, [ - 'cereal', - 'burn', - 'phreak', - 'joey', - ]); - assert.strictEqual(group.avatar.contentType, 'image/jpeg'); - assert.strictEqual(group.avatar.length, 255); - assert.strictEqual(group.avatar.data.byteLength, 255); - const avatarBytes = new Uint8Array(group.avatar.data); - for (let j = 0; j < 255; j += 1) { - assert.strictEqual(avatarBytes[j], j); - } - group = groupBuffer.next(); - } - assert.strictEqual(count, 3); - }); -}); diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index 2b731981b..8614ae33c 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -39,11 +39,9 @@ - - diff --git a/libtextsecure/test/message_receiver_test.js b/libtextsecure/test/message_receiver_test.js deleted file mode 100644 index 5e6aa4f90..000000000 --- a/libtextsecure/test/message_receiver_test.js +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global textsecure */ - -describe('MessageReceiver', () => { - const { WebSocket } = window; - const number = '+19999999999'; - const uuid = 'AAAAAAAA-BBBB-4CCC-9DDD-EEEEEEEEEEEE'; - const deviceId = 1; - const signalingKey = window.Signal.Crypto.getRandomBytes(32 + 20); - - before(() => { - localStorage.clear(); - window.WebSocket = MockSocket; - textsecure.storage.user.setNumberAndDeviceId(number, deviceId, 'name'); - textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId); - textsecure.storage.put('password', 'password'); - textsecure.storage.put('signaling_key', signalingKey); - }); - after(() => { - localStorage.clear(); - window.WebSocket = WebSocket; - }); - - describe('connecting', () => { - let attrs; - let websocketmessage; - - before(() => { - attrs = { - type: textsecure.protobuf.Envelope.Type.CIPHERTEXT, - source: number, - sourceUuid: uuid, - sourceDevice: deviceId, - timestamp: Date.now(), - content: window.Signal.Crypto.getRandomBytes(200), - }; - const body = new textsecure.protobuf.Envelope(attrs).toArrayBuffer(); - - websocketmessage = new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: { verb: 'PUT', path: '/api/v1/message', body }, - }); - }); - - it('generates decryption-error event when it cannot decrypt', done => { - const mockServer = new MockServer('ws://localhost:8081/'); - - mockServer.on('connection', server => { - setTimeout(() => { - server.send(new Blob([websocketmessage.toArrayBuffer()])); - }, 1); - }); - - const messageReceiver = new textsecure.MessageReceiver( - 'oldUsername.2', - 'username.2', - 'password', - 'signalingKey', - { - serverTrustRoot: 'AAAAAAAA', - } - ); - - messageReceiver.addEventListener('decrytion-error', done()); - }); - }); - - // For when we start testing individual MessageReceiver methods - - // describe('methods', () => { - // let messageReceiver; - // let mockServer; - - // beforeEach(() => { - // // Necessary to populate the server property inside of MockSocket. Without it, we - // // crash when doing any number of things to a MockSocket instance. - // mockServer = new MockServer('ws://localhost:8081'); - - // messageReceiver = new textsecure.MessageReceiver( - // 'oldUsername.3', - // 'username.3', - // 'password', - // 'signalingKey', - // { - // serverTrustRoot: 'AAAAAAAA', - // } - // ); - // }); - // afterEach(() => { - // mockServer.close(); - // }); - // }); -}); diff --git a/libtextsecure/test/protocol_test.js b/libtextsecure/test/protocol_test.js deleted file mode 100644 index 609877f10..000000000 --- a/libtextsecure/test/protocol_test.js +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global textsecure */ - -describe('Protocol', () => { - describe('Unencrypted PushMessageProto "decrypt"', () => { - // exclusive - it('works', done => { - localStorage.clear(); - - const textMessage = new textsecure.protobuf.DataMessage(); - textMessage.body = 'Hi Mom'; - const serverMessage = { - type: 4, // unencrypted - source: '+19999999999', - timestamp: 42, - message: textMessage.encode(), - }; - - return textsecure.protocol_wrapper - .handleEncryptedMessage( - serverMessage.source, - serverMessage.source_device, - serverMessage.type, - serverMessage.message - ) - .then(message => { - assert.equal(message.body, textMessage.body); - assert.equal( - message.attachments.length, - textMessage.attachments.length - ); - assert.equal(textMessage.attachments.length, 0); - }) - .then(done) - .catch(done); - }); - }); -}); diff --git a/libtextsecure/test/websocket_test.js b/libtextsecure/test/websocket_test.js deleted file mode 100644 index e1b44991e..000000000 --- a/libtextsecure/test/websocket_test.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2015-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global TextSecureWebSocket */ - -describe('TextSecureWebSocket', () => { - const RealWebSocket = window.WebSocket; - before(() => { - window.WebSocket = MockSocket; - }); - after(() => { - window.WebSocket = RealWebSocket; - }); - it('connects and disconnects', done => { - const mockServer = new MockServer('ws://localhost:8080'); - mockServer.on('connection', server => { - socket.close(); - server.close(); - done(); - }); - const socket = new TextSecureWebSocket('ws://localhost:8080'); - }); - - it('sends and receives', done => { - const mockServer = new MockServer('ws://localhost:8080'); - mockServer.on('connection', server => { - server.on('message', () => { - server.send('ack'); - server.close(); - }); - }); - const socket = new TextSecureWebSocket('ws://localhost:8080'); - socket.onmessage = response => { - assert.strictEqual(response.data, 'ack'); - socket.close(); - done(); - }; - socket.send('syn'); - }); - - it('exposes the socket status', done => { - const mockServer = new MockServer('ws://localhost:8082'); - mockServer.on('connection', server => { - assert.strictEqual(socket.getStatus(), WebSocket.OPEN); - server.close(); - socket.close(); - }); - const socket = new TextSecureWebSocket('ws://localhost:8082'); - socket.onclose = () => { - assert.strictEqual(socket.getStatus(), WebSocket.CLOSING); - done(); - }; - }); - - it('reconnects', function thisNeeded(done) { - this.timeout(60000); - const mockServer = new MockServer('ws://localhost:8082'); - const socket = new TextSecureWebSocket('ws://localhost:8082'); - socket.onclose = () => { - const secondServer = new MockServer('ws://localhost:8082'); - secondServer.on('connection', server => { - socket.close(); - server.close(); - done(); - }); - }; - mockServer.close(); - }); -}); diff --git a/preload.js b/preload.js index 0a021b42a..49b823142 100644 --- a/preload.js +++ b/preload.js @@ -509,25 +509,6 @@ try { // https://stackoverflow.com/a/23299989 window.isValidE164 = maybeE164 => /^\+?[1-9]\d{1,14}$/.test(maybeE164); - window.normalizeUuids = (obj, paths, context) => { - if (!obj) { - return; - } - paths.forEach(path => { - const val = _.get(obj, path); - if (val) { - if (!val || !window.isValidGuid(val)) { - window.log.warn( - `Normalizing invalid uuid: ${val} at path ${path} in context "${context}"` - ); - } - if (val && val.toLowerCase) { - _.set(obj, path, val.toLowerCase()); - } - } - }); - }; - window.React = require('react'); window.ReactDOM = require('react-dom'); window.moment = require('moment'); diff --git a/test/.eslintrc.js b/test/.eslintrc.js index 0eba343e8..909ded871 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -15,8 +15,6 @@ module.exports = { dcodeIO: true, getString: true, hexToArrayBuffer: true, - MockServer: true, - MockSocket: true, PROTO_ROOT: true, stringToArrayBuffer: true, }, diff --git a/test/stickers_test.js b/test/stickers_test.js index 622f34aa7..2df5b8430 100644 --- a/test/stickers_test.js +++ b/test/stickers_test.js @@ -7,58 +7,58 @@ const { Stickers } = Signal; describe('Stickers', () => { describe('getDataFromLink', () => { - it('returns null for invalid URLs', () => { - assert.isNull(Stickers.getDataFromLink('https://')); - assert.isNull(Stickers.getDataFromLink('signal.art/addstickers/')); + it('returns undefined for invalid URLs', () => { + assert.isUndefined(Stickers.getDataFromLink('https://')); + assert.isUndefined(Stickers.getDataFromLink('signal.art/addstickers/')); }); - it("returns null for URLs that don't have a hash", () => { - assert.isNull( + it("returns undefined for URLs that don't have a hash", () => { + assert.isUndefined( Stickers.getDataFromLink('https://signal.art/addstickers/') ); - assert.isNull( + assert.isUndefined( Stickers.getDataFromLink('https://signal.art/addstickers/#') ); }); - it('returns null when no key or pack ID is found', () => { - assert.isNull( + it('returns undefined when no key or pack ID is found', () => { + assert.isUndefined( Stickers.getDataFromLink( 'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a' ) ); - assert.isNull( + assert.isUndefined( Stickers.getDataFromLink( 'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key=' ) ); - assert.isNull( + assert.isUndefined( Stickers.getDataFromLink( 'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' ) ); - assert.isNull( + assert.isUndefined( Stickers.getDataFromLink( 'https://signal.art/addstickers/#pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e&pack_id=' ) ); }); - it('returns null when the pack ID is invalid', () => { - assert.isNull( + it('returns undefined when the pack ID is invalid', () => { + assert.isUndefined( Stickers.getDataFromLink( 'https://signal.art/addstickers/#pack_id=garbage&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' ) ); }); - it('returns null if the ID or key are passed as arrays', () => { - assert.isNull( + it('returns undefined if the ID or key are passed as arrays', () => { + assert.isUndefined( Stickers.getDataFromLink( 'https://signal.art/addstickers/#pack_id[]=c8c83285b547872ac4c589d64a6edd6a&pack_key=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' ) ); - assert.isNull( + assert.isUndefined( Stickers.getDataFromLink( 'https://signal.art/addstickers/#pack_id=c8c83285b547872ac4c589d64a6edd6a&pack_key[]=59bb3a8860f0e6a5a83a5337a015c8d55ecd2193f82d77202f3b8112a845636e' ) diff --git a/ts/background.ts b/ts/background.ts index 621221937..846a49335 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1,7 +1,7 @@ // Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { isNumber } from 'lodash'; +import { isNumber, noop } from 'lodash'; import { bindActionCreators } from 'redux'; import { render } from 'react-dom'; import { @@ -9,16 +9,23 @@ import { PlaintextContent, } from '@signalapp/signal-client'; -import { DataMessageClass, SyncMessageClass } from './textsecure.d'; -import { SessionResetsType } from './textsecure/Types.d'; -import { MessageAttributesType } from './model-types.d'; +import MessageReceiver from './textsecure/MessageReceiver'; +import { SessionResetsType, ProcessedDataMessage } from './textsecure/Types.d'; +import { + MessageAttributesType, + ConversationAttributesType, +} from './model-types.d'; +import * as Bytes from './Bytes'; +import { typedArrayToArrayBuffer } from './Crypto'; import { WhatIsThis } from './window.d'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { SocketStatus } from './types/SocketStatus'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; import { ChallengeHandler } from './challenge'; import { isWindowDragElement } from './util/isWindowDragElement'; -import { assert } from './util/assert'; +import { assert, strictAssert } from './util/assert'; +import { dropNull } from './util/dropNull'; +import { normalizeUuid } from './util/normalizeUuid'; import { filter } from './util/iterables'; import { isNotNil } from './util/isNotNil'; import { senderCertificateService } from './services/senderCertificate'; @@ -36,9 +43,30 @@ import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey' import { LatestQueue } from './util/LatestQueue'; import { parseIntOrThrow } from './util/parseIntOrThrow'; import { - DecryptionErrorType, - RetryRequestType, -} from './textsecure/MessageReceiver'; + TypingEvent, + ErrorEvent, + DeliveryEvent, + DecryptionErrorEvent, + DecryptionErrorEventData, + SentEvent, + SentEventData, + ProfileKeyUpdateEvent, + MessageEvent, + MessageEventData, + RetryRequestEvent, + RetryRequestEventData, + ReadEvent, + ConfigurationEvent, + ViewSyncEvent, + MessageRequestResponseEvent, + FetchLatestEvent, + KeysEvent, + StickerPackEvent, + VerifiedEvent, + ReadSyncEvent, + ContactEvent, + GroupEvent, +} from './textsecure/messageReceiverEvents'; import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials'; import * as universalExpireTimer from './util/universalExpireTimer'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; @@ -59,6 +87,7 @@ import { SystemTraySetting, parseSystemTraySetting, } from './types/SystemTraySetting'; +import * as Stickers from './types/Stickers'; import { SignalService as Proto } from './protobuf'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -149,7 +178,7 @@ export async function startApp(): Promise { name: 'Whisper.deliveryReceiptBatcher', wait: 500, maxSize: 500, - processBatch: async (items: WhatIsThis) => { + processBatch: async items => { const byConversationId = window._.groupBy(items, item => window.ConversationController.ensureContactIds({ e164: item.source, @@ -320,7 +349,7 @@ export async function startApp(): Promise { window.getAccountManager()!.refreshPreKeys(); }); - let messageReceiver: WhatIsThis; + let messageReceiver: MessageReceiver | undefined; let preMessageReceiverStatus: SocketStatus | undefined; window.getSocketStatus = () => { if (messageReceiver) { @@ -589,7 +618,7 @@ export async function startApp(): Promise { if (messageReceiver) { messageReceiver.unregisterBatchers(); - messageReceiver = null; + messageReceiver = undefined; } // A number of still-to-queue database queries might be waiting inside batchers. @@ -619,14 +648,14 @@ export async function startApp(): Promise { window.isShowingModal = true; // Kick off the download - window.Signal.Stickers.downloadEphemeralPack(packId, key); + Stickers.downloadEphemeralPack(packId, key); const props = { packId, onClose: async () => { window.isShowingModal = false; stickerPreviewModalView.remove(); - await window.Signal.Stickers.removeEphemeralPack(packId); + await Stickers.removeEphemeralPack(packId); }, }; @@ -703,7 +732,7 @@ export async function startApp(): Promise { }, installStickerPack: async (packId: string, key: string) => { - window.Signal.Stickers.downloadStickerPack(packId, key, { + Stickers.downloadStickerPack(packId, key, { finalStatus: 'installed', }); }, @@ -894,7 +923,7 @@ export async function startApp(): Promise { try { await Promise.all([ window.ConversationController.load(), - window.Signal.Stickers.load(), + Stickers.load(), window.Signal.Emojis.load(), window.textsecure.storage.protocol.hydrateCaches(), ]); @@ -963,7 +992,7 @@ export async function startApp(): Promise { }, emojis: window.Signal.Emojis.getInitialState(), items: window.storage.getItemsState(), - stickers: window.Signal.Stickers.getInitialState(), + stickers: Stickers.getInitialState(), user: { attachmentsPath: window.baseAttachmentsPath, stickersPath: window.baseStickersPath, @@ -1850,6 +1879,8 @@ export async function startApp(): Promise { } window.getSyncRequest = (timeoutMillis?: number) => { + strictAssert(messageReceiver, 'MessageReceiver not initialized'); + const syncRequest = new window.textsecure.SyncRequest( window.textsecure.messaging, messageReceiver, @@ -1859,8 +1890,8 @@ export async function startApp(): Promise { return syncRequest; }; - let disconnectTimer: WhatIsThis | null = null; - let reconnectTimer: WhatIsThis | null = null; + let disconnectTimer: NodeJS.Timeout | undefined; + let reconnectTimer: number | undefined; function onOffline() { window.log.info('offline'); @@ -1886,12 +1917,12 @@ export async function startApp(): Promise { if (disconnectTimer && isSocketOnline()) { window.log.warn('Already online. Had a blip in online/offline status.'); clearTimeout(disconnectTimer); - disconnectTimer = null; + disconnectTimer = undefined; return; } if (disconnectTimer) { clearTimeout(disconnectTimer); - disconnectTimer = null; + disconnectTimer = undefined; } connect(); @@ -1909,7 +1940,7 @@ export async function startApp(): Promise { window.log.info('disconnect'); // Clear timer, since we're only called when the timer is expired - disconnectTimer = null; + disconnectTimer = undefined; AttachmentDownloads.stop(); if (messageReceiver) { @@ -1934,7 +1965,7 @@ export async function startApp(): Promise { if (reconnectTimer) { clearTimeout(reconnectTimer); - reconnectTimer = null; + reconnectTimer = undefined; } // Bootstrap our online/offline detection, only the first time we connect @@ -1964,7 +1995,7 @@ export async function startApp(): Promise { if (messageReceiver) { messageReceiver.unregisterBatchers(); - messageReceiver = null; + messageReceiver = undefined; } const OLD_USERNAME = window.storage.get('number_id', ''); @@ -2043,8 +2074,11 @@ export async function startApp(): Promise { preMessageReceiverStatus = undefined; // eslint-disable-next-line no-inner-declarations - function addQueuedEventListener(name: string, handler: WhatIsThis) { - messageReceiver.addEventListener(name, (...args: Array) => + function queuedEventListener>( + handler: (...args: Args) => Promise | void, + track = true + ): (...args: Args) => void { + return (...args: Args): void => { eventHandlerQueue.add(async () => { try { await handler(...args); @@ -2052,40 +2086,97 @@ export async function startApp(): Promise { // message/sent: Message.handleDataMessage has its own queue and will // trigger this event itself when complete. // error: Error processing (below) also has its own queue and self-trigger. - if (name !== 'message' && name !== 'sent' && name !== 'error') { + if (track) { window.Whisper.events.trigger('incrementProgress'); } } - }) - ); + }); + }; } - addQueuedEventListener('message', onMessageReceived); - addQueuedEventListener('delivery', onDeliveryReceipt); - addQueuedEventListener('contact', onContactReceived); - addQueuedEventListener('contactsync', onContactSyncComplete); - addQueuedEventListener('group', onGroupReceived); - addQueuedEventListener('groupsync', onGroupSyncComplete); - addQueuedEventListener('sent', onSentMessage); - addQueuedEventListener('readSync', onReadSync); - addQueuedEventListener('read', onReadReceipt); - addQueuedEventListener('verified', onVerified); - addQueuedEventListener('error', onError); - addQueuedEventListener('decryption-error', onDecryptionError); - addQueuedEventListener('retry-request', onRetryRequest); - addQueuedEventListener('empty', onEmpty); - addQueuedEventListener('reconnect', onReconnect); - addQueuedEventListener('configuration', onConfiguration); - addQueuedEventListener('typing', onTyping); - addQueuedEventListener('sticker-pack', onStickerPack); - addQueuedEventListener('viewSync', onViewSync); - addQueuedEventListener( - 'messageRequestResponse', - onMessageRequestResponse + messageReceiver.addEventListener( + 'message', + queuedEventListener(onMessageReceived, false) ); - addQueuedEventListener('profileKeyUpdate', onProfileKeyUpdate); - addQueuedEventListener('fetchLatest', onFetchLatestSync); - addQueuedEventListener('keys', onKeysSync); + messageReceiver.addEventListener( + 'delivery', + queuedEventListener(onDeliveryReceipt) + ); + messageReceiver.addEventListener( + 'contact', + queuedEventListener(onContactReceived) + ); + messageReceiver.addEventListener( + 'contactSync', + queuedEventListener(onContactSyncComplete) + ); + messageReceiver.addEventListener( + 'group', + queuedEventListener(onGroupReceived) + ); + messageReceiver.addEventListener( + 'groupSync', + queuedEventListener(onGroupSyncComplete) + ); + messageReceiver.addEventListener( + 'sent', + queuedEventListener(onSentMessage, false) + ); + messageReceiver.addEventListener( + 'readSync', + queuedEventListener(onReadSync) + ); + messageReceiver.addEventListener( + 'read', + queuedEventListener(onReadReceipt) + ); + messageReceiver.addEventListener( + 'verified', + queuedEventListener(onVerified) + ); + messageReceiver.addEventListener( + 'error', + queuedEventListener(onError, false) + ); + messageReceiver.addEventListener( + 'decryption-error', + queuedEventListener(onDecryptionError) + ); + messageReceiver.addEventListener( + 'retry-request', + queuedEventListener(onRetryRequest) + ); + messageReceiver.addEventListener('empty', queuedEventListener(onEmpty)); + messageReceiver.addEventListener( + 'reconnect', + queuedEventListener(onReconnect) + ); + messageReceiver.addEventListener( + 'configuration', + queuedEventListener(onConfiguration) + ); + messageReceiver.addEventListener('typing', queuedEventListener(onTyping)); + messageReceiver.addEventListener( + 'sticker-pack', + queuedEventListener(onStickerPack) + ); + messageReceiver.addEventListener( + 'viewSync', + queuedEventListener(onViewSync) + ); + messageReceiver.addEventListener( + 'messageRequestResponse', + queuedEventListener(onMessageRequestResponse) + ); + messageReceiver.addEventListener( + 'profileKeyUpdate', + queuedEventListener(onProfileKeyUpdate) + ); + messageReceiver.addEventListener( + 'fetchLatest', + queuedEventListener(onFetchLatestSync) + ); + messageReceiver.addEventListener('keys', queuedEventListener(onKeysSync)); AttachmentDownloads.start({ getMessageReceiver: () => messageReceiver, @@ -2093,7 +2184,7 @@ export async function startApp(): Promise { }); if (connectCount === 1) { - window.Signal.Stickers.downloadQueuedPacks(); + Stickers.downloadQueuedPacks(); if (!newVersion) { runStorageService(); } @@ -2229,9 +2320,9 @@ export async function startApp(): Promise { syncMessage: true, }); - const installedStickerPacks = window.Signal.Stickers.getInstalledStickerPacks(); + const installedStickerPacks = Stickers.getInstalledStickerPacks(); if (installedStickerPacks.length) { - const operations = installedStickerPacks.map((pack: WhatIsThis) => ({ + const operations = installedStickerPacks.map(pack => ({ packId: pack.id, packKey: pack.key, installed: true, @@ -2313,18 +2404,22 @@ export async function startApp(): Promise { window.log.info( 'waitForEmptyEventQueue: Waiting for MessageReceiver empty event...' ); - let resolve: WhatIsThis; - let reject: WhatIsThis; - const promise = new Promise((innerResolve, innerReject) => { + let resolve: undefined | (() => void); + let reject: undefined | ((error: Error) => void); + const promise = new Promise((innerResolve, innerReject) => { resolve = innerResolve; reject = innerReject; }); - const timeout = setTimeout(reject, FIVE_MINUTES); + const timeout = reject && setTimeout(reject, FIVE_MINUTES); const onEmptyOnce = () => { - messageReceiver.removeEventListener('empty', onEmptyOnce); + if (messageReceiver) { + messageReceiver.removeEventListener('empty', onEmptyOnce); + } clearTimeout(timeout); - resolve(); + if (resolve) { + resolve(); + } }; messageReceiver.addEventListener('empty', onEmptyOnce); @@ -2459,7 +2554,7 @@ export async function startApp(): Promise { connect(); } - function onConfiguration(ev: WhatIsThis) { + function onConfiguration(ev: ConfigurationEvent) { ev.confirm(); const { configuration } = ev; @@ -2470,7 +2565,7 @@ export async function startApp(): Promise { linkPreviews, } = configuration; - window.storage.put('read-receipt-setting', readReceipts); + window.storage.put('read-receipt-setting', Boolean(readReceipts)); if ( unidentifiedDeliveryIndicators === true || @@ -2491,7 +2586,7 @@ export async function startApp(): Promise { } } - function onTyping(ev: WhatIsThis) { + function onTyping(ev: TypingEvent) { // Note: this type of message is automatically removed from cache in MessageReceiver const { typing, sender, senderUuid, senderDevice } = ev; @@ -2557,12 +2652,12 @@ export async function startApp(): Promise { }); } - async function onStickerPack(ev: WhatIsThis) { + async function onStickerPack(ev: StickerPackEvent) { ev.confirm(); - const packs = ev.stickerPacks || []; + const packs = ev.stickerPacks; - packs.forEach((pack: WhatIsThis) => { + packs.forEach(pack => { const { id, key, isInstall, isRemove } = pack || {}; if (!id || !key || (!isInstall && !isRemove)) { @@ -2572,7 +2667,7 @@ export async function startApp(): Promise { return; } - const status = window.Signal.Stickers.getStickerPackStatus(id); + const status = Stickers.getStickerPackStatus(id); if (status === 'installed' && isRemove) { window.reduxActions.stickers.uninstallStickerPack(id, key, { @@ -2584,7 +2679,7 @@ export async function startApp(): Promise { fromSync: true, }); } else { - window.Signal.Stickers.downloadStickerPack(id, key, { + Stickers.downloadStickerPack(id, key, { finalStatus: 'installed', fromSync: true, }); @@ -2598,7 +2693,7 @@ export async function startApp(): Promise { await window.storage.put('synced_at', Date.now()); } - async function onContactReceived(ev: WhatIsThis) { + async function onContactReceived(ev: ContactEvent) { const details = ev.contactDetails; if ( @@ -2610,20 +2705,20 @@ export async function startApp(): Promise { // special case for syncing details about ourselves if (details.profileKey) { window.log.info('Got sync message with our own profile key'); - ourProfileKeyService.set(details.profileKey); + ourProfileKeyService.set(typedArrayToArrayBuffer(details.profileKey)); } } - const c = new window.Whisper.Conversation({ + const c = new window.Whisper.Conversation(({ e164: details.number, uuid: details.uuid, type: 'private', - } as WhatIsThis); + } as Partial) as WhatIsThis); const validationError = c.validate(); if (validationError) { window.log.error( 'Invalid contact received:', - Errors.toLogFormat(validationError as WhatIsThis) + Errors.toLogFormat(validationError) ); return; } @@ -2638,9 +2733,7 @@ export async function startApp(): Promise { const conversation = window.ConversationController.get(detailsId)!; if (details.profileKey) { - const profileKey = window.Signal.Crypto.arrayBufferToBase64( - details.profileKey - ); + const profileKey = Bytes.toBase64(details.profileKey); conversation.setProfileKey(profileKey); } @@ -2698,14 +2791,18 @@ export async function startApp(): Promise { if (details.verified) { const { verified } = details; - const verifiedEvent = new Event('verified'); - verifiedEvent.verified = { - state: verified.state, - destination: verified.destination, - destinationUuid: verified.destinationUuid, - identityKey: verified.identityKey.toArrayBuffer(), - }; - (verifiedEvent as WhatIsThis).viaContactSync = true; + const verifiedEvent = new VerifiedEvent( + { + state: dropNull(verified.state), + destination: dropNull(verified.destination), + destinationUuid: dropNull(verified.destinationUuid), + identityKey: verified.identityKey + ? typedArrayToArrayBuffer(verified.identityKey) + : undefined, + viaContactSync: true, + }, + noop + ); await onVerified(verifiedEvent); } @@ -2726,11 +2823,11 @@ export async function startApp(): Promise { } // Note: this handler is only for v1 groups received via 'group sync' messages - async function onGroupReceived(ev: WhatIsThis) { + async function onGroupReceived(ev: GroupEvent) { const details = ev.groupDetails; const { id } = details; - const idBuffer = window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(id); + const idBuffer = id; const idBytes = idBuffer.byteLength; if (idBytes !== 16) { window.log.error( @@ -2740,7 +2837,7 @@ export async function startApp(): Promise { } const conversation = await window.ConversationController.getOrCreateAndWait( - id, + Bytes.toBinary(id), 'group' ); if (isGroupV2(conversation.attributes)) { @@ -2751,18 +2848,18 @@ export async function startApp(): Promise { return; } - const memberConversations = details.membersE164.map((e164: WhatIsThis) => + const memberConversations = details.membersE164.map(e164 => window.ConversationController.getOrCreate(e164, 'private') ); - const members = memberConversations.map((c: WhatIsThis) => c.get('id')); + const members = memberConversations.map(c => c.get('id')); - const updates = { + const updates: Partial = { name: details.name, members, type: 'group', inbox_position: details.inboxPosition, - } as WhatIsThis; + }; if (details.active) { updates.left = false; @@ -2823,8 +2920,16 @@ export async function startApp(): Promise { data, confirm, messageDescriptor, - }: WhatIsThis) { - const profileKey = data.message.profileKey.toString('base64'); + }: { + data: MessageEventData; + confirm: () => void; + messageDescriptor: MessageDescriptor; + }) { + const { profileKey } = data.message; + strictAssert( + profileKey !== undefined, + 'handleMessageReceivedProfileUpdate: missing profileKey' + ); const sender = window.ConversationController.get(messageDescriptor.id); if (sender) { @@ -2864,7 +2969,7 @@ export async function startApp(): Promise { // Note: We do very little in this function, since everything in handleDataMessage is // inside a conversation-specific queue(). Any code here might run before an earlier // message is processed in handleDataMessage(). - function onMessageReceived(event: WhatIsThis) { + function onMessageReceived(event: MessageEvent) { const { data, confirm } = event; const messageDescriptor = getMessageDescriptor({ @@ -2903,10 +3008,13 @@ export async function startApp(): Promise { } if (data.message.reaction) { - window.normalizeUuids( - data.message.reaction, - ['targetAuthorUuid'], - 'background::onMessageReceived' + strictAssert( + data.message.reaction.targetAuthorUuid, + 'Reaction without targetAuthorUuid' + ); + const targetAuthorUuid = normalizeUuid( + data.message.reaction.targetAuthorUuid, + 'DataMessage.Reaction.targetAuthorUuid' ); const { reaction } = data.message; @@ -2924,7 +3032,7 @@ export async function startApp(): Promise { const reactionModel = Reactions.getSingleton().add({ emoji: reaction.emoji, remove: reaction.remove, - targetAuthorUuid: reaction.targetAuthorUuid, + targetAuthorUuid, targetTimestamp: reaction.targetTimestamp, timestamp: Date.now(), fromId: window.ConversationController.ensureContactIds({ @@ -2965,7 +3073,7 @@ export async function startApp(): Promise { return Promise.resolve(); } - async function onProfileKeyUpdate({ data, confirm }: WhatIsThis) { + async function onProfileKeyUpdate({ data, confirm }: ProfileKeyUpdateEvent) { const conversationId = window.ConversationController.ensureContactIds({ e164: data.source, uuid: data.sourceUuid, @@ -3007,7 +3115,11 @@ export async function startApp(): Promise { data, confirm, messageDescriptor, - }: WhatIsThis) { + }: { + data: SentEventData; + confirm: () => void; + messageDescriptor: MessageDescriptor; + }) { // First set profileSharing = true for the conversation we sent to const { id } = messageDescriptor; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -3020,7 +3132,11 @@ export async function startApp(): Promise { const ourId = window.ConversationController.getOurConversationId(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const me = window.ConversationController.get(ourId)!; - const profileKey = data.message.profileKey.toString('base64'); + const { profileKey } = data.message; + strictAssert( + profileKey !== undefined, + 'handleMessageSentProfileUpdate: missing profileKey' + ); // Will do the save for us if needed await me.setProfileKey(profileKey); @@ -3028,18 +3144,17 @@ export async function startApp(): Promise { return confirm(); } - function createSentMessage(data: WhatIsThis, descriptor: MessageDescriptor) { + function createSentMessage( + data: SentEventData, + descriptor: MessageDescriptor + ) { const now = Date.now(); const timestamp = data.timestamp || now; - const unidentifiedStatus: Array = Array.isArray( - data.unidentifiedStatus - ) - ? data.unidentifiedStatus - : []; - + const { unidentifiedStatus = [] } = data; let sentTo: Array = []; + let unidentifiedDeliveries: Array = []; if (unidentifiedStatus.length) { sentTo = unidentifiedStatus .map(item => item.destinationUuid || item.destination) @@ -3048,13 +3163,12 @@ export async function startApp(): Promise { const unidentified = window._.filter(data.unidentifiedStatus, item => Boolean(item.unidentified) ); - // eslint-disable-next-line no-param-reassign - data.unidentifiedDeliveries = unidentified.map( - item => item.destinationUuid || item.destination - ); + unidentifiedDeliveries = unidentified + .map(item => item.destinationUuid || item.destination) + .filter(isNotNil); } - return new window.Whisper.Message({ + return new window.Whisper.Message(({ source: window.textsecure.storage.user.getNumber(), sourceUuid: window.textsecure.storage.user.getUuid(), sourceDevice: data.device, @@ -3067,12 +3181,12 @@ export async function startApp(): Promise { timestamp, type: 'outgoing', sent: true, - unidentifiedDeliveries: data.unidentifiedDeliveries || [], + unidentifiedDeliveries, expirationStartTimestamp: Math.min( data.expirationStartTimestamp || timestamp, now ), - } as WhatIsThis); + } as Partial) as WhatIsThis); } // Works with 'sent' and 'message' data sent from MessageReceiver, with a little massage @@ -3084,11 +3198,11 @@ export async function startApp(): Promise { destination, destinationUuid, }: { - message: DataMessageClass; - source: string; - sourceUuid: string; - destination: string; - destinationUuid: string; + message: ProcessedDataMessage; + source?: string; + sourceUuid?: string; + destination?: string; + destinationUuid?: string; }): MessageDescriptor => { if (message.groupV2) { const { id } = message.groupV2; @@ -3186,14 +3300,19 @@ export async function startApp(): Promise { // Note: We do very little in this function, since everything in handleDataMessage is // inside a conversation-specific queue(). Any code here might run before an earlier // message is processed in handleDataMessage(). - function onSentMessage(event: WhatIsThis) { + function onSentMessage(event: SentEvent) { const { data, confirm } = event; + const source = window.textsecure.storage.user.getNumber(); + const sourceUuid = window.textsecure.storage.user.getUuid(); + strictAssert(source && sourceUuid, 'Missing user number and uuid'); + const messageDescriptor = getMessageDescriptor({ ...data, + // 'sent' event: the sender is always us! - source: window.textsecure.storage.user.getNumber(), - sourceUuid: window.textsecure.storage.user.getUuid(), + source, + sourceUuid, }); const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags; @@ -3210,10 +3329,13 @@ export async function startApp(): Promise { const message = createSentMessage(data, messageDescriptor); if (data.message.reaction) { - window.normalizeUuids( - data.message.reaction, - ['targetAuthorUuid'], - 'background::onSentMessage' + strictAssert( + data.message.reaction.targetAuthorUuid, + 'Reaction without targetAuthorUuid' + ); + const targetAuthorUuid = normalizeUuid( + data.message.reaction.targetAuthorUuid, + 'DataMessage.Reaction.targetAuthorUuid' ); const { reaction } = data.message; @@ -3228,7 +3350,7 @@ export async function startApp(): Promise { const reactionModel = Reactions.getSingleton().add({ emoji: reaction.emoji, remove: reaction.remove, - targetAuthorUuid: reaction.targetAuthorUuid, + targetAuthorUuid, targetTimestamp: reaction.targetTimestamp, timestamp: Date.now(), fromId: window.ConversationController.getOurConversationId(), @@ -3246,7 +3368,7 @@ export async function startApp(): Promise { window.log.info('Queuing sent DOE for', del.targetSentTimestamp); const deleteModel = Deletes.getSingleton().add({ targetSentTimestamp: del.targetSentTimestamp, - serverTimestamp: del.serverTimestamp, + serverTimestamp: data.serverTimestamp, fromId: window.ConversationController.getOurConversationId(), }); // Note: We do not wait for completion here @@ -3274,14 +3396,14 @@ export async function startApp(): Promise { }; function initIncomingMessage( - data: WhatIsThis, + data: MessageEventData, descriptor: MessageDescriptor ) { assert( Boolean(data.receivedAtCounter), `Did not receive receivedAtCounter for message: ${data.timestamp}` ); - return new window.Whisper.Message({ + return new window.Whisper.Message(({ source: data.source, sourceUuid: data.sourceUuid, sourceDevice: data.sourceDevice, @@ -3293,13 +3415,14 @@ export async function startApp(): Promise { conversationId: descriptor.id, unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, type: 'incoming', - unread: 1, - } as WhatIsThis); + unread: true, + timestamp: data.timestamp, + } as Partial) as WhatIsThis); } // Returns `false` if this message isn't a group call message. function handleGroupCallUpdateMessage( - message: DataMessageClass, + message: ProcessedDataMessage, messageDescriptor: MessageDescriptor ): boolean { if (message.groupCallUpdate) { @@ -3329,7 +3452,7 @@ export async function startApp(): Promise { if (messageReceiver) { messageReceiver.unregisterBatchers(); - messageReceiver = null; + messageReceiver = undefined; } onEmpty(); @@ -3399,7 +3522,7 @@ export async function startApp(): Promise { } } - function onError(ev: WhatIsThis) { + function onError(ev: ErrorEvent) { const { error } = ev; window.log.error('background onError:', Errors.toLogFormat(error)); @@ -3436,10 +3559,6 @@ export async function startApp(): Promise { window.log.warn('background onError: Doing nothing with incoming error'); } - type RetryRequestEventType = Event & { - retryRequest: RetryRequestType; - }; - function isInList( conversation: ConversationModel, list: Array | undefined @@ -3471,7 +3590,7 @@ export async function startApp(): Promise { requesterUuid, requesterDevice, senderDevice, - }: RetryRequestType): Promise { + }: RetryRequestEventData): Promise { const ourDeviceId = parseIntOrThrow( window.textsecure.storage.user.getDeviceId(), 'archiveSessionOnMatch/getDeviceId' @@ -3486,7 +3605,7 @@ export async function startApp(): Promise { } async function sendDistributionMessageOrNullMessage( - options: RetryRequestType + options: RetryRequestEventData ): Promise { const { groupId, requesterUuid } = options; let sentDistributionMessage = false; @@ -3558,7 +3677,7 @@ export async function startApp(): Promise { } } - async function onRetryRequest(event: RetryRequestEventType) { + async function onRetryRequest(event: RetryRequestEvent) { const { retryRequest } = event; const { requesterDevice, @@ -3637,11 +3756,7 @@ export async function startApp(): Promise { await targetMessage.resend(requesterUuid); } - type DecryptionErrorEventType = Event & { - decryptionError: DecryptionErrorType; - }; - - async function onDecryptionError(event: DecryptionErrorEventType) { + async function onDecryptionError(event: DecryptionErrorEvent) { const { decryptionError } = event; const { senderUuid, senderDevice, timestamp } = decryptionError; const logId = `${senderUuid}.${senderDevice} ${timestamp}`; @@ -3666,7 +3781,7 @@ export async function startApp(): Promise { window.log.info(`onDecryptionError/${logId}: ...complete`); } - async function requestResend(decryptionError: DecryptionErrorType) { + async function requestResend(decryptionError: DecryptionErrorEventData) { const { cipherTextBytes, cipherTextType, @@ -3784,7 +3899,9 @@ export async function startApp(): Promise { }); } - function startAutomaticSessionReset(decryptionError: DecryptionErrorType) { + function startAutomaticSessionReset( + decryptionError: DecryptionErrorEventData + ) { const { senderUuid, senderDevice, timestamp } = decryptionError; const logId = `${senderUuid}.${senderDevice} ${timestamp}`; @@ -3818,7 +3935,7 @@ export async function startApp(): Promise { }); } - async function onViewSync(ev: WhatIsThis) { + async function onViewSync(ev: ViewSyncEvent) { ev.confirm(); const { source, sourceUuid, timestamp } = ev; @@ -3833,7 +3950,7 @@ export async function startApp(): Promise { ViewSyncs.getSingleton().onSync(sync); } - async function onFetchLatestSync(ev: WhatIsThis) { + async function onFetchLatestSync(ev: FetchLatestEvent) { ev.confirm(); const { eventType } = ev; @@ -3856,7 +3973,7 @@ export async function startApp(): Promise { } } - async function onKeysSync(ev: WhatIsThis) { + async function onKeysSync(ev: KeysEvent) { ev.confirm(); const { storageServiceKey } = ev; @@ -3877,7 +3994,7 @@ export async function startApp(): Promise { } } - async function onMessageRequestResponse(ev: WhatIsThis) { + async function onMessageRequestResponse(ev: MessageRequestResponseEvent) { ev.confirm(); const { @@ -3907,9 +4024,9 @@ export async function startApp(): Promise { MessageRequests.getSingleton().onResponse(sync); } - function onReadReceipt(ev: WhatIsThis) { - const readAt = ev.timestamp; + function onReadReceipt(ev: ReadEvent) { const { envelopeTimestamp, timestamp, source, sourceUuid } = ev.read; + const readAt = envelopeTimestamp; const reader = window.ConversationController.ensureContactIds({ e164: source, uuid: sourceUuid, @@ -3941,9 +4058,9 @@ export async function startApp(): Promise { ReadReceipts.getSingleton().onReceipt(receipt); } - function onReadSync(ev: WhatIsThis) { - const readAt = ev.timestamp; + function onReadSync(ev: ReadSyncEvent) { const { envelopeTimestamp, sender, senderUuid, timestamp } = ev.read; + const readAt = envelopeTimestamp; const senderId = window.ConversationController.ensureContactIds({ e164: sender, uuid: senderUuid, @@ -3974,7 +4091,7 @@ export async function startApp(): Promise { return ReadSyncs.getSingleton().onReceipt(receipt); } - async function onVerified(ev: WhatIsThis) { + async function onVerified(ev: VerifiedEvent) { const e164 = ev.verified.destination; const uuid = ev.verified.destinationUuid; const key = ev.verified.identityKey; @@ -3984,18 +4101,18 @@ export async function startApp(): Promise { ev.confirm(); } - const c = new window.Whisper.Conversation({ + const c = new window.Whisper.Conversation(({ e164, uuid, type: 'private', - } as WhatIsThis); + } as Partial) as WhatIsThis); const error = c.validate(); if (error) { window.log.error( 'Invalid verified sync received:', e164, uuid, - Errors.toLogFormat(error as WhatIsThis) + Errors.toLogFormat(error) ); return; } @@ -4019,7 +4136,7 @@ export async function startApp(): Promise { e164, uuid, state, - ev.viaContactSync ? 'via contact sync' : '' + ev.verified.viaContactSync ? 'via contact sync' : '' ); const verifiedId = window.ConversationController.ensureContactIds({ @@ -4031,7 +4148,7 @@ export async function startApp(): Promise { const contact = window.ConversationController.get(verifiedId)!; const options = { viaSyncMessage: true, - viaContactSync: ev.viaContactSync, + viaContactSync: ev.verified.viaContactSync, key, }; @@ -4044,7 +4161,7 @@ export async function startApp(): Promise { } } - function onDeliveryReceipt(ev: WhatIsThis) { + function onDeliveryReceipt(ev: DeliveryEvent) { const { deliveryReceipt } = ev; const { envelopeTimestamp, diff --git a/ts/components/conversation/GroupV2Change.stories.tsx b/ts/components/conversation/GroupV2Change.stories.tsx index 8b5b5e660..f8e7ef0fb 100644 --- a/ts/components/conversation/GroupV2Change.stories.tsx +++ b/ts/components/conversation/GroupV2Change.stories.tsx @@ -8,6 +8,7 @@ import { storiesOf } from '@storybook/react'; import { setup as setupI18n } from '../../../js/modules/i18n'; import enMessages from '../../../_locales/en/messages.json'; import { GroupV2ChangeType } from '../../groups'; +import { SignalService as Proto } from '../../protobuf'; import { SmartContactRendererType } from '../../groupChange'; import { GroupV2Change } from './GroupV2Change'; @@ -20,25 +21,8 @@ const CONTACT_C = 'CONTACT_C'; const ADMIN_A = 'ADMIN_A'; const INVITEE_A = 'INVITEE_A'; -class AccessControlEnum { - static UNKNOWN = 0; - - static ANY = 1; - - static MEMBER = 2; - - static ADMINISTRATOR = 3; - - static UNSATISFIABLE = 4; -} - -class RoleEnum { - static UNKNOWN = 0; - - static ADMINISTRATOR = 1; - - static DEFAULT = 2; -} +const AccessControlEnum = Proto.AccessControl.AccessRequired; +const RoleEnum = Proto.Member.Role; const renderContact: SmartContactRendererType = (conversationId: string) => ( @@ -48,13 +32,11 @@ const renderContact: SmartContactRendererType = (conversationId: string) => ( const renderChange = (change: GroupV2ChangeType, groupName?: string) => ( ); diff --git a/ts/components/conversation/GroupV2Change.tsx b/ts/components/conversation/GroupV2Change.tsx index c8b6dad97..152bcc47e 100644 --- a/ts/components/conversation/GroupV2Change.tsx +++ b/ts/components/conversation/GroupV2Change.tsx @@ -14,14 +14,10 @@ import { GroupV2ChangeType, GroupV2DescriptionChangeType } from '../../groups'; import { renderChange, SmartContactRendererType } from '../../groupChange'; import { Modal } from '../Modal'; -import { AccessControlClass, MemberClass } from '../../textsecure.d'; - export type PropsDataType = { groupName?: string; ourConversationId: string; change: GroupV2ChangeType; - AccessControlEnum: typeof AccessControlClass.AccessRequired; - RoleEnum: typeof MemberClass.Role; }; export type PropsHousekeepingType = { @@ -40,15 +36,7 @@ function renderStringToIntl( } export function GroupV2Change(props: PropsType): ReactElement { - const { - AccessControlEnum, - change, - groupName, - i18n, - ourConversationId, - renderContact, - RoleEnum, - } = props; + const { change, groupName, i18n, ourConversationId, renderContact } = props; const [ isGroupDescriptionDialogOpen, @@ -64,12 +52,10 @@ export function GroupV2Change(props: PropsType): ReactElement {
{renderChange(change, { - AccessControlEnum, i18n, ourConversationId, renderContact, renderString: renderStringToIntl, - RoleEnum, }).map((item: FullJSXType, index: number) => ( // Difficult to find a unique key for this type // eslint-disable-next-line react/no-array-index-key diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx index bcf4abb60..ef5fae2b1 100644 --- a/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx +++ b/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx @@ -9,6 +9,7 @@ import { action } from '@storybook/addon-actions'; import { setup as setupI18n } from '../../../../js/modules/i18n'; import enMessages from '../../../../_locales/en/messages.json'; import { GroupLinkManagement, PropsType } from './GroupLinkManagement'; +import { SignalService as Proto } from '../../../protobuf'; import { ConversationType } from '../../../state/ducks/conversations'; import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; @@ -19,17 +20,7 @@ const story = storiesOf( module ); -class AccessEnum { - static ANY = 0; - - static UNKNOWN = 1; - - static MEMBER = 2; - - static ADMINISTRATOR = 3; - - static UNSATISFIABLE = 4; -} +const AccessControlEnum = Proto.AccessControl.AccessRequired; function getConversation( groupLink?: string, @@ -47,7 +38,7 @@ function getConversation( accessControlAddFromInviteLink: accessControlAddFromInviteLink !== undefined ? accessControlAddFromInviteLink - : AccessEnum.UNSATISFIABLE, + : AccessControlEnum.UNSATISFIABLE, }); } @@ -55,7 +46,6 @@ const createProps = ( conversation?: ConversationType, isAdmin = false ): PropsType => ({ - accessEnum: AccessEnum, changeHasGroupLink: action('changeHasGroupLink'), conversation: conversation || getConversation(), copyGroupLink: action('copyGroupLink'), @@ -75,7 +65,7 @@ story.add('Off (Admin)', () => { story.add('On (Admin)', () => { const props = createProps( - getConversation('https://signal.group/1', AccessEnum.ANY), + getConversation('https://signal.group/1', AccessControlEnum.ANY), true ); @@ -84,7 +74,7 @@ story.add('On (Admin)', () => { story.add('On (Admin + Admin Approval Needed)', () => { const props = createProps( - getConversation('https://signal.group/1', AccessEnum.ADMINISTRATOR), + getConversation('https://signal.group/1', AccessControlEnum.ADMINISTRATOR), true ); @@ -93,7 +83,7 @@ story.add('On (Admin + Admin Approval Needed)', () => { story.add('On (Non-admin)', () => { const props = createProps( - getConversation('https://signal.group/1', AccessEnum.ANY) + getConversation('https://signal.group/1', AccessControlEnum.ANY) ); return ; diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx index 44d9620ce..e8a82ea3a 100644 --- a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx +++ b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx @@ -4,15 +4,16 @@ import React from 'react'; import { ConversationDetailsIcon } from './ConversationDetailsIcon'; +import { SignalService as Proto } from '../../../protobuf'; import { ConversationType } from '../../../state/ducks/conversations'; import { LocalizerType } from '../../../types/Util'; import { PanelRow } from './PanelRow'; import { PanelSection } from './PanelSection'; -import { AccessControlClass } from '../../../textsecure.d'; import { Select } from '../../Select'; +const AccessControlEnum = Proto.AccessControl.AccessRequired; + export type PropsType = { - accessEnum: typeof AccessControlClass.AccessRequired; changeHasGroupLink: (value: boolean) => void; conversation?: ConversationType; copyGroupLink: (groupLink: string) => void; @@ -23,7 +24,6 @@ export type PropsType = { }; export const GroupLinkManagement: React.ComponentType = ({ - accessEnum, changeHasGroupLink, conversation, copyGroupLink, @@ -43,11 +43,13 @@ export const GroupLinkManagement: React.ComponentType = ({ }; const membersNeedAdminApproval = - conversation.accessControlAddFromInviteLink === accessEnum.ADMINISTRATOR; + conversation.accessControlAddFromInviteLink === + AccessControlEnum.ADMINISTRATOR; const hasGroupLink = conversation.groupLink && - conversation.accessControlAddFromInviteLink !== accessEnum.UNSATISFIABLE; + conversation.accessControlAddFromInviteLink !== + AccessControlEnum.UNSATISFIABLE; const groupLinkInfo = hasGroupLink ? conversation.groupLink : ''; return ( diff --git a/ts/components/conversation/conversation-details/GroupV2Permissions.stories.tsx b/ts/components/conversation/conversation-details/GroupV2Permissions.stories.tsx index 48be2dd55..5aa7ac7d6 100644 --- a/ts/components/conversation/conversation-details/GroupV2Permissions.stories.tsx +++ b/ts/components/conversation/conversation-details/GroupV2Permissions.stories.tsx @@ -29,20 +29,7 @@ const conversation: ConversationType = getDefaultConversation({ sharedGroupNames: [], }); -class AccessEnum { - static ANY = 0; - - static UNKNOWN = 1; - - static MEMBER = 2; - - static ADMINISTRATOR = 3; - - static UNSATISFIABLE = 4; -} - const createProps = (): PropsType => ({ - accessEnum: AccessEnum, conversation, i18n, setAccessControlAttributesSetting: action( diff --git a/ts/components/conversation/conversation-details/GroupV2Permissions.tsx b/ts/components/conversation/conversation-details/GroupV2Permissions.tsx index 3d7a38ec8..417989b2f 100644 --- a/ts/components/conversation/conversation-details/GroupV2Permissions.tsx +++ b/ts/components/conversation/conversation-details/GroupV2Permissions.tsx @@ -6,14 +6,12 @@ import React from 'react'; import { ConversationType } from '../../../state/ducks/conversations'; import { LocalizerType } from '../../../types/Util'; import { getAccessControlOptions } from '../../../util/getAccessControlOptions'; -import { AccessControlClass } from '../../../textsecure.d'; import { PanelRow } from './PanelRow'; import { PanelSection } from './PanelSection'; import { Select } from '../../Select'; export type PropsType = { - accessEnum: typeof AccessControlClass.AccessRequired; conversation?: ConversationType; i18n: LocalizerType; setAccessControlAttributesSetting: (value: number) => void; @@ -21,7 +19,6 @@ export type PropsType = { }; export const GroupV2Permissions: React.ComponentType = ({ - accessEnum, conversation, i18n, setAccessControlAttributesSetting, @@ -37,7 +34,7 @@ export const GroupV2Permissions: React.ComponentType = ({ const updateAccessControlMembers = (value: string) => { setAccessControlMembersSetting(Number(value)); }; - const accessControlOptions = getAccessControlOptions(accessEnum, i18n); + const accessControlOptions = getAccessControlOptions(i18n); return ( diff --git a/ts/groupChange.ts b/ts/groupChange.ts index fe214db57..ee7a86726 100644 --- a/ts/groupChange.ts +++ b/ts/groupChange.ts @@ -6,8 +6,8 @@ import { LocalizerType } from './types/Util'; import { ReplacementValuesType } from './types/I18N'; import { missingCaseError } from './util/missingCaseError'; -import { AccessControlClass, MemberClass } from './textsecure.d'; import { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups'; +import { SignalService as Proto } from './protobuf'; export type SmartContactRendererType = (conversationId: string) => FullJSXType; export type StringRendererType = ( @@ -17,15 +17,16 @@ export type StringRendererType = ( ) => FullJSXType; export type RenderOptionsType = { - AccessControlEnum: typeof AccessControlClass.AccessRequired; from?: string; i18n: LocalizerType; ourConversationId: string; renderContact: SmartContactRendererType; renderString: StringRendererType; - RoleEnum: typeof MemberClass.Role; }; +const AccessControlEnum = Proto.AccessControl.AccessRequired; +const RoleEnum = Proto.Member.Role; + export function renderChange( change: GroupV2ChangeType, options: RenderOptionsType @@ -45,13 +46,11 @@ export function renderChangeDetail( options: RenderOptionsType ): FullJSXType { const { - AccessControlEnum, from, i18n, ourConversationId, renderContact, renderString, - RoleEnum, } = options; const fromYou = Boolean(from && from === ourConversationId); diff --git a/ts/groups.ts b/ts/groups.ts index ace72e65d..4f0236caa 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -1370,6 +1370,13 @@ export function idForLogging(groupId: string | undefined): string { } export function deriveGroupFields(masterKey: Uint8Array): GroupFields { + if (masterKey.length !== MASTER_KEY_LENGTH) { + throw new Error( + `deriveGroupFields: masterKey had length ${masterKey.length}, ` + + `expected ${MASTER_KEY_LENGTH}` + ); + } + const cacheKey = Bytes.toBase64(masterKey); const cached = groupFieldsCache.get(cacheKey); if (cached) { diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index fd4b3032f..560d99e74 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -58,7 +58,7 @@ export type QuotedMessageType = { author?: string; authorUuid?: string; bodyRanges?: BodyRangesType; - id: string; + id: number; referencedMessageNotFound: boolean; isViewOnce: boolean; text?: string; @@ -190,6 +190,8 @@ export type MessageAttributesType = { // Backwards-compatibility with prerelease data schema invitedGV2Members?: Array; droppedGV2MemberIds?: Array; + + sendHQImages?: boolean; }; export type ConversationAttributesTypeType = 'private' | 'group'; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index ecbe3071b..0b5141fa5 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -14,7 +14,9 @@ import { VerificationOptions, WhatIsThis, } from '../model-types.d'; +import { AttachmentType } from '../types/Attachment'; import { CallMode, CallHistoryDetailsType } from '../types/Calling'; +import * as Stickers from '../types/Stickers'; import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage'; import { ConversationType } from '../state/ducks/conversations'; import { @@ -3100,7 +3102,7 @@ export class ConversationModel extends window.Backbone ? [{ contentType: 'image/jpeg', fileName: null }] : await this.getQuoteAttachment(attachments, preview, sticker), bodyRanges: quotedMessage.get('bodyRanges'), - id: String(quotedMessage.get('sent_at')), + id: quotedMessage.get('sent_at'), isViewOnce: isTapToView(quotedMessage.attributes), messageId: quotedMessage.get('id'), referencedMessageNotFound: false, @@ -3109,8 +3111,8 @@ export class ConversationModel extends window.Backbone } async sendStickerMessage(packId: string, stickerId: number): Promise { - const packData = window.Signal.Stickers.getStickerPack(packId); - const stickerData = window.Signal.Stickers.getSticker(packId, stickerId); + const packData = Stickers.getStickerPack(packId); + const stickerData = Stickers.getSticker(packId, stickerId); if (!stickerData || !packData) { window.log.warn( `Attempted to send nonexistent (${packId}, ${stickerId}) sticker!` @@ -3152,7 +3154,7 @@ export class ConversationModel extends window.Backbone }, }; - this.sendMessage(null, [], null, [], sticker); + this.sendMessage(undefined, [], undefined, [], sticker); window.reduxActions.stickers.useSticker(packId, stickerId); } @@ -3451,10 +3453,10 @@ export class ConversationModel extends window.Backbone } sendMessage( - body: string | null, - attachments: Array, - quote: WhatIsThis, - preview: WhatIsThis, + body: string | undefined, + attachments: Array, + quote?: QuotedMessageType, + preview?: WhatIsThis, sticker?: WhatIsThis, mentions?: BodyRangesType, { @@ -3503,6 +3505,7 @@ export class ConversationModel extends window.Backbone // Here we move attachments to disk const messageWithSchema = await upgradeMessageSchema({ + timestamp: now, type: 'outgoing', body, conversationId: this.id, @@ -3575,7 +3578,7 @@ export class ConversationModel extends window.Backbone } const attachmentsWithData = await Promise.all( - messageWithSchema.attachments.map(loadAttachmentData) + messageWithSchema.attachments?.map(loadAttachmentData) ?? [] ); const { @@ -5086,7 +5089,7 @@ export class ConversationModel extends window.Backbone isTyping: boolean; senderId: string; fromMe: boolean; - senderDevice: string; + senderDevice: number; }): void { const { isTyping, senderId, fromMe, senderDevice } = options; diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 26e1b8e14..58263036d 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -12,9 +12,10 @@ import { QuotedMessageType, WhatIsThis, } from '../model-types.d'; +import { strictAssert } from '../util/assert'; +import { dropNull } from '../util/dropNull'; import { map, filter, find } from '../util/iterables'; import { isNotNil } from '../util/isNotNil'; -import { DataMessageClass } from '../textsecure.d'; import { ConversationModel } from './conversations'; import { MessageStatusType } from '../components/conversation/Message'; import { @@ -23,9 +24,17 @@ import { } from '../state/smart/MessageDetail'; import { getCallingNotificationText } from '../util/callingNotification'; import { CallbackResultType } from '../textsecure/SendMessage'; +import { ProcessedDataMessage, ProcessedQuote } from '../textsecure/Types.d'; import * as expirationTimer from '../util/expirationTimer'; import { ReactionType } from '../types/Reactions'; +import { + copyStickerToAttachments, + deletePackReference, + savePackMetadata, + getStickerPackStatus, +} from '../types/Stickers'; +import * as Stickers from '../types/Stickers'; import { AttachmentType, isImage, isVideo } from '../types/Attachment'; import { MIMEType, IMAGE_WEBP } from '../types/MIME'; import { ourProfileKeyService } from '../services/ourProfileKey'; @@ -107,12 +116,6 @@ const { loadStickerData, upgradeMessageSchema, } = window.Signal.Migrations; -const { - copyStickerToAttachments, - deletePackReference, - savePackMetadata, - getStickerPackStatus, -} = window.Signal.Stickers; const { getTextWithMentions, GoogleChrome } = window.Signal.Util; const { addStickerPackReference, getMessageBySender } = window.Signal.Data; @@ -124,7 +127,7 @@ const includesAny = (haystack: Array, ...needles: Array) => export function isQuoteAMatch( message: MessageModel | null | undefined, conversationId: string, - quote: QuotedMessageType | DataMessageClass.Quote + quote: QuotedMessageType ): message is MessageModel { if (!message) { return false; @@ -614,7 +617,7 @@ export class MessageModel extends window.Backbone.Model { const stickerData = this.get('sticker'); if (stickerData) { - const sticker = window.Signal.Stickers.getSticker( + const sticker = Stickers.getSticker( stickerData.packId, stickerData.stickerId ); @@ -624,7 +627,7 @@ export class MessageModel extends window.Backbone.Model { } return { text: window.i18n('message--getNotificationText--stickers'), - emoji, + emoji: dropNull(emoji), }; } @@ -1460,11 +1463,7 @@ export class MessageModel extends window.Backbone.Model { senderKeyInfo.distributionId ); - contentMessage.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap( - window.Signal.Crypto.typedArrayToArrayBuffer( - senderKeyDistributionMessage.serialize() - ) - ); + contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize(); } } @@ -2217,18 +2216,48 @@ export class MessageModel extends window.Backbone.Model { // eslint-disable-next-line class-methods-use-this async copyFromQuotedMessage( - message: DataMessageClass, + quote: ProcessedQuote | undefined, conversationId: string - ): Promise { - const { quote } = message; + ): Promise { if (!quote) { - return message; + return undefined; } const { id } = quote; + strictAssert(id, 'Quote must have an id'); + + const result: QuotedMessageType = { + ...quote, + + id, + + attachments: quote.attachments.slice(), + bodyRanges: quote.bodyRanges.map(({ start, length, mentionUuid }) => { + strictAssert( + start !== undefined && start !== null, + 'Received quote with a bodyRange.start == null' + ); + strictAssert( + length !== undefined && length !== null, + 'Received quote with a bodyRange.length == null' + ); + + return { + start, + length, + mentionUuid: dropNull(mentionUuid), + }; + }), + + // Just placeholder values for the fields + referencedMessageNotFound: false, + isViewOnce: false, + messageId: '', + }; + const inMemoryMessages = window.MessageController.filterBySentAt(id); const matchingMessage = find(inMemoryMessages, item => - isQuoteAMatch(item, conversationId, quote) + isQuoteAMatch(item, conversationId, result) ); let queryMessage: undefined | MessageModel; @@ -2241,35 +2270,35 @@ export class MessageModel extends window.Backbone.Model { MessageCollection: window.Whisper.MessageCollection, }); const found = collection.find(item => - isQuoteAMatch(item, conversationId, quote) + isQuoteAMatch(item, conversationId, result) ); if (!found) { - quote.referencedMessageNotFound = true; - return message; + result.referencedMessageNotFound = true; + return result; } queryMessage = window.MessageController.register(found.id, found); } if (queryMessage) { - await this.copyQuoteContentFromOriginal(queryMessage, quote); + await this.copyQuoteContentFromOriginal(queryMessage, result); } - return message; + return result; } // eslint-disable-next-line class-methods-use-this async copyQuoteContentFromOriginal( originalMessage: MessageModel, - quote: QuotedMessageType | DataMessageClass.Quote + quote: QuotedMessageType ): Promise { const { attachments } = quote; const firstAttachment = attachments ? attachments[0] : undefined; if (isTapToView(originalMessage.attributes)) { // eslint-disable-next-line no-param-reassign - quote.text = null; + quote.text = undefined; // eslint-disable-next-line no-param-reassign quote.attachments = [ { @@ -2362,7 +2391,7 @@ export class MessageModel extends window.Backbone.Model { } handleDataMessage( - initialMessage: DataMessageClass, + initialMessage: ProcessedDataMessage, confirm: () => void, options: { data?: typeof window.WhatIsThis } = {} ): WhatIsThis { @@ -2631,16 +2660,19 @@ export class MessageModel extends window.Backbone.Model { }); } - const withQuoteReference = await this.copyFromQuotedMessage( - initialMessage, - conversation.id - ); + const withQuoteReference = { + ...initialMessage, + quote: await this.copyFromQuotedMessage( + initialMessage.quote, + conversation.id + ), + }; const dataMessage = await upgradeMessageSchema(withQuoteReference); try { const now = new Date().getTime(); - const urls = LinkPreview.findLinks(dataMessage.body); + const urls = LinkPreview.findLinks(dataMessage.body || ''); const incomingPreview = dataMessage.preview || []; const preview = incomingPreview.filter( (item: typeof window.WhatIsThis) => diff --git a/ts/services/calling.ts b/ts/services/calling.ts index 91f842d29..275fe2ce0 100644 --- a/ts/services/calling.ts +++ b/ts/services/calling.ts @@ -22,7 +22,6 @@ import { GumVideoCapturer, HangupMessage, HangupType, - OfferType, OpaqueMessage, PeekInfo, RingRTC, @@ -38,7 +37,6 @@ import { GroupCallPeekInfoType, } from '../state/ducks/calling'; import { getConversationCallMode } from '../state/ducks/conversations'; -import { EnvelopeClass } from '../textsecure.d'; import { CallMode, AudioDevice, @@ -57,12 +55,14 @@ import { typedArrayToArrayBuffer, } from '../Crypto'; import { assert } from '../util/assert'; +import { dropNull, shallowDropNull } from '../util/dropNull'; import { getOwn } from '../util/getOwn'; import { fetchMembershipProof, getMembershipList, wrapWithSyncMessageSend, } from '../groups'; +import { ProcessedEnvelope } from '../textsecure/Types.d'; import { missingCaseError } from '../util/missingCaseError'; import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp'; import { @@ -74,6 +74,9 @@ import { notify } from './notify'; import { getSendOptions } from '../util/getSendOptions'; import { SignalService as Proto } from '../protobuf'; +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + const RINGRTC_HTTP_METHOD_TO_OUR_HTTP_METHOD: Map< HttpMethod, 'GET' | 'PUT' | 'POST' | 'DELETE' @@ -121,6 +124,135 @@ function translateSourceName( return name; } +function protoToCallingMessage({ + offer, + answer, + iceCandidates, + legacyHangup, + busy, + hangup, + supportsMultiRing, + destinationDeviceId, + opaque, +}: Proto.ICallingMessage): CallingMessage { + return { + offer: offer + ? { + ...shallowDropNull(offer), + + type: dropNull(offer.type) as number, + opaque: offer.opaque ? Buffer.from(offer.opaque) : undefined, + } + : undefined, + answer: answer + ? { + ...shallowDropNull(answer), + opaque: answer.opaque ? Buffer.from(answer.opaque) : undefined, + } + : undefined, + iceCandidates: iceCandidates + ? iceCandidates.map(candidate => { + return { + ...shallowDropNull(candidate), + opaque: candidate.opaque + ? Buffer.from(candidate.opaque) + : undefined, + }; + }) + : undefined, + legacyHangup: legacyHangup + ? { + ...shallowDropNull(legacyHangup), + type: dropNull(legacyHangup.type) as number, + } + : undefined, + busy: shallowDropNull(busy), + hangup: hangup + ? { + ...shallowDropNull(hangup), + type: dropNull(hangup.type) as number, + } + : undefined, + supportsMultiRing: dropNull(supportsMultiRing), + destinationDeviceId: dropNull(destinationDeviceId), + opaque: opaque + ? { + data: opaque.data ? Buffer.from(opaque.data) : undefined, + } + : undefined, + }; +} + +function bufferToProto( + value: Buffer | { toArrayBuffer(): ArrayBuffer } | undefined +): Uint8Array | undefined { + if (!value) { + return undefined; + } + if (value instanceof Uint8Array) { + return value; + } + + return new FIXMEU8(value.toArrayBuffer()); +} + +function callingMessageToProto({ + offer, + answer, + iceCandidates, + legacyHangup, + busy, + hangup, + supportsMultiRing, + destinationDeviceId, + opaque, +}: CallingMessage): Proto.ICallingMessage { + return { + offer: offer + ? { + ...offer, + type: offer.type as number, + opaque: bufferToProto(offer.opaque), + } + : undefined, + answer: answer + ? { + ...answer, + opaque: bufferToProto(answer.opaque), + } + : undefined, + iceCandidates: iceCandidates + ? iceCandidates.map(candidate => { + return { + ...candidate, + opaque: bufferToProto(candidate.opaque), + }; + }) + : undefined, + legacyHangup: legacyHangup + ? { + ...legacyHangup, + type: legacyHangup.type as number, + } + : undefined, + busy, + hangup: hangup + ? { + ...hangup, + type: hangup.type as number, + } + : undefined, + supportsMultiRing, + destinationDeviceId, + opaque: opaque + ? { + ...opaque, + data: bufferToProto(opaque.data), + } + : undefined, + }; +} + export class CallingClass { readonly videoCapturer: GumVideoCapturer; @@ -1231,8 +1363,8 @@ export class CallingClass { } async handleCallingMessage( - envelope: EnvelopeClass, - callingMessage: CallingMessage + envelope: ProcessedEnvelope, + callingMessage: Proto.ICallingMessage ): Promise { window.log.info('CallingClass.handleCallingMessage()'); @@ -1298,9 +1430,10 @@ export class CallingClass { await this.handleOutgoingSignaling(remoteUserId, message); + const ProtoOfferType = Proto.CallingMessage.Offer.Type; this.addCallHistoryForFailedIncomingCall( conversation, - callingMessage.offer.type === OfferType.VideoCall, + callingMessage.offer.type === ProtoOfferType.OFFER_VIDEO_CALL, envelope.timestamp ); @@ -1321,7 +1454,7 @@ export class CallingClass { remoteDeviceId, this.localDeviceId, messageAgeSec, - callingMessage, + protoToCallingMessage(callingMessage), Buffer.from(senderIdentityKey), Buffer.from(receiverIdentityKey) ); @@ -1428,7 +1561,7 @@ export class CallingClass { try { await window.textsecure.messaging.sendCallingMessage( remoteUserId, - message, + callingMessageToProto(message), sendOptions ); diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index b0578b9ab..f72bb3512 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -24,6 +24,7 @@ import { waitThenRespondToGroupV2Migration, } from '../groups'; import { assert } from '../util/assert'; +import { normalizeUuid } from '../util/normalizeUuid'; import { missingCaseError } from '../util/missingCaseError'; import { PhoneNumberSharingMode, @@ -719,13 +720,18 @@ export async function mergeGroupV2Record( export async function mergeContactRecord( storageID: string, - contactRecord: ContactRecordClass + originalContactRecord: ContactRecordClass ): Promise { - window.normalizeUuids( - contactRecord, - ['serviceUuid'], - 'storageService.mergeContactRecord' - ); + const contactRecord = { + ...originalContactRecord, + + serviceUuid: originalContactRecord.serviceUuid + ? normalizeUuid( + originalContactRecord.serviceUuid, + 'ContactRecord.serviceUuid' + ) + : undefined, + }; const e164 = contactRecord.serviceE164 || undefined; const uuid = contactRecord.serviceUuid || undefined; diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 0bf60674c..c8398a790 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -4,19 +4,19 @@ /* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable camelcase */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { +import type { ConversationAttributesType, ConversationModelCollectionType, MessageAttributesType, MessageModelCollectionType, } from '../model-types.d'; -import { MessageModel } from '../models/messages'; -import { ConversationModel } from '../models/conversations'; -import { StoredJob } from '../jobs/types'; -import { ReactionType } from '../types/Reactions'; -import { ConversationColorType, CustomColorType } from '../types/Colors'; +import type { MessageModel } from '../models/messages'; +import type { ConversationModel } from '../models/conversations'; +import type { StoredJob } from '../jobs/types'; +import type { ReactionType } from '../types/Reactions'; +import type { ConversationColorType, CustomColorType } from '../types/Colors'; import { StorageAccessType } from '../types/Storage.d'; -import { AttachmentType } from '../types/Attachment'; +import type { AttachmentType } from '../types/Attachment'; export type AttachmentDownloadJobTypeType = | 'long-message' @@ -111,41 +111,48 @@ export type SignedPreKeyType = { privateKey: ArrayBuffer; publicKey: ArrayBuffer; }; -export type StickerPackStatusType = - | 'known' - | 'ephemeral' - | 'downloaded' - | 'installed' - | 'pending' - | 'error'; -export type StickerType = { +export type StickerType = Readonly<{ id: number; packId: string; - emoji: string | null; + emoji?: string; isCoverOnly: boolean; lastUsed?: number; path: string; + width: number; height: number; -}; -export type StickerPackType = { +}>; + +export const StickerPackStatuses = [ + 'known', + 'ephemeral', + 'downloaded', + 'installed', + 'pending', + 'error', +] as const; + +export type StickerPackStatusType = typeof StickerPackStatuses[number]; + +export type StickerPackType = Readonly<{ id: string; key: string; - attemptedStatus: 'downloaded' | 'installed' | 'ephemeral'; + attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral'; author: string; coverStickerId: number; createdAt: number; downloadAttempts: number; - installedAt: number | null; - lastUsed: number; + installedAt?: number; + lastUsed?: number; status: StickerPackStatusType; stickerCount: number; - stickers: ReadonlyArray; + stickers: Record; title: string; -}; +}>; + export type UnprocessedType = { id: string; timestamp: number; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 64fddfe6b..94e2802bf 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -33,6 +33,7 @@ import { ReactionType } from '../types/Reactions'; import { StoredJob } from '../jobs/types'; import { assert } from '../util/assert'; import { combineNames } from '../util/combineNames'; +import { dropNull } from '../util/dropNull'; import { isNormalNumber } from '../util/isNormalNumber'; import { isNotNil } from '../util/isNotNil'; import { ConversationColorType, CustomColorType } from '../types/Colors'; @@ -301,6 +302,7 @@ function rowToSticker(row: StickerRow): StickerType { return { ...row, isCoverOnly: Boolean(row.isCoverOnly), + emoji: dropNull(row.emoji), }; } @@ -4416,13 +4418,13 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise { ) .all({ id }); const payload = { - attemptedStatus, + attemptedStatus: attemptedStatus ?? null, author, coverStickerId, createdAt: createdAt || Date.now(), downloadAttempts: downloadAttempts || 1, id, - installedAt, + installedAt: installedAt ?? null, key, lastUsed: lastUsed || null, status, @@ -4563,7 +4565,7 @@ async function createOrUpdateSticker(sticker: StickerType): Promise { ) ` ).run({ - emoji, + emoji: emoji ?? null, height, id, isCoverOnly: isCoverOnly ? 1 : 0, diff --git a/ts/state/ducks/stickers.ts b/ts/state/ducks/stickers.ts index 22eb04aae..6efff3280 100644 --- a/ts/state/ducks/stickers.ts +++ b/ts/state/ducks/stickers.ts @@ -2,11 +2,17 @@ // SPDX-License-Identifier: AGPL-3.0-only import { Dictionary, omit, reject } from 'lodash'; +import type { + StickerPackStatusType, + StickerType as StickerDBType, + StickerPackType as StickerPackDBType, +} from '../../sql/Interface'; import dataInterface from '../../sql/Client'; import { downloadStickerPack as externalDownloadStickerPack, maybeDeletePack, -} from '../../../js/modules/stickers'; + RecentStickerType, +} from '../../types/Stickers'; import { sendStickerPackSync } from '../../shims/textsecure'; import { trigger } from '../../shims/events'; @@ -20,49 +26,6 @@ const { // State -export type StickerDBType = { - readonly id: number; - readonly packId: string; - - readonly emoji: string | null; - readonly isCoverOnly: boolean; - readonly lastUsed: number; - readonly path: string; -}; - -export const StickerPackStatuses = [ - 'known', - 'ephemeral', - 'downloaded', - 'installed', - 'pending', - 'error', -] as const; - -export type StickerPackStatus = typeof StickerPackStatuses[number]; - -export type StickerPackDBType = { - readonly id: string; - readonly key: string; - - readonly attemptedStatus: 'downloaded' | 'installed' | 'ephemeral'; - readonly author: string; - readonly coverStickerId: number; - readonly createdAt: number; - readonly downloadAttempts: number; - readonly installedAt: number | null; - readonly lastUsed: number; - readonly status: StickerPackStatus; - readonly stickerCount: number; - readonly stickers: Dictionary; - readonly title: string; -}; - -export type RecentStickerType = { - readonly stickerId: number; - readonly packId: string; -}; - export type StickersStateType = { readonly installedPack: string | null; readonly packs: Dictionary; @@ -75,23 +38,23 @@ export type StickersStateType = { export type StickerType = { readonly id: number; readonly packId: string; - readonly emoji: string | null; + readonly emoji?: string; readonly url: string; }; -export type StickerPackType = { - readonly id: string; - readonly key: string; - readonly title: string; - readonly author: string; - readonly isBlessed: boolean; - readonly cover?: StickerType; - readonly lastUsed: number; - readonly attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral'; - readonly status: StickerPackStatus; - readonly stickers: Array; - readonly stickerCount: number; -}; +export type StickerPackType = Readonly<{ + id: string; + key: string; + title: string; + author: string; + isBlessed: boolean; + cover?: StickerType; + lastUsed?: number; + attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral'; + status: StickerPackStatusType; + stickers: Array; + stickerCount: number; +}>; // Actions @@ -128,7 +91,7 @@ type UninstallStickerPackPayloadType = { packId: string; fromSync: boolean; status: 'downloaded'; - installedAt: null; + installedAt?: undefined; recentStickers: Array; }; type UninstallStickerPackAction = { @@ -306,7 +269,7 @@ async function doUninstallStickerPack( packId, fromSync, status, - installedAt: null, + installedAt: undefined, recentStickers: recentStickers.map(item => ({ packId: item.packId, stickerId: item.id, diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index a9a9ee626..b7cd7db8d 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -32,6 +32,7 @@ import { BodyRangesType } from '../../types/Util'; import { LinkPreviewType } from '../../types/message/LinkPreviews'; import { ConversationColors } from '../../types/Colors'; import { CallMode } from '../../types/Calling'; +import { SignalService as Proto } from '../../protobuf'; import { AttachmentType, isVoiceMessage } from '../../types/Attachment'; import { CallingNotificationType } from '../../util/callingNotification'; @@ -430,8 +431,7 @@ function getPropsForUnsupportedMessage( ourNumber: string | undefined, ourUuid: string | undefined ): PropsForUnsupportedMessage { - const CURRENT_PROTOCOL_VERSION = - window.textsecure.protobuf.DataMessage.ProtocolVersion.CURRENT; + const CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT; const requiredVersion = message.requiredProtocolVersion; const canProcessNow = Boolean( @@ -463,9 +463,6 @@ function getPropsForGroupV2Change( conversationSelector: GetConversationByIdType, ourConversationId: string ): GroupsV2Props { - const AccessControlEnum = - window.textsecure.protobuf.AccessControl.AccessRequired; - const RoleEnum = window.textsecure.protobuf.Member.Role; const change = message.groupV2Change; if (!change) { @@ -476,8 +473,6 @@ function getPropsForGroupV2Change( return { groupName: conversation?.type === 'group' ? conversation?.name : undefined, - AccessControlEnum, - RoleEnum, ourConversationId, change, }; @@ -547,8 +542,7 @@ export function isMessageHistoryUnsynced( export function isExpirationTimerUpdate( message: Pick ): boolean { - const flag = - window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; + const flag = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; // eslint-disable-next-line no-bitwise return Boolean(message.flags && message.flags & flag); } @@ -734,7 +728,7 @@ function getPropsForGroupNotification( export function isEndSession( message: Pick ): boolean { - const flag = window.textsecure.protobuf.DataMessage.Flags.END_SESSION; + const flag = Proto.DataMessage.Flags.END_SESSION; // eslint-disable-next-line no-bitwise return Boolean(message.flags && message.flags & flag); } diff --git a/ts/state/selectors/stickers.ts b/ts/state/selectors/stickers.ts index b09c4ada0..abd5cf96d 100644 --- a/ts/state/selectors/stickers.ts +++ b/ts/state/selectors/stickers.ts @@ -14,13 +14,15 @@ import { } from 'lodash'; import { createSelector } from 'reselect'; +import type { RecentStickerType } from '../../types/Stickers'; +import type { + StickerType as StickerDBType, + StickerPackType as StickerPackDBType, +} from '../../sql/Interface'; import { StateType } from '../reducer'; import { - RecentStickerType, - StickerDBType, - StickerPackDBType, - StickerPackType, StickersStateType, + StickerPackType, StickerType, } from '../ducks/stickers'; import { getStickersPath, getTempPath } from './user'; @@ -95,7 +97,7 @@ export const translatePackFromDB = ( const filterAndTransformPacks = ( packs: Dictionary, packFilter: (sticker: StickerPackDBType) => boolean, - packSort: (sticker: StickerPackDBType) => number | null, + packSort: (sticker: StickerPackDBType) => number | undefined, blessedPacks: Dictionary, stickersPath: string, tempPath: string diff --git a/ts/state/smart/GroupLinkManagement.tsx b/ts/state/smart/GroupLinkManagement.tsx index b32bd1cac..0e9150fef 100644 --- a/ts/state/smart/GroupLinkManagement.tsx +++ b/ts/state/smart/GroupLinkManagement.tsx @@ -10,10 +10,8 @@ import { } from '../../components/conversation/conversation-details/GroupLinkManagement'; import { getConversationSelector } from '../selectors/conversations'; import { getIntl } from '../selectors/user'; -import { AccessControlClass } from '../../textsecure.d'; export type SmartGroupLinkManagementProps = { - accessEnum: typeof AccessControlClass.AccessRequired; changeHasGroupLink: (value: boolean) => void; conversationId: string; copyGroupLink: (groupLink: string) => void; diff --git a/ts/state/smart/GroupV2Permissions.tsx b/ts/state/smart/GroupV2Permissions.tsx index de180fc45..80ac406e0 100644 --- a/ts/state/smart/GroupV2Permissions.tsx +++ b/ts/state/smart/GroupV2Permissions.tsx @@ -10,10 +10,8 @@ import { } from '../../components/conversation/conversation-details/GroupV2Permissions'; import { getConversationSelector } from '../selectors/conversations'; import { getIntl } from '../selectors/user'; -import { AccessControlClass } from '../../textsecure.d'; export type SmartGroupV2PermissionsProps = { - accessEnum: typeof AccessControlClass.AccessRequired; conversationId: string; setAccessControlAttributesSetting: (value: number) => void; setAccessControlMembersSetting: (value: number) => void; diff --git a/ts/test-both/ContactsParser_test.ts b/ts/test-both/ContactsParser_test.ts new file mode 100644 index 000000000..883fcaa5a --- /dev/null +++ b/ts/test-both/ContactsParser_test.ts @@ -0,0 +1,126 @@ +// Copyright 2015-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import { Writer } from 'protobufjs'; + +import * as Bytes from '../Bytes'; +import { typedArrayToArrayBuffer } from '../Crypto'; +import { SignalService as Proto } from '../protobuf'; +import { ContactBuffer, GroupBuffer } from '../textsecure/ContactsParser'; + +describe('ContactsParser', () => { + function generateAvatar(): Uint8Array { + const result = new Uint8Array(255); + for (let i = 0; i < result.length; i += 1) { + result[i] = i; + } + return result; + } + + describe('ContactBuffer', () => { + function getTestBuffer(): ArrayBuffer { + const avatarBuffer = generateAvatar(); + + const contactInfoBuffer = Proto.ContactDetails.encode({ + name: 'Zero Cool', + number: '+10000000000', + uuid: '7198E1BD-1293-452A-A098-F982FF201902', + avatar: { contentType: 'image/jpeg', length: avatarBuffer.length }, + }).finish(); + + const writer = new Writer(); + writer.bytes(contactInfoBuffer); + const prefixedContact = writer.finish(); + + const chunks: Array = []; + for (let i = 0; i < 3; i += 1) { + chunks.push(prefixedContact); + chunks.push(avatarBuffer); + } + + return typedArrayToArrayBuffer(Bytes.concatenate(chunks)); + } + + it('parses an array buffer of contacts', () => { + const arrayBuffer = getTestBuffer(); + const contactBuffer = new ContactBuffer(arrayBuffer); + let contact = contactBuffer.next(); + let count = 0; + while (contact !== undefined) { + count += 1; + assert.strictEqual(contact.name, 'Zero Cool'); + assert.strictEqual(contact.number, '+10000000000'); + assert.strictEqual( + contact.uuid, + '7198e1bd-1293-452a-a098-f982ff201902' + ); + assert.strictEqual(contact.avatar?.contentType, 'image/jpeg'); + assert.strictEqual(contact.avatar?.length, 255); + assert.strictEqual(contact.avatar?.data.byteLength, 255); + const avatarBytes = new Uint8Array( + contact.avatar?.data || new ArrayBuffer(0) + ); + for (let j = 0; j < 255; j += 1) { + assert.strictEqual(avatarBytes[j], j); + } + contact = contactBuffer.next(); + } + assert.strictEqual(count, 3); + }); + }); + + describe('GroupBuffer', () => { + function getTestBuffer(): ArrayBuffer { + const avatarBuffer = generateAvatar(); + + const groupInfoBuffer = Proto.GroupDetails.encode({ + id: new Uint8Array([1, 3, 3, 7]), + name: 'Hackers', + membersE164: ['cereal', 'burn', 'phreak', 'joey'], + avatar: { contentType: 'image/jpeg', length: avatarBuffer.length }, + }).finish(); + + const writer = new Writer(); + writer.bytes(groupInfoBuffer); + const prefixedGroup = writer.finish(); + + const chunks: Array = []; + for (let i = 0; i < 3; i += 1) { + chunks.push(prefixedGroup); + chunks.push(avatarBuffer); + } + + return typedArrayToArrayBuffer(Bytes.concatenate(chunks)); + } + + it('parses an array buffer of groups', () => { + const arrayBuffer = getTestBuffer(); + const groupBuffer = new GroupBuffer(arrayBuffer); + let group = groupBuffer.next(); + let count = 0; + while (group !== undefined) { + count += 1; + assert.strictEqual(group.name, 'Hackers'); + assert.deepEqual(group.id, new Uint8Array([1, 3, 3, 7])); + assert.sameMembers(group.membersE164, [ + 'cereal', + 'burn', + 'phreak', + 'joey', + ]); + assert.strictEqual(group.avatar?.contentType, 'image/jpeg'); + assert.strictEqual(group.avatar?.length, 255); + assert.strictEqual(group.avatar?.data.byteLength, 255); + const avatarBytes = new Uint8Array( + group.avatar?.data || new ArrayBuffer(0) + ); + for (let j = 0; j < 255; j += 1) { + assert.strictEqual(avatarBytes[j], j); + } + group = groupBuffer.next(); + } + assert.strictEqual(count, 3); + }); + }); +}); diff --git a/ts/test-both/processDataMessage_test.ts b/ts/test-both/processDataMessage_test.ts new file mode 100644 index 000000000..85b7a68cd --- /dev/null +++ b/ts/test-both/processDataMessage_test.ts @@ -0,0 +1,333 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-restricted-syntax */ + +import { assert } from 'chai'; + +import { + processDataMessage, + ATTACHMENT_MAX, +} from '../textsecure/processDataMessage'; +import { ProcessedAttachment } from '../textsecure/Types.d'; +import { SignalService as Proto } from '../protobuf'; + +const FLAGS = Proto.DataMessage.Flags; + +const TIMESTAMP = Date.now(); + +const UNPROCESSED_ATTACHMENT: Proto.IAttachmentPointer = { + cdnId: 123, + key: new Uint8Array([1, 2, 3]), + digest: new Uint8Array([4, 5, 6]), +}; + +const PROCESSED_ATTACHMENT: ProcessedAttachment = { + cdnId: '123', + key: 'AQID', + digest: 'BAUG', +}; + +const GROUP_ID = new Uint8Array([0x68, 0x65, 0x79]); + +const DERIVED_GROUPV2_ID = '7qQUi8Wa6Jm3Rl+l63saATGeciEqokbHpP+lV3F5t9o='; + +describe('processDataMessage', () => { + const check = (message: Proto.IDataMessage) => + processDataMessage( + { + timestamp: TIMESTAMP, + ...message, + }, + TIMESTAMP + ); + + it('should process attachments', async () => { + const out = await check({ + attachments: [UNPROCESSED_ATTACHMENT], + }); + + assert.deepStrictEqual(out.attachments, [PROCESSED_ATTACHMENT]); + }); + + it('should throw on too many attachments', async () => { + const attachments: Array = []; + for (let i = 0; i < ATTACHMENT_MAX + 1; i += 1) { + attachments.push(UNPROCESSED_ATTACHMENT); + } + + await assert.isRejected( + check({ attachments }), + `Too many attachments: ${ATTACHMENT_MAX + 1} included in one message` + + `, max is ${ATTACHMENT_MAX}` + ); + }); + + it('should process group context UPDATE/QUIT message', async () => { + const { UPDATE, QUIT } = Proto.GroupContext.Type; + + for (const type of [UPDATE, QUIT]) { + // eslint-disable-next-line no-await-in-loop + const out = await check({ + body: 'should be deleted', + attachments: [UNPROCESSED_ATTACHMENT], + group: { + id: GROUP_ID, + name: 'Group', + avatar: UNPROCESSED_ATTACHMENT, + type, + membersE164: ['+1'], + }, + }); + + assert.isUndefined(out.body); + assert.strictEqual(out.attachments.length, 0); + assert.deepStrictEqual(out.group, { + id: 'hey', + name: 'Group', + avatar: PROCESSED_ATTACHMENT, + type, + membersE164: ['+1'], + derivedGroupV2Id: DERIVED_GROUPV2_ID, + }); + } + }); + + it('should process group context DELIVER message', async () => { + const out = await check({ + body: 'should not be deleted', + attachments: [UNPROCESSED_ATTACHMENT], + group: { + id: GROUP_ID, + name: 'should be deleted', + membersE164: ['should be deleted'], + avatar: {}, + type: Proto.GroupContext.Type.DELIVER, + }, + }); + + assert.strictEqual(out.body, 'should not be deleted'); + assert.strictEqual(out.attachments.length, 1); + assert.deepStrictEqual(out.group, { + id: 'hey', + type: Proto.GroupContext.Type.DELIVER, + membersE164: [], + derivedGroupV2Id: DERIVED_GROUPV2_ID, + avatar: undefined, + name: undefined, + }); + }); + + it('should process groupv2 context', async () => { + const out = await check({ + groupV2: { + masterKey: new Uint8Array(32), + revision: 1, + groupChange: new Uint8Array([4, 5, 6]), + }, + }); + + assert.deepStrictEqual(out.groupV2, { + masterKey: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', + revision: 1, + groupChange: 'BAUG', + id: 'd/rq8//fR4RzhvN3G9KcKlQoj7cguQFjTOqLV6JUSbo=', + secretParams: + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAd/rq8//fR' + + '4RzhvN3G9KcKlQoj7cguQFjTOqLV6JUSbrURzeILsUmsymGJmHt3kpBJ2zosqp4ex' + + 'sg+qwF1z6YdB/rxKnxKRLZZP/V0F7bERslYILy2lUh3Sh3iA98yO4CGfzjjFVo1SI' + + '7U8XApLeVNQHJo7nkflf/JyBrqPft5gEucbKW/h+S3OYjfQ5zl2Cpw3XrV7N6OKEu' + + 'tLUWPHQuJx11A4xDPrmtAOnGy2NBxoOybDNlWipeNbn1WQJqOjMF7YA80oEm+5qnM' + + 'kEYcFVqbYaSzPcMhg3mQ0SYfQpxYgSOJpwp9f/8EDnwJV4ISPBOo2CiaSqVfnd8Dw' + + 'ZOc58gQA==', + publicParams: + 'AHf66vP/30eEc4bzdxvSnCpUKI+3ILkBY0zqi1eiVEm6LnGylv4fk' + + 'tzmI30Oc5dgqcN161ezejihLrS1Fjx0LieOJpwp9f/8EDnwJV4ISPBOo2CiaSqVfn' + + 'd8DwZOc58gQA==', + }); + }); + + it('should base64 profileKey', async () => { + const out = await check({ + profileKey: new Uint8Array([42, 23, 55]), + }); + + assert.strictEqual(out.profileKey, 'Khc3'); + }); + + it('should process quote', async () => { + const out = await check({ + quote: { + id: 1, + authorUuid: 'author', + text: 'text', + attachments: [ + { + contentType: 'image/jpeg', + fileName: 'image.jpg', + thumbnail: UNPROCESSED_ATTACHMENT, + }, + ], + }, + }); + + assert.deepStrictEqual(out.quote, { + id: 1, + authorUuid: 'author', + text: 'text', + attachments: [ + { + contentType: 'image/jpeg', + fileName: 'image.jpg', + thumbnail: PROCESSED_ATTACHMENT, + }, + ], + bodyRanges: [], + }); + }); + + it('should process contact', async () => { + const out = await check({ + contact: [ + { + avatar: { + avatar: UNPROCESSED_ATTACHMENT, + }, + }, + { + avatar: { + avatar: UNPROCESSED_ATTACHMENT, + isProfile: true, + }, + }, + ], + }); + + assert.deepStrictEqual(out.contact, [ + { + avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: false }, + }, + { + avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: true }, + }, + ]); + }); + + it('should process reaction', async () => { + assert.deepStrictEqual( + ( + await check({ + reaction: { + emoji: '😎', + targetTimestamp: TIMESTAMP, + }, + }) + ).reaction, + { + emoji: '😎', + remove: false, + targetAuthorUuid: undefined, + targetTimestamp: TIMESTAMP, + } + ); + + assert.deepStrictEqual( + ( + await check({ + reaction: { + emoji: '😎', + remove: true, + targetTimestamp: TIMESTAMP, + }, + }) + ).reaction, + { + emoji: '😎', + remove: true, + targetAuthorUuid: undefined, + targetTimestamp: TIMESTAMP, + } + ); + }); + + it('should process preview', async () => { + const out = await check({ + preview: [ + { + date: TIMESTAMP, + image: UNPROCESSED_ATTACHMENT, + }, + ], + }); + + assert.deepStrictEqual(out.preview, [ + { + date: TIMESTAMP, + description: undefined, + title: undefined, + url: undefined, + image: PROCESSED_ATTACHMENT, + }, + ]); + }); + + it('should process sticker', async () => { + const out = await check({ + sticker: { + packId: new Uint8Array([1, 2, 3]), + packKey: new Uint8Array([4, 5, 6]), + stickerId: 1, + data: UNPROCESSED_ATTACHMENT, + }, + }); + + assert.deepStrictEqual(out.sticker, { + packId: '010203', + packKey: 'BAUG', + stickerId: 1, + data: PROCESSED_ATTACHMENT, + }); + }); + + it('should process FLAGS=END_SESSION', async () => { + const out = await check({ + flags: FLAGS.END_SESSION, + body: 'should be deleted', + group: { + id: GROUP_ID, + type: Proto.GroupContext.Type.DELIVER, + }, + attachments: [UNPROCESSED_ATTACHMENT], + }); + + assert.isUndefined(out.body); + assert.isUndefined(out.group); + assert.deepStrictEqual(out.attachments, []); + }); + + it('should process FLAGS=EXPIRATION_TIMER_UPDATE,PROFILE_KEY_UPDATE', async () => { + const values = [FLAGS.EXPIRATION_TIMER_UPDATE, FLAGS.PROFILE_KEY_UPDATE]; + for (const flags of values) { + // eslint-disable-next-line no-await-in-loop + const out = await check({ + flags, + body: 'should be deleted', + attachments: [UNPROCESSED_ATTACHMENT], + }); + + assert.isUndefined(out.body); + assert.deepStrictEqual(out.attachments, []); + } + }); + + it('processes trivial fields', async () => { + assert.strictEqual((await check({ flags: null })).flags, 0); + assert.strictEqual((await check({ flags: 1 })).flags, 1); + + assert.strictEqual((await check({ expireTimer: null })).expireTimer, 0); + assert.strictEqual((await check({ expireTimer: 123 })).expireTimer, 123); + + assert.isFalse((await check({ isViewOnce: null })).isViewOnce); + assert.isFalse((await check({ isViewOnce: false })).isViewOnce); + assert.isTrue((await check({ isViewOnce: true })).isViewOnce); + }); +}); diff --git a/ts/test-both/processSyncMessage_test.ts b/ts/test-both/processSyncMessage_test.ts new file mode 100644 index 000000000..e0e8f9af8 --- /dev/null +++ b/ts/test-both/processSyncMessage_test.ts @@ -0,0 +1,37 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import getGuid from 'uuid/v4'; + +import { processSyncMessage } from '../textsecure/processSyncMessage'; + +describe('processSyncMessage', () => { + it('should normalize UUIDs in sent', () => { + const destinationUuid = getGuid(); + + const out = processSyncMessage({ + sent: { + destinationUuid: destinationUuid.toUpperCase(), + + unidentifiedStatus: [ + { + destinationUuid: destinationUuid.toUpperCase(), + }, + ], + }, + }); + + assert.deepStrictEqual(out, { + sent: { + destinationUuid, + + unidentifiedStatus: [ + { + destinationUuid, + }, + ], + }, + }); + }); +}); diff --git a/ts/test-both/state/ducks/composer_test.ts b/ts/test-both/state/ducks/composer_test.ts index 1bc0e1415..de5180e82 100644 --- a/ts/test-both/state/ducks/composer_test.ts +++ b/ts/test-both/state/ducks/composer_test.ts @@ -13,7 +13,7 @@ describe('both/state/ducks/composer', () => { conversationId: '123', quote: { attachments: [], - id: '456', + id: 456, isViewOnce: false, messageId: '789', referencedMessageNotFound: false, @@ -114,7 +114,7 @@ describe('both/state/ducks/composer', () => { const nextState = reducer(state, setQuotedMessage(QUOTED_MESSAGE)); assert.equal(nextState.quotedMessage?.conversationId, '123'); - assert.equal(nextState.quotedMessage?.quote?.id, '456'); + assert.equal(nextState.quotedMessage?.quote?.id, 456); }); }); }); diff --git a/ts/test-both/util/dropNull_test.ts b/ts/test-both/util/dropNull_test.ts index d13d897d1..431779d58 100644 --- a/ts/test-both/util/dropNull_test.ts +++ b/ts/test-both/util/dropNull_test.ts @@ -2,7 +2,12 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import { dropNull } from '../../util/dropNull'; +import { dropNull, shallowDropNull } from '../../util/dropNull'; + +type Test = { + a: number | null; + b: number | undefined; +}; describe('dropNull', () => { it('swaps null with undefined', () => { @@ -16,4 +21,42 @@ describe('dropNull', () => { it('non-null values undefined be', () => { assert.strictEqual(dropNull('test'), 'test'); }); + + describe('shallowDropNull', () => { + it('return undefined with given null', () => { + assert.strictEqual(shallowDropNull(null), undefined); + }); + + it('return undefined with given undefined', () => { + assert.strictEqual(shallowDropNull(undefined), undefined); + }); + + it('swaps null with undefined', () => { + const result: + | { + a: number | undefined; + b: number | undefined; + } + | undefined = shallowDropNull({ + a: null, + b: 1, + }); + + assert.deepStrictEqual(result, { a: undefined, b: 1 }); + }); + + it('leaves undefined be', () => { + const result: + | { + a: number | undefined; + b: number | undefined; + } + | undefined = shallowDropNull({ + a: 1, + b: undefined, + }); + + assert.deepStrictEqual(result, { a: 1, b: undefined }); + }); + }); }); diff --git a/ts/test-electron/MessageReceiver_test.ts b/ts/test-electron/MessageReceiver_test.ts new file mode 100644 index 000000000..5ce83664a --- /dev/null +++ b/ts/test-electron/MessageReceiver_test.ts @@ -0,0 +1,76 @@ +// Copyright 2015-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable + class-methods-use-this, + @typescript-eslint/no-empty-function + */ + +import { assert } from 'chai'; +import EventEmitter from 'events'; +import { connection as WebSocket } from 'websocket'; + +import MessageReceiver from '../textsecure/MessageReceiver'; +import { DecryptionErrorEvent } from '../textsecure/messageReceiverEvents'; +import { SignalService as Proto } from '../protobuf'; +import * as Crypto from '../Crypto'; + +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + +describe('MessageReceiver', () => { + class FakeSocket extends EventEmitter { + public sendBytes(_: Uint8Array) {} + + public close() {} + } + + const number = '+19999999999'; + const uuid = 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee'; + const deviceId = 1; + const signalingKey = Crypto.getRandomBytes(32 + 20); + + describe('connecting', () => { + it('generates decryption-error event when it cannot decrypt', done => { + const socket = new FakeSocket(); + + const messageReceiver = new MessageReceiver( + 'oldUsername.2', + 'username.2', + 'password', + signalingKey, + { + serverTrustRoot: 'AAAAAAAA', + socket: socket as WebSocket, + } + ); + + const body = Proto.Envelope.encode({ + type: Proto.Envelope.Type.CIPHERTEXT, + source: number, + sourceUuid: uuid, + sourceDevice: deviceId, + timestamp: Date.now(), + content: new FIXMEU8(Crypto.getRandomBytes(200)), + }).finish(); + + const message = Proto.WebSocketMessage.encode({ + type: Proto.WebSocketMessage.Type.REQUEST, + request: { id: 1, verb: 'PUT', path: '/api/v1/message', body }, + }).finish(); + + socket.emit('message', { + type: 'binary', + binaryData: message, + }); + + messageReceiver.addEventListener( + 'decryption-error', + (error: DecryptionErrorEvent) => { + assert.strictEqual(error.decryptionError.senderUuid, uuid); + assert.strictEqual(error.decryptionError.senderDevice, deviceId); + done(); + } + ); + }); + }); +}); diff --git a/ts/test-electron/models/messages_test.ts b/ts/test-electron/models/messages_test.ts index 672e6dcb6..89162514e 100644 --- a/ts/test-electron/models/messages_test.ts +++ b/ts/test-electron/models/messages_test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { setup as setupI18n } from '../../../js/modules/i18n'; import enMessages from '../../../_locales/en/messages.json'; +import { SignalService as Proto } from '../../protobuf'; describe('Message', () => { const i18n = setupI18n('en', enMessages); @@ -384,8 +385,7 @@ describe('Message', () => { title: 'voice message', attachment: { contentType: 'audio/ogg', - flags: - window.textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE, + flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE, }, expectedText: 'Voice Message', expectedEmoji: '🎤', diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index 45b680a1f..c141c99c9 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -18,6 +18,7 @@ import { Storage } from './textsecure/Storage'; import { StorageServiceCallOptionsType, StorageServiceCredentials, + ProcessedAttachment, } from './textsecure/Types.d'; export type UnprocessedType = { @@ -31,6 +32,7 @@ export type UnprocessedType = { source?: string; sourceDevice?: number; sourceUuid?: string; + messageAgeSec?: number; version: number; }; @@ -71,7 +73,6 @@ type DeviceNameProtobufTypes = { type GroupsProtobufTypes = { AvatarUploadAttributes: typeof AvatarUploadAttributesClass; - Member: typeof MemberClass; MemberPendingProfileKey: typeof MemberPendingProfileKeyClass; MemberPendingAdminApproval: typeof MemberPendingAdminApprovalClass; AccessControl: typeof AccessControlClass; @@ -86,9 +87,6 @@ type GroupsProtobufTypes = { type SignalServiceProtobufTypes = { AttachmentPointer: typeof AttachmentPointerClass; - ContactDetails: typeof ContactDetailsClass; - Content: typeof ContentClass; - DataMessage: typeof DataMessageClass; Envelope: typeof EnvelopeClass; GroupContext: typeof GroupContextClass; GroupContextV2: typeof GroupContextV2Class; @@ -159,39 +157,14 @@ export declare class AvatarUploadAttributesClass { signature?: string; } -export declare class MemberClass { - static decode: ( - data: ArrayBuffer | ByteBufferClass, - encoding?: string - ) => MemberClass; - - userId?: ProtoBinaryType; - role?: MemberRoleEnum; - profileKey?: ProtoBinaryType; - presentation?: ProtoBinaryType; - joinedAtVersion?: number; - - // Note: only role and presentation are required when creating a group -} - export type MemberRoleEnum = number; -// Note: we need to use namespaces to express nested classes in Typescript -export declare namespace MemberClass { - class Role { - static UNKNOWN: number; - static DEFAULT: number; - static ADMINISTRATOR: number; - } -} - export declare class MemberPendingProfileKeyClass { static decode: ( data: ArrayBuffer | ByteBufferClass, encoding?: string ) => MemberPendingProfileKeyClass; - member?: MemberClass; addedByUserId?: ProtoBinaryType; timestamp?: ProtoBigNumberType; } @@ -245,7 +218,6 @@ export declare class GroupClass { disappearingMessagesTimer?: ProtoBinaryType; accessControl?: AccessControlClass; version?: number; - members?: Array; membersPendingProfileKey?: Array; membersPendingAdminApproval?: Array; inviteLinkPassword?: ProtoBinaryType; @@ -299,7 +271,6 @@ export declare namespace GroupChangeClass { // Note: we need to use namespaces to express nested classes in Typescript export declare namespace GroupChangeClass.Actions { class AddMemberAction { - added?: MemberClass; joinFromInviteLink?: boolean; } @@ -480,7 +451,7 @@ export declare class AttachmentPointerClass { GIF: number; }; - cdnId?: ProtoBigNumberType; + cdnId?: string; cdnKey?: string; contentType?: string; key?: ProtoBinaryType; @@ -493,184 +464,16 @@ export declare class AttachmentPointerClass { height?: number; caption?: string; blurHash?: string; - uploadTimestamp?: ProtoBigNumberType; cdnNumber?: number; } -export type DownloadAttachmentType = { +export type DownloadAttachmentType = Omit< + ProcessedAttachment, + 'digest' | 'key' +> & { data: ArrayBuffer; - cdnId?: ProtoBigNumberType; - cdnKey?: string; - contentType?: string; - size?: number; - thumbnail?: ProtoBinaryType; - fileName?: string; - flags?: number; - width?: number; - height?: number; - caption?: string; - blurHash?: string; - uploadTimestamp?: ProtoBigNumberType; - cdnNumber?: number; }; -export declare class ContactDetailsClass { - static decode: ( - data: ArrayBuffer | ByteBufferClass, - encoding?: string - ) => ContactDetailsClass; - - number?: string; - uuid?: string; - name?: string; - avatar?: ContactDetailsClass.Avatar; - color?: string; - verified?: VerifiedClass; - profileKey?: ProtoBinaryType; - blocked?: boolean; - expireTimer?: number; - inboxPosition?: number; -} - -// Note: we need to use namespaces to express nested classes in Typescript -export declare namespace ContactDetailsClass { - class Avatar { - contentType?: string; - length?: number; - } -} - -export declare class ContentClass { - static decode: ( - data: ArrayBuffer | ByteBufferClass, - encoding?: string - ) => ContentClass; - toArrayBuffer: () => ArrayBuffer; - - dataMessage?: DataMessageClass; - syncMessage?: SyncMessageClass; - callingMessage?: CallingMessageClass; - nullMessage?: NullMessageClass; - receiptMessage?: ReceiptMessageClass; - typingMessage?: TypingMessageClass; - senderKeyDistributionMessage?: ByteBufferClass; - decryptionErrorMessage?: ByteBufferClass; -} - -export declare class DataMessageClass { - static decode: ( - data: ArrayBuffer | ByteBufferClass, - encoding?: string - ) => DataMessageClass; - toArrayBuffer(): ArrayBuffer; - - body?: string | null; - attachments?: Array; - group?: GroupContextClass | null; - groupV2?: GroupContextV2Class | null; - flags?: number; - expireTimer?: number; - profileKey?: ProtoBinaryType; - timestamp?: ProtoBigNumberType; - quote?: DataMessageClass.Quote; - contact?: Array; - preview?: Array; - sticker?: DataMessageClass.Sticker; - requiredProtocolVersion?: number; - isViewOnce?: boolean; - reaction?: DataMessageClass.Reaction; - delete?: DataMessageClass.Delete; - bodyRanges?: Array; - groupCallUpdate?: DataMessageClass.GroupCallUpdate; -} - -// Note: we need to use namespaces to express nested classes in Typescript -export declare namespace DataMessageClass { - // Note: deep nesting - class Contact { - name: any; - number: any; - email: any; - address: any; - avatar: any; - organization?: string; - } - - class Flags { - static END_SESSION: number; - static EXPIRATION_TIMER_UPDATE: number; - static PROFILE_KEY_UPDATE: number; - } - - class Preview { - url?: string; - title?: string; - image?: AttachmentPointerClass; - description?: string; - date?: ProtoBigNumberType; - } - - class ProtocolVersion { - static INITIAL: number; - static MESSAGE_TIMERS: number; - static VIEW_ONCE: number; - static VIEW_ONCE_VIDEO: number; - static REACTIONS: number; - static MENTIONS: number; - static CURRENT: number; - } - - // Note: deep nesting - class Quote { - id?: ProtoBigNumberType | null; - authorUuid?: string | null; - text?: string | null; - attachments?: Array; - bodyRanges?: Array; - - // Added later during processing - referencedMessageNotFound?: boolean; - isViewOnce?: boolean; - } - - class BodyRange { - start?: number; - length?: number; - mentionUuid?: string; - } - - class Reaction { - emoji: string | null; - remove: boolean; - targetAuthorUuid: string | null; - targetTimestamp: ProtoBigNumberType | null; - } - - class Delete { - targetSentTimestamp?: ProtoBigNumberType; - } - - class Sticker { - packId?: ProtoBinaryType; - packKey?: ProtoBinaryType; - stickerId?: number; - data?: AttachmentPointerClass; - } - - class GroupCallUpdate { - eraId?: string; - } -} - -// Note: we need to use namespaces to express nested classes in Typescript -export declare namespace DataMessageClass.Quote { - class QuotedAttachment { - contentType?: string; - fileName?: string; - thumbnail?: AttachmentPointerClass; - } -} - declare class DeviceNameClass { static decode: ( data: ArrayBuffer | ByteBufferClass, @@ -1140,7 +943,6 @@ export declare namespace SyncMessageClass { destination?: string; destinationUuid?: string; timestamp?: ProtoBigNumberType; - message?: DataMessageClass; expirationStartTimestamp?: ProtoBigNumberType; unidentifiedStatus?: Array; isRecipientUpdate?: boolean; diff --git a/ts/textsecure/ContactsParser.ts b/ts/textsecure/ContactsParser.ts index 2b776f50e..a7521d1ed 100644 --- a/ts/textsecure/ContactsParser.ts +++ b/ts/textsecure/ContactsParser.ts @@ -1,101 +1,160 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable max-classes-per-file */ -import { ByteBufferClass } from '../window.d'; -import { AttachmentType } from './SendMessage'; +import { Reader } from 'protobufjs'; -type ProtobufConstructorType = { - decode: (data: ArrayBuffer) => ProtobufType; +import { SignalService as Proto } from '../protobuf'; +import { normalizeUuid } from '../util/normalizeUuid'; +import { typedArrayToArrayBuffer } from '../Crypto'; + +import Avatar = Proto.ContactDetails.IAvatar; + +type OptionalAvatar = { avatar?: Avatar | null }; + +type DecoderBase = { + decodeDelimited(reader: Reader): Message | undefined; }; -type ProtobufType = { - avatar?: PackedAttachmentType; - profileKey?: any; - uuid?: string; - members: Array; +export type MessageWithAvatar = Omit< + Message, + 'avatar' +> & { + avatar?: (Avatar & { data: ArrayBuffer }) | null; }; -export type PackedAttachmentType = AttachmentType & { - length: number; -}; +export type ModifiedGroupDetails = MessageWithAvatar; -export class ProtoParser { - buffer: ByteBufferClass; +export type ModifiedContactDetails = MessageWithAvatar; - protobuf: ProtobufConstructorType; +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; - constructor(arrayBuffer: ArrayBuffer, protobuf: ProtobufConstructorType) { - this.protobuf = protobuf; - this.buffer = new window.dcodeIO.ByteBuffer(); - this.buffer.append(arrayBuffer); - this.buffer.offset = 0; - this.buffer.limit = arrayBuffer.byteLength; +class ParserBase< + Message extends OptionalAvatar, + Decoder extends DecoderBase +> { + protected readonly reader: Reader; + + constructor(arrayBuffer: ArrayBuffer, private readonly decoder: Decoder) { + this.reader = new Reader(new FIXMEU8(arrayBuffer)); } - next(): ProtobufType | undefined | null { + protected decodeDelimited(): MessageWithAvatar | undefined { + if (this.reader.pos === this.reader.len) { + return undefined; // eof + } + try { - if (this.buffer.limit === this.buffer.offset) { - return undefined; // eof - } - const len = this.buffer.readVarint32(); - const nextBuffer = this.buffer - .slice(this.buffer.offset, this.buffer.offset + len) - .toArrayBuffer(); + const proto = this.decoder.decodeDelimited(this.reader); - const proto = this.protobuf.decode(nextBuffer); - this.buffer.skip(len); - - if (proto.avatar) { - const attachmentLen = proto.avatar.length; - proto.avatar.data = this.buffer - .slice(this.buffer.offset, this.buffer.offset + attachmentLen) - .toArrayBuffer(); - this.buffer.skip(attachmentLen); + if (!proto) { + return undefined; } - if (proto.profileKey) { - proto.profileKey = proto.profileKey.toArrayBuffer(); + if (!proto.avatar) { + return { + ...proto, + avatar: null, + }; } - if (proto.uuid) { - window.normalizeUuids( - proto, - ['uuid'], - 'ProtoParser::next (proto.uuid)' - ); - } + const attachmentLen = proto.avatar.length ?? 0; + const avatarData = this.reader.buf.slice( + this.reader.pos, + this.reader.pos + attachmentLen + ); + this.reader.skip(attachmentLen); - if (proto.members) { - window.normalizeUuids( - proto, - proto.members.map((_member, i) => `members.${i}.uuid`), - 'ProtoParser::next (proto.members)' - ); - } + return { + ...proto, - return proto; + avatar: { + ...proto.avatar, + + data: typedArrayToArrayBuffer(avatarData), + }, + }; } catch (error) { window.log.error( 'ProtoParser.next error:', error && error.stack ? error.stack : error ); + return undefined; + } + } +} + +export class GroupBuffer extends ParserBase< + Proto.GroupDetails, + typeof Proto.GroupDetails +> { + constructor(arrayBuffer: ArrayBuffer) { + super(arrayBuffer, Proto.GroupDetails); + } + + public next(): ModifiedGroupDetails | undefined { + const proto = this.decodeDelimited(); + if (!proto) { + return undefined; } - return null; + if (!proto.members) { + return proto; + } + return { + ...proto, + members: proto.members.map((member, i) => { + if (!member.uuid) { + return member; + } + + return { + ...member, + uuid: normalizeUuid(member.uuid, `GroupBuffer.member[${i}].uuid`), + }; + }), + }; } } -export class GroupBuffer extends ProtoParser { +export class ContactBuffer extends ParserBase< + Proto.ContactDetails, + typeof Proto.ContactDetails +> { constructor(arrayBuffer: ArrayBuffer) { - super(arrayBuffer, window.textsecure.protobuf.GroupDetails as any); + super(arrayBuffer, Proto.ContactDetails); } -} -export class ContactBuffer extends ProtoParser { - constructor(arrayBuffer: ArrayBuffer) { - super(arrayBuffer, window.textsecure.protobuf.ContactDetails as any); + public next(): ModifiedContactDetails | undefined { + const proto = this.decodeDelimited(); + if (!proto) { + return undefined; + } + + if (!proto.uuid) { + return proto; + } + + const { verified } = proto; + + return { + ...proto, + + verified: + verified && verified.destinationUuid + ? { + ...verified, + + destinationUuid: normalizeUuid( + verified.destinationUuid, + 'ContactBuffer.verified.destinationUuid' + ), + } + : verified, + + uuid: normalizeUuid(proto.uuid, 'ContactBuffer.uuid'), + }; } } diff --git a/ts/textsecure/Crypto.ts b/ts/textsecure/Crypto.ts index a8d22f175..e26e5c36f 100644 --- a/ts/textsecure/Crypto.ts +++ b/ts/textsecure/Crypto.ts @@ -184,7 +184,7 @@ const Crypto = { async decryptAttachment( encryptedBin: ArrayBuffer, keys: ArrayBuffer, - theirDigest: ArrayBuffer + theirDigest?: ArrayBuffer ): Promise { if (keys.byteLength !== 64) { throw new Error('Got invalid length attachment keys'); diff --git a/ts/textsecure/EventTarget.ts b/ts/textsecure/EventTarget.ts index c3bbb482e..e11618be0 100644 --- a/ts/textsecure/EventTarget.ts +++ b/ts/textsecure/EventTarget.ts @@ -12,8 +12,10 @@ * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget */ +export type EventHandler = (event: any) => unknown; + export default class EventTarget { - listeners?: { [type: string]: Array }; + listeners?: { [type: string]: Array }; dispatchEvent(ev: Event): Array { if (!(ev instanceof Event)) { @@ -36,7 +38,7 @@ export default class EventTarget { return results; } - addEventListener(eventName: string, callback: Function): void { + addEventListener(eventName: string, callback: EventHandler): void { if (typeof eventName !== 'string') { throw new Error('First argument expects a string'); } @@ -54,7 +56,7 @@ export default class EventTarget { this.listeners[eventName] = listeners; } - removeEventListener(eventName: string, callback: Function): void { + removeEventListener(eventName: string, callback: EventHandler): void { if (typeof eventName !== 'string') { throw new Error('First argument expects a string'); } diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 711d72dc8..7a37bcfeb 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -4,7 +4,6 @@ /* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable no-bitwise */ /* eslint-disable class-methods-use-this */ -/* eslint-disable more/no-then */ /* eslint-disable camelcase */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable max-classes-per-file */ @@ -14,7 +13,6 @@ import { isNumber, map, omit } from 'lodash'; import PQueue from 'p-queue'; import { v4 as getGuid } from 'uuid'; import { connection as WebSocket } from 'websocket'; -import { z } from 'zod'; import { DecryptionErrorMessage, @@ -41,39 +39,67 @@ import { Sessions, SignedPreKeys, } from '../LibSignalStores'; -import { assert } from '../util/assert'; import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff'; +import { assert, strictAssert } from '../util/assert'; import { BatcherType, createBatcher } from '../util/batcher'; +import { dropNull } from '../util/dropNull'; +import { normalizeUuid } from '../util/normalizeUuid'; +import { normalizeNumber } from '../util/normalizeNumber'; import { sleep } from '../util/sleep'; import { parseIntOrThrow } from '../util/parseIntOrThrow'; import { Zone } from '../util/Zone'; -import EventTarget from './EventTarget'; +import { processAttachment, processDataMessage } from './processDataMessage'; +import { processSyncMessage } from './processSyncMessage'; +import EventTarget, { EventHandler } from './EventTarget'; import { WebAPIType } from './WebAPI'; import utils from './Helpers'; import WebSocketResource, { IncomingWebSocketRequest, + CloseEvent, } from './WebsocketResources'; import { ConnectTimeoutError } from './Errors'; import * as Bytes from '../Bytes'; import Crypto from './Crypto'; import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; import { ContactBuffer, GroupBuffer } from './ContactsParser'; -import { isByteBufferEmpty } from '../util/isByteBufferEmpty'; import { SocketStatus } from '../types/SocketStatus'; +import { SignalService as Proto } from '../protobuf'; + +import { DownloadAttachmentType, UnprocessedType } from '../textsecure.d'; import { - AttachmentPointerClass, - CallingMessageClass, - DataMessageClass, - DownloadAttachmentType, - EnvelopeClass, - ReceiptMessageClass, - SyncMessageClass, - TypingMessageClass, - UnprocessedType, - VerifiedClass, -} from '../textsecure.d'; -import { ByteBufferClass } from '../window.d'; + ProcessedAttachment, + ProcessedDataMessage, + ProcessedSyncMessage, + ProcessedSent, + ProcessedEnvelope, +} from './Types.d'; +import { + ReconnectEvent, + EmptyEvent, + ProgressEvent, + TypingEvent, + ErrorEvent, + DeliveryEvent, + DecryptionErrorEvent, + SentEvent, + ProfileKeyUpdateEvent, + MessageEvent, + RetryRequestEvent, + ReadEvent, + ConfigurationEvent, + ViewSyncEvent, + MessageRequestResponseEvent, + FetchLatestEvent, + KeysEvent, + StickerPackEvent, + VerifiedEvent, + ReadSyncEvent, + ContactEvent, + ContactSyncEvent, + GroupEvent, + GroupSyncEvent, +} from './messageReceiverEvents'; import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups'; @@ -84,87 +110,39 @@ const GROUPV1_ID_LENGTH = 16; const GROUPV2_ID_LENGTH = 32; const RETRY_TIMEOUT = 2 * 60 * 1000; -const decryptionErrorTypeSchema = z - .object({ - cipherTextBytes: z.instanceof(ArrayBuffer).optional(), - cipherTextType: z.number().optional(), - contentHint: z.number().optional(), - groupId: z.string().optional(), - receivedAtCounter: z.number(), - receivedAtDate: z.number(), - senderDevice: z.number(), - senderUuid: z.string(), - timestamp: z.number(), - }) - .passthrough(); -export type DecryptionErrorType = z.infer; - -const retryRequestTypeSchema = z - .object({ - groupId: z.string().optional(), - requesterUuid: z.string(), - requesterDevice: z.number(), - senderDevice: z.number(), - sentAt: z.number(), - }) - .passthrough(); -export type RetryRequestType = z.infer; - -declare global { - // We want to extend `Event`, so we need an interface. - // eslint-disable-next-line no-restricted-syntax - interface Event { - code?: string | number; - configuration?: any; - confirm?: () => void; - contactDetails?: any; - count?: number; - data?: any; - deliveryReceipt?: any; - error?: any; - eventType?: string | number; - groupDetails?: any; +type DecryptedEnvelope = Readonly< + ProcessedEnvelope & { + unidentifiedDeliveryReceived?: boolean; + contentHint?: number; groupId?: string; - groupV2Id?: string; - messageRequestResponseType?: number | null; - proto?: any; - read?: any; - reason?: any; - sender?: any; - senderDevice?: any; - senderUuid?: any; - source?: any; - sourceUuid?: any; - stickerPacks?: any; - threadE164?: string | null; - threadUuid?: string | null; - storageServiceKey?: ArrayBuffer; - timestamp?: any; - typing?: any; - verified?: any; - retryRequest?: RetryRequestType; - decryptionError?: DecryptionErrorType; + usmc?: UnidentifiedSenderMessageContent; } - // We want to extend `Error`, so we need an interface. - // eslint-disable-next-line no-restricted-syntax - interface Error { - reason?: any; - stackForLog?: string; +>; + +type DecryptResult = Readonly<{ + envelope: DecryptedEnvelope; + plaintext?: Uint8Array; +}>; + +type DecryptSealedSenderResult = Readonly< + DecryptResult & { + unsealedPlaintext?: SealedSenderDecryptionResult; + isBlocked?: boolean; } -} +>; + +type InnerDecryptResult = Readonly< + DecryptResult & { + isBlocked?: boolean; + } +>; type CacheAddItemType = { - envelope: EnvelopeClass; + envelope: ProcessedEnvelope; data: UnprocessedType; request: Pick; }; -type DecryptedEnvelope = { - readonly plaintext: ArrayBuffer; - readonly data: UnprocessedType; - readonly envelope: EnvelopeClass; -}; - type LockedStores = { readonly sessionStore: Sessions; readonly identityKeyStore: IdentityKeys; @@ -179,6 +157,8 @@ enum TaskType { class MessageReceiverInner extends EventTarget { _onClose?: (code: number, reason: string) => Promise; + _onWSRClose?: (event: CloseEvent) => void; + _onError?: (error: Error) => Promise; appQueue: PQueue; @@ -318,7 +298,7 @@ class MessageReceiverInner extends EventTarget { static arrayBufferToStringBase64 = (arrayBuffer: ArrayBuffer): string => window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64'); - async connect(): Promise { + async connect(socket?: WebSocket): Promise { if (this.calledClose) { return; } @@ -328,8 +308,7 @@ class MessageReceiverInner extends EventTarget { this.count = 0; if (this.hasConnected) { - const ev = new Event('reconnect'); - this.dispatchEvent(ev); + this.dispatchEvent(new ReconnectEvent()); } this.isEmptied = false; @@ -348,7 +327,7 @@ class MessageReceiverInner extends EventTarget { // initialize the socket and start listening for messages try { - this.socket = await this.server.getMessageSocket(); + this.socket = socket || (await this.server.getMessageSocket()); } catch (error) { this.socketStatus = SocketStatus.CLOSED; @@ -357,9 +336,7 @@ class MessageReceiverInner extends EventTarget { return; } - const event = new Event('error'); - event.error = error; - await this.dispatchAndWait(event); + await this.dispatchAndWait(new ErrorEvent(error)); return; } @@ -371,6 +348,11 @@ class MessageReceiverInner extends EventTarget { if (!this._onClose) { this._onClose = this.onclose.bind(this); } + if (!this._onWSRClose) { + this._onWSRClose = ({ code, reason }: CloseEvent): void => { + this.onclose(code, reason); + }; + } if (!this._onError) { this._onError = this.onerror.bind(this); } @@ -387,8 +369,8 @@ class MessageReceiverInner extends EventTarget { }); // Because sometimes the socket doesn't properly emit its close event - if (this._onClose) { - this.wsr.addEventListener('close', this._onClose); + if (this._onWSRClose) { + this.wsr.addEventListener('close', this._onWSRClose); } } @@ -417,8 +399,8 @@ class MessageReceiverInner extends EventTarget { } if (this.wsr) { - if (this._onClose) { - this.wsr.removeEventListener('close', this._onClose); + if (this._onWSRClose) { + this.wsr.removeEventListener('close', this._onWSRClose); } this.wsr = undefined; } @@ -444,10 +426,8 @@ class MessageReceiverInner extends EventTarget { window.log.error('websocket error', error); } - async dispatchAndWait(event: Event) { + async dispatchAndWait(event: Event): Promise { this.appQueue.add(async () => Promise.all(this.dispatchEvent(event))); - - return Promise.resolve(); } async onclose(code: number, reason: string): Promise { @@ -510,7 +490,7 @@ class MessageReceiverInner extends EventTarget { } const job = async () => { - let plaintext; + let plaintext: Uint8Array; const headers = request.headers || []; if (!request.body) { @@ -520,21 +500,46 @@ class MessageReceiverInner extends EventTarget { } if (headers.includes('X-Signal-Key: true')) { - plaintext = await Crypto.decryptWebsocketMessage( - typedArrayToArrayBuffer(request.body), - this.signalingKey + plaintext = new FIXMEU8( + await Crypto.decryptWebsocketMessage( + typedArrayToArrayBuffer(request.body), + this.signalingKey + ) ); } else { - plaintext = typedArrayToArrayBuffer(request.body); + plaintext = request.body; } try { - const envelope = window.textsecure.protobuf.Envelope.decode(plaintext); - window.normalizeUuids( - envelope, - ['sourceUuid'], - 'message_receiver::handleRequest::job' - ); + const decoded = Proto.Envelope.decode(plaintext); + const serverTimestamp = normalizeNumber(decoded.serverTimestamp); + + const envelope: ProcessedEnvelope = { + // Make non-private envelope IDs dashless so they don't get redacted + // from logs + id: getGuid().replace(/-/g, ''), + receivedAtCounter: window.Signal.Util.incrementMessageCounter(), + receivedAtDate: Date.now(), + // Calculate the message age (time on server). + messageAgeSec: this.calculateMessageAge(headers, serverTimestamp), + + // Proto.Envelope fields + type: decoded.type, + source: decoded.source, + sourceUuid: decoded.sourceUuid + ? normalizeUuid( + decoded.sourceUuid, + 'MessageReceiver.handleRequest.sourceUuid' + ) + : undefined, + sourceDevice: decoded.sourceDevice, + timestamp: normalizeNumber(decoded.timestamp), + legacyMessage: dropNull(decoded.legacyMessage), + content: dropNull(decoded.content), + serverGuid: decoded.serverGuid, + serverTimestamp, + }; + // 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 @@ -549,22 +554,6 @@ class MessageReceiverInner extends EventTarget { return; } - // Make non-private envelope IDs dashless so they don't get redacted - // from logs - envelope.id = getGuid().replace(/-/g, ''); - envelope.serverTimestamp = envelope.serverTimestamp - ? envelope.serverTimestamp.toNumber() - : null; - - envelope.receivedAtCounter = window.Signal.Util.incrementMessageCounter(); - envelope.receivedAtDate = Date.now(); - - // Calculate the message age (time on server). - envelope.messageAgeSec = this.calculateMessageAge( - headers, - envelope.serverTimestamp - ); - this.decryptAndCache(envelope, plaintext, request); this.processedCount += 1; } catch (e) { @@ -573,9 +562,7 @@ class MessageReceiverInner extends EventTarget { 'Error handling incoming message:', e && e.stack ? e.stack : e ); - const ev = new Event('error'); - ev.error = e; - await this.dispatchAndWait(ev); + await this.dispatchAndWait(new ErrorEvent(e)); } }; @@ -640,8 +627,7 @@ class MessageReceiverInner extends EventTarget { ]); window.log.info("MessageReceiver: emitting 'empty' event"); - const ev = new Event('empty'); - this.dispatchEvent(ev); + this.dispatchEvent(new EmptyEvent()); this.isEmptied = true; this.maybeScheduleRetryTimeout(); @@ -693,9 +679,7 @@ class MessageReceiverInner extends EventTarget { if (count % 10 !== 0) { return; } - const ev = new Event('progress'); - ev.count = count; - this.dispatchEvent(ev); + this.dispatchEvent(new ProgressEvent({ count })); } async queueAllCached() { @@ -710,50 +694,48 @@ class MessageReceiverInner extends EventTarget { async queueCached(item: UnprocessedType) { window.log.info('MessageReceiver.queueCached', item.id); try { - let envelopePlaintext: ArrayBuffer; + let envelopePlaintext: Uint8Array; if (item.envelope && item.version === 2) { - envelopePlaintext = MessageReceiverInner.stringToArrayBufferBase64( - item.envelope - ); + envelopePlaintext = Bytes.fromBase64(item.envelope); } else if (item.envelope && typeof item.envelope === 'string') { - envelopePlaintext = MessageReceiverInner.stringToArrayBuffer( - item.envelope - ); + envelopePlaintext = Bytes.fromBinary(item.envelope); } else { throw new Error( 'MessageReceiver.queueCached: item.envelope was malformed' ); } - const envelope = window.textsecure.protobuf.Envelope.decode( - envelopePlaintext - ); - envelope.id = item.id; - envelope.receivedAtCounter = item.timestamp; - envelope.receivedAtDate = Date.now(); - envelope.source = envelope.source || item.source; - envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid; - envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice; - envelope.serverTimestamp = - item.serverTimestamp || envelope.serverTimestamp; + const decoded = Proto.Envelope.decode(envelopePlaintext); - if (envelope.serverTimestamp && envelope.serverTimestamp.toNumber) { - envelope.serverTimestamp = envelope.serverTimestamp.toNumber(); - } + const envelope: ProcessedEnvelope = { + id: item.id, + receivedAtCounter: item.timestamp, + receivedAtDate: Date.now(), + messageAgeSec: item.messageAgeSec || 0, + + // Proto.Envelope fields + type: decoded.type, + source: decoded.source || item.source, + sourceUuid: decoded.sourceUuid || item.sourceUuid, + sourceDevice: decoded.sourceDevice || item.sourceDevice, + timestamp: normalizeNumber(decoded.timestamp), + legacyMessage: dropNull(decoded.legacyMessage), + content: dropNull(decoded.content), + serverGuid: decoded.serverGuid, + serverTimestamp: normalizeNumber( + item.serverTimestamp || decoded.serverTimestamp + ), + }; const { decrypted } = item; if (decrypted) { - let payloadPlaintext: ArrayBuffer; + let payloadPlaintext: Uint8Array; if (item.version === 2) { - payloadPlaintext = MessageReceiverInner.stringToArrayBufferBase64( - decrypted - ); + payloadPlaintext = Bytes.fromBase64(decrypted); } else if (typeof decrypted === 'string') { - payloadPlaintext = MessageReceiverInner.stringToArrayBuffer( - decrypted - ); + payloadPlaintext = Bytes.fromBinary(decrypted); } else { throw new Error('Cached decrypted value was not a string!'); } @@ -787,11 +769,8 @@ class MessageReceiverInner extends EventTarget { } } - getEnvelopeId(envelope: EnvelopeClass) { - const timestamp = - envelope && envelope.timestamp && envelope.timestamp.toNumber - ? envelope.timestamp.toNumber() - : null; + getEnvelopeId(envelope: ProcessedEnvelope) { + const { timestamp } = envelope; if (envelope.sourceUuid || envelope.source) { const sender = envelope.sourceUuid || envelope.source; @@ -864,7 +843,14 @@ class MessageReceiverInner extends EventTarget { async decryptAndCacheBatch(items: Array) { window.log.info('MessageReceiver.decryptAndCacheBatch', items.length); - const decrypted: Array = []; + const decrypted: Array< + Readonly<{ + plaintext: Uint8Array; + data: UnprocessedType; + envelope: DecryptedEnvelope; + }> + > = []; + const storageProtocol = window.textsecure.storage.protocol; try { @@ -889,12 +875,16 @@ class MessageReceiverInner extends EventTarget { await Promise.all( items.map(async ({ data, envelope }) => { try { - const plaintext = await this.queueEncryptedEnvelope( + const result = await this.queueEncryptedEnvelope( { sessionStore, identityKeyStore, zone }, envelope ); - if (plaintext) { - decrypted.push({ plaintext, data, envelope }); + if (result.plaintext) { + decrypted.push({ + plaintext: result.plaintext, + envelope: result.envelope, + data, + }); } } catch (error) { failed.push(data); @@ -922,9 +912,7 @@ class MessageReceiverInner extends EventTarget { sourceDevice: envelope.sourceDevice, serverGuid: envelope.serverGuid, serverTimestamp: envelope.serverTimestamp, - decrypted: MessageReceiverInner.arrayBufferToStringBase64( - plaintext - ), + decrypted: Bytes.toBase64(plaintext), }; } ); @@ -980,17 +968,18 @@ class MessageReceiverInner extends EventTarget { } decryptAndCache( - envelope: EnvelopeClass, - plaintext: ArrayBuffer, + envelope: ProcessedEnvelope, + plaintext: Uint8Array, request: IncomingWebSocketRequest ) { const { id } = envelope; - const data = { + const data: UnprocessedType = { id, version: 2, - envelope: MessageReceiverInner.arrayBufferToStringBase64(plaintext), + envelope: Bytes.toBase64(plaintext), timestamp: envelope.receivedAtCounter, attempts: 1, + messageAgeSec: envelope.messageAgeSec, }; this.decryptAndCacheBatcher.add({ request, @@ -1003,14 +992,14 @@ class MessageReceiverInner extends EventTarget { await window.textsecure.storage.protocol.removeUnprocessed(items); } - removeFromCache(envelope: EnvelopeClass) { + removeFromCache(envelope: ProcessedEnvelope) { const { id } = envelope; this.cacheRemoveBatcher.add(id); } async queueDecryptedEnvelope( - envelope: EnvelopeClass, - plaintext: ArrayBuffer + envelope: DecryptedEnvelope, + plaintext: Uint8Array ) { const id = this.getEnvelopeId(envelope); window.log.info('queueing decrypted envelope', id); @@ -1020,21 +1009,22 @@ class MessageReceiverInner extends EventTarget { task, `queueDecryptedEnvelope ${id}` ); - const promise = this.addToQueue(taskWithTimeout, TaskType.Decrypted); - return promise.catch(error => { + try { + await this.addToQueue(taskWithTimeout, TaskType.Decrypted); + } catch (error) { window.log.error( `queueDecryptedEnvelope error handling envelope ${id}:`, error && error.extra ? JSON.stringify(error.extra) : '', error && error.stack ? error.stack : error ); - }); + } } async queueEncryptedEnvelope( stores: LockedStores, - envelope: EnvelopeClass - ): Promise { + envelope: ProcessedEnvelope + ): Promise { const id = this.getEnvelopeId(envelope); window.log.info('queueing envelope', id); @@ -1059,13 +1049,16 @@ class MessageReceiverInner extends EventTarget { } else { window.log.error(...args); } - return undefined; + return { + plaintext: undefined, + envelope, + }; } } async queueCachedEnvelope( data: UnprocessedType, - envelope: EnvelopeClass + envelope: ProcessedEnvelope ): Promise { this.decryptAndCacheBatcher.add({ request: { @@ -1083,8 +1076,8 @@ class MessageReceiverInner extends EventTarget { // Called after `decryptEnvelope` decrypted the message. async handleDecryptedEnvelope( - envelope: EnvelopeClass, - plaintext: ArrayBuffer + envelope: DecryptedEnvelope, + plaintext: Uint8Array ): Promise { if (this.stoppingProcessing) { return; @@ -1109,15 +1102,15 @@ class MessageReceiverInner extends EventTarget { async decryptEnvelope( stores: LockedStores, - envelope: EnvelopeClass - ): Promise { + envelope: ProcessedEnvelope + ): Promise { if (this.stoppingProcessing) { - return undefined; + return { plaintext: undefined, envelope }; } - if (envelope.type === window.textsecure.protobuf.Envelope.Type.RECEIPT) { + if (envelope.type === Proto.Envelope.Type.RECEIPT) { await this.onDeliveryReceipt(envelope); - return undefined; + return { plaintext: undefined, envelope }; } if (envelope.content) { @@ -1135,365 +1128,392 @@ class MessageReceiverInner extends EventTarget { return this.socketStatus; } - async onDeliveryReceipt(envelope: EnvelopeClass): Promise { - 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, - sourceUuid: envelope.sourceUuid, - sourceDevice: envelope.sourceDevice, - }; - this.dispatchAndWait(ev).then(resolve as any, reject as any); - }); + async onDeliveryReceipt(envelope: ProcessedEnvelope): Promise { + await this.dispatchAndWait( + new DeliveryEvent( + { + timestamp: envelope.timestamp, + source: envelope.source, + sourceUuid: envelope.sourceUuid, + sourceDevice: envelope.sourceDevice, + }, + this.removeFromCache.bind(this, envelope) + ) + ); } - unpad(paddedData: ArrayBuffer) { - const paddedPlaintext = new Uint8Array(paddedData); - let plaintext; - + unpad(paddedPlaintext: Uint8Array): Uint8Array { 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) { + return new Uint8Array(paddedPlaintext.slice(0, i)); + } + if (paddedPlaintext[i] !== 0x00) { throw new Error('Invalid padding'); } } - return plaintext; + return paddedPlaintext; } - async decrypt( + private async decryptSealedSender( { sessionStore, identityKeyStore, zone }: LockedStores, - envelope: EnvelopeClass, - ciphertext: ByteBufferClass - ): Promise { - const logId = this.getEnvelopeId(envelope); - const { serverTrustRoot } = this; - const envelopeTypeEnum = window.textsecure.protobuf.Envelope.Type; - const unidentifiedSenderTypeEnum = - window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type; - - const identifier = envelope.sourceUuid || envelope.source; - const { sourceDevice } = envelope; + envelope: ProcessedEnvelope, + ciphertext: Uint8Array + ): Promise { + const buffer = Buffer.from(ciphertext); const localE164 = window.textsecure.storage.user.getNumber(); const localUuid = window.textsecure.storage.user.getUuid(); const localDeviceId = parseIntOrThrow( window.textsecure.storage.user.getDeviceId(), - 'MessageReceiver.decrypt: localDeviceId' + 'MessageReceiver.decryptSealedSender: localDeviceId' ); if (!localUuid) { - throw new Error('MessageReceiver.decrypt: Failed to fetch local UUID'); + throw new Error( + 'MessageReceiver.decryptSealedSender: Failed to fetch local UUID' + ); } + const messageContent: UnidentifiedSenderMessageContent = await sealedSenderDecryptToUsmc( + buffer, + identityKeyStore + ); + + // Here we take this sender information and attach it back to the envelope + // to make the rest of the app work properly. + const certificate = messageContent.senderCertificate(); + + const originalSource = envelope.source; + const originalSourceUuid = envelope.sourceUuid; + + const unidentifiedLogId = this.getEnvelopeId(envelope); + + const newEnvelope: DecryptedEnvelope = { + ...envelope, + + source: dropNull(certificate.senderE164()), + sourceUuid: normalizeUuid( + certificate.senderUuid(), + 'MessageReceiver.decryptSealedSender.UNIDENTIFIED_SENDER.sourceUuid' + ), + sourceDevice: certificate.senderDeviceId(), + unidentifiedDeliveryReceived: !(originalSource || originalSourceUuid), + contentHint: messageContent.contentHint(), + groupId: messageContent.groupId()?.toString('base64'), + usmc: messageContent, + }; + + if ( + (newEnvelope.source && this.isBlocked(newEnvelope.source)) || + (newEnvelope.sourceUuid && this.isUuidBlocked(newEnvelope.sourceUuid)) + ) { + window.log.info( + 'MessageReceiver.decryptSealedSender: Dropping blocked message after ' + + 'partial sealed sender decryption' + ); + return { isBlocked: true, envelope: newEnvelope }; + } + + if (!newEnvelope.serverTimestamp) { + throw new Error( + 'MessageReceiver.decryptSealedSender: ' + + 'Sealed sender message was missing serverTimestamp' + ); + } + + const unidentifiedSenderTypeEnum = + Proto.UnidentifiedSenderMessage.Message.Type; + + if ( + messageContent.msgType() === unidentifiedSenderTypeEnum.PLAINTEXT_CONTENT + ) { + window.log.info( + `decrypt/${unidentifiedLogId}: unidentified message/plaintext contents` + ); + const plaintextContent = PlaintextContent.deserialize( + messageContent.contents() + ); + + return { + plaintext: plaintextContent.body(), + envelope: newEnvelope, + }; + } + + if ( + messageContent.msgType() === unidentifiedSenderTypeEnum.SENDERKEY_MESSAGE + ) { + window.log.info( + `decrypt/${unidentifiedLogId}: unidentified message/sender key contents` + ); + const sealedSenderIdentifier = certificate.senderUuid(); + const sealedSenderSourceDevice = certificate.senderDeviceId(); + const senderKeyStore = new SenderKeys(); + + const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`; + + const plaintext = await window.textsecure.storage.protocol.enqueueSenderKeyJob( + address, + () => + groupDecrypt( + ProtocolAddress.new( + sealedSenderIdentifier, + sealedSenderSourceDevice + ), + senderKeyStore, + messageContent.contents() + ), + zone + ); + + return { + plaintext, + envelope: newEnvelope, + }; + } + + window.log.info( + `decrypt/${unidentifiedLogId}: unidentified message/passing to sealedSenderDecryptMessage` + ); + const preKeyStore = new PreKeys(); const signedPreKeyStore = new SignedPreKeys(); - let promise: Promise< - ArrayBuffer | { isMe: boolean } | { isBlocked: boolean } | undefined - >; + const sealedSenderIdentifier = newEnvelope.sourceUuid || newEnvelope.source; + const address = `${sealedSenderIdentifier}.${newEnvelope.sourceDevice}`; + const unsealedPlaintext = await window.textsecure.storage.protocol.enqueueSessionJob( + address, + () => + sealedSenderDecryptMessage( + buffer, + PublicKey.deserialize(Buffer.from(this.serverTrustRoot)), + newEnvelope.serverTimestamp, + localE164 || null, + localUuid, + localDeviceId, + sessionStore, + identityKeyStore, + preKeyStore, + signedPreKeyStore + ), + zone + ); + + return { unsealedPlaintext, envelope: newEnvelope }; + } + + private async innerDecrypt( + stores: LockedStores, + envelope: ProcessedEnvelope, + ciphertext: Uint8Array + ): Promise { + const { sessionStore, identityKeyStore, zone } = stores; + + const logId = this.getEnvelopeId(envelope); + const envelopeTypeEnum = Proto.Envelope.Type; + + const identifier = envelope.sourceUuid || envelope.source; + const { sourceDevice } = envelope; + + const preKeyStore = new PreKeys(); + const signedPreKeyStore = new SignedPreKeys(); if (envelope.type === envelopeTypeEnum.PLAINTEXT_CONTENT) { window.log.info(`decrypt/${logId}: plaintext message`); - const buffer = Buffer.from(ciphertext.toArrayBuffer()); + const buffer = Buffer.from(ciphertext); const plaintextContent = PlaintextContent.deserialize(buffer); - promise = Promise.resolve( - this.unpad(typedArrayToArrayBuffer(plaintextContent.body())) - ); - } else if (envelope.type === envelopeTypeEnum.CIPHERTEXT) { + return { + plaintext: this.unpad(plaintextContent.body()), + envelope, + }; + } + if (envelope.type === envelopeTypeEnum.CIPHERTEXT) { window.log.info(`decrypt/${logId}: ciphertext message`); if (!identifier) { throw new Error( - 'MessageReceiver.decrypt: No identifier for CIPHERTEXT message' + 'MessageReceiver.innerDecrypt: No identifier for CIPHERTEXT message' ); } if (!sourceDevice) { throw new Error( - 'MessageReceiver.decrypt: No sourceDevice for CIPHERTEXT message' + 'MessageReceiver.innerDecrypt: No sourceDevice for CIPHERTEXT message' ); } - const signalMessage = SignalMessage.deserialize( - Buffer.from(ciphertext.toArrayBuffer()) - ); + const signalMessage = SignalMessage.deserialize(Buffer.from(ciphertext)); const address = `${identifier}.${sourceDevice}`; - promise = window.textsecure.storage.protocol.enqueueSessionJob( - address, - () => - signalDecrypt( - signalMessage, - ProtocolAddress.new(identifier, sourceDevice), - sessionStore, - identityKeyStore - ).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext))), - zone - ); - } else if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) { + return { + plaintext: await window.textsecure.storage.protocol.enqueueSessionJob( + address, + async () => + this.unpad( + await signalDecrypt( + signalMessage, + ProtocolAddress.new(identifier, sourceDevice), + sessionStore, + identityKeyStore + ) + ), + zone + ), + envelope, + }; + } + if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) { window.log.info(`decrypt/${logId}: prekey message`); if (!identifier) { throw new Error( - 'MessageReceiver.decrypt: No identifier for PREKEY_BUNDLE message' + 'MessageReceiver.innerDecrypt: No identifier for PREKEY_BUNDLE message' ); } if (!sourceDevice) { throw new Error( - 'MessageReceiver.decrypt: No sourceDevice for PREKEY_BUNDLE message' + 'MessageReceiver.innerDecrypt: No sourceDevice for PREKEY_BUNDLE message' ); } const preKeySignalMessage = PreKeySignalMessage.deserialize( - Buffer.from(ciphertext.toArrayBuffer()) + Buffer.from(ciphertext) ); const address = `${identifier}.${sourceDevice}`; - promise = window.textsecure.storage.protocol.enqueueSessionJob( - address, - () => - signalDecryptPreKey( - preKeySignalMessage, - ProtocolAddress.new(identifier, sourceDevice), - sessionStore, - identityKeyStore, - preKeyStore, - signedPreKeyStore - ).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext))), - zone - ); - } else if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) { - window.log.info(`decrypt/${logId}: unidentified message`); - const buffer = Buffer.from(ciphertext.toArrayBuffer()); - - const decryptSealedSender = async (): Promise< - SealedSenderDecryptionResult | Buffer | null | { isBlocked: true } - > => { - const messageContent: UnidentifiedSenderMessageContent = await sealedSenderDecryptToUsmc( - buffer, - identityKeyStore - ); - - // Here we take this sender information and attach it back to the envelope - // to make the rest of the app work properly. - const certificate = messageContent.senderCertificate(); - - const originalSource = envelope.source; - const originalSourceUuid = envelope.sourceUuid; - - // eslint-disable-next-line no-param-reassign - envelope.source = certificate.senderE164() || undefined; - // eslint-disable-next-line no-param-reassign - envelope.sourceUuid = certificate.senderUuid(); - window.normalizeUuids( - envelope, - ['sourceUuid'], - 'message_receiver::decrypt::UNIDENTIFIED_SENDER' - ); - - // eslint-disable-next-line no-param-reassign - envelope.sourceDevice = certificate.senderDeviceId(); - // eslint-disable-next-line no-param-reassign - envelope.unidentifiedDeliveryReceived = !( - originalSource || originalSourceUuid - ); - - const unidentifiedLogId = this.getEnvelopeId(envelope); - - // eslint-disable-next-line no-param-reassign - envelope.contentHint = messageContent.contentHint(); - // eslint-disable-next-line no-param-reassign - envelope.groupId = messageContent.groupId()?.toString('base64'); - // eslint-disable-next-line no-param-reassign - envelope.usmc = messageContent; - - if ( - (envelope.source && this.isBlocked(envelope.source)) || - (envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)) - ) { - window.log.info( - 'MessageReceiver.decrypt: Dropping blocked message after partial sealed sender decryption' - ); - return { isBlocked: true }; - } - - if (!envelope.serverTimestamp) { - throw new Error( - 'MessageReceiver.decrypt: Sealed sender message was missing serverTimestamp' - ); - } - - if ( - messageContent.msgType() === - unidentifiedSenderTypeEnum.PLAINTEXT_CONTENT - ) { - window.log.info( - `decrypt/${unidentifiedLogId}: unidentified message/plaintext contents` - ); - const plaintextContent = PlaintextContent.deserialize( - messageContent.contents() - ); - - return plaintextContent.body(); - } - - if ( - messageContent.msgType() === - unidentifiedSenderTypeEnum.SENDERKEY_MESSAGE - ) { - window.log.info( - `decrypt/${unidentifiedLogId}: unidentified message/sender key contents` - ); - const sealedSenderIdentifier = certificate.senderUuid(); - const sealedSenderSourceDevice = certificate.senderDeviceId(); - const senderKeyStore = new SenderKeys(); - - const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`; - - return window.textsecure.storage.protocol.enqueueSenderKeyJob( - address, - () => - groupDecrypt( - ProtocolAddress.new( - sealedSenderIdentifier, - sealedSenderSourceDevice - ), - senderKeyStore, - messageContent.contents() - ), - zone - ); - } - - window.log.info( - `decrypt/${unidentifiedLogId}: unidentified message/passing to sealedSenderDecryptMessage` - ); - const sealedSenderIdentifier = envelope.sourceUuid || envelope.source; - const address = `${sealedSenderIdentifier}.${envelope.sourceDevice}`; - return window.textsecure.storage.protocol.enqueueSessionJob( + return { + plaintext: await window.textsecure.storage.protocol.enqueueSessionJob( address, - () => - sealedSenderDecryptMessage( - buffer, - PublicKey.deserialize(Buffer.from(serverTrustRoot)), - envelope.serverTimestamp, - localE164 || null, - localUuid, - localDeviceId, - sessionStore, - identityKeyStore, - preKeyStore, - signedPreKeyStore + async () => + this.unpad( + await signalDecryptPreKey( + preKeySignalMessage, + ProtocolAddress.new(identifier, sourceDevice), + sessionStore, + identityKeyStore, + preKeyStore, + signedPreKeyStore + ) ), zone - ); + ), + envelope, }; + } + if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) { + window.log.info(`decrypt/${logId}: unidentified message`); + const { + plaintext, + unsealedPlaintext, + isBlocked, + envelope: newEnvelope, + } = await this.decryptSealedSender(stores, envelope, ciphertext); - promise = decryptSealedSender().then(result => { - if (result === null) { - return { isMe: true }; - } - if ('isBlocked' in result) { - return result; - } - if (result instanceof Buffer) { - return this.unpad(typedArrayToArrayBuffer(result)); - } + if (isBlocked) { + return { isBlocked: true, envelope: newEnvelope }; + } - const content = typedArrayToArrayBuffer(result.message()); + if (plaintext) { + return { + plaintext: this.unpad(plaintext), + envelope: newEnvelope, + }; + } + + if (unsealedPlaintext) { + const content = unsealedPlaintext.message(); if (!content) { throw new Error( - 'MessageReceiver.decrypt: Content returned was falsey!' + 'MessageReceiver.innerDecrypt: Content returned was falsey!' ); } // Return just the content because that matches the signature of the other // decrypt methods used above. - return this.unpad(content); - }); - } else { - promise = Promise.reject(new Error('Unknown message type')); + return { + plaintext: this.unpad(content), + envelope: newEnvelope, + }; + } + + throw new Error('Unexpected lack of plaintext from unidentified sender'); } + throw new Error('Unknown message type'); + } - return promise - .then( - ( - plaintext: - | ArrayBuffer - | { isMe: boolean } - | { isBlocked: boolean } - | undefined - ) => { - if (!plaintext || 'isMe' in plaintext || 'isBlocked' in plaintext) { - this.removeFromCache(envelope); - return null; - } + async decrypt( + stores: LockedStores, + envelope: ProcessedEnvelope, + ciphertext: Uint8Array + ): Promise { + let newEnvelope: DecryptedEnvelope = envelope; + try { + const result = await this.innerDecrypt(stores, envelope, ciphertext); - return plaintext; - } - ) - .catch(async error => { + newEnvelope = result.envelope; + + const { isBlocked, plaintext } = result; + + if (isBlocked) { this.removeFromCache(envelope); + return { plaintext: undefined, envelope: newEnvelope }; + } - const uuid = envelope.sourceUuid; - const deviceId = envelope.sourceDevice; + assert(plaintext, 'Should have plaintext from innerDecrypt'); + return { plaintext: new FIXMEU8(plaintext), envelope: newEnvelope }; + } catch (error) { + this.removeFromCache(newEnvelope); - // We don't do a light session reset if it's just a duplicated message - if ( - error?.message?.includes && - error.message.includes('message with old counter') - ) { - throw error; - } - - // We don't do a light session reset if it's an error with the sealed sender - // wrapper, since we don't trust the sender information. - if ( - error?.message?.includes && - error.message.includes('trust root validation failed') - ) { - throw error; - } - - if (uuid && deviceId) { - const event = new Event('decryption-error'); - event.decryptionError = { - cipherTextBytes: envelope.usmc - ? typedArrayToArrayBuffer(envelope.usmc.contents()) - : undefined, - cipherTextType: envelope.usmc ? envelope.usmc.msgType() : undefined, - contentHint: envelope.contentHint, - groupId: envelope.groupId, - receivedAtCounter: envelope.receivedAtCounter, - receivedAtDate: envelope.receivedAtDate, - senderDevice: deviceId, - senderUuid: uuid, - timestamp: envelope.timestamp.toNumber(), - }; - - // Avoid deadlocks by scheduling processing on decrypted queue - this.addToQueue( - () => this.dispatchAndWait(event), - TaskType.Decrypted - ); - } else { - const envelopeId = this.getEnvelopeId(envelope); - window.log.error( - `MessageReceiver.decrypt: Envelope ${envelopeId} missing uuid or deviceId` - ); - } + const uuid = newEnvelope.sourceUuid; + const deviceId = newEnvelope.sourceDevice; + // We don't do a light session reset if it's just a duplicated message + if ( + error?.message?.includes && + error.message.includes('message with old counter') + ) { throw error; - }); + } + + // We don't do a light session reset if it's an error with the sealed sender + // wrapper, since we don't trust the sender information. + if ( + error?.message?.includes && + error.message.includes('trust root validation failed') + ) { + throw error; + } + + if (uuid && deviceId) { + const { usmc } = newEnvelope; + const event = new DecryptionErrorEvent({ + cipherTextBytes: usmc + ? typedArrayToArrayBuffer(usmc.contents()) + : undefined, + cipherTextType: usmc ? usmc.msgType() : undefined, + contentHint: newEnvelope.contentHint, + groupId: newEnvelope.groupId, + receivedAtCounter: newEnvelope.receivedAtCounter, + receivedAtDate: newEnvelope.receivedAtDate, + senderDevice: deviceId, + senderUuid: uuid, + timestamp: newEnvelope.timestamp, + }); + + // Avoid deadlocks by scheduling processing on decrypted queue + this.addToQueue(() => this.dispatchAndWait(event), TaskType.Decrypted); + } else { + const envelopeId = this.getEnvelopeId(newEnvelope); + window.log.error( + `MessageReceiver.decrypt: Envelope ${envelopeId} missing uuid or deviceId` + ); + } + + throw error; + } } async handleSentMessage( - envelope: EnvelopeClass, - sentContainer: SyncMessageClass.Sent + envelope: ProcessedEnvelope, + sentContainer: ProcessedSent ) { window.log.info( 'MessageReceiver.handleSentMessage', @@ -1515,10 +1535,7 @@ class MessageReceiverInner extends EventTarget { let p: Promise = Promise.resolve(); // eslint-disable-next-line no-bitwise - if ( - msg.flags && - msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION - ) { + if (msg.flags && msg.flags & Proto.DataMessage.Flags.END_SESSION) { const identifier = destination || destinationUuid; if (!identifier) { throw new Error( @@ -1527,59 +1544,57 @@ class MessageReceiverInner extends EventTarget { } p = this.handleEndSession(identifier); } - return p.then(async () => - this.processDecrypted(envelope, msg).then(message => { - // prettier-ignore - const groupId = this.getGroupId(message); - const isBlocked = this.isGroupBlocked(groupId); - const { source, sourceUuid } = envelope; - const ourE164 = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); - const isMe = - (source && ourE164 && source === ourE164) || - (sourceUuid && ourUuid && sourceUuid === ourUuid); - const isLeavingGroup = Boolean( - !message.groupV2 && - message.group && - message.group.type === - window.textsecure.protobuf.GroupContext.Type.QUIT - ); + await p; - if (groupId && isBlocked && !(isMe && isLeavingGroup)) { - window.log.warn( - `Message ${this.getEnvelopeId( - envelope - )} ignored; destined for blocked group` - ); - this.removeFromCache(envelope); - return undefined; - } - - const ev = new Event('sent'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { - destination, - destinationUuid, - timestamp: timestamp.toNumber(), - serverTimestamp: envelope.serverTimestamp, - device: envelope.sourceDevice, - unidentifiedStatus, - message, - isRecipientUpdate, - receivedAtCounter: envelope.receivedAtCounter, - receivedAtDate: envelope.receivedAtDate, - }; - if (expirationStartTimestamp) { - ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber(); - } - return this.dispatchAndWait(ev); - }) + const message = await this.processDecrypted(envelope, msg); + const groupId = this.getProcessedGroupId(message); + const isBlocked = groupId ? this.isGroupBlocked(groupId) : false; + const { source, sourceUuid } = envelope; + const ourE164 = window.textsecure.storage.user.getNumber(); + const ourUuid = window.textsecure.storage.user.getUuid(); + const isMe = + (source && ourE164 && source === ourE164) || + (sourceUuid && ourUuid && sourceUuid === ourUuid); + const isLeavingGroup = Boolean( + !message.groupV2 && + message.group && + message.group.type === Proto.GroupContext.Type.QUIT ); + + if (groupId && isBlocked && !(isMe && isLeavingGroup)) { + window.log.warn( + `Message ${this.getEnvelopeId( + envelope + )} ignored; destined for blocked group` + ); + this.removeFromCache(envelope); + return undefined; + } + + const ev = new SentEvent( + { + destination: dropNull(destination), + destinationUuid: dropNull(destinationUuid), + timestamp: timestamp ? normalizeNumber(timestamp) : undefined, + serverTimestamp: envelope.serverTimestamp, + device: envelope.sourceDevice, + unidentifiedStatus, + message, + isRecipientUpdate: Boolean(isRecipientUpdate), + receivedAtCounter: envelope.receivedAtCounter, + receivedAtDate: envelope.receivedAtDate, + expirationStartTimestamp: expirationStartTimestamp + ? normalizeNumber(expirationStartTimestamp) + : undefined, + }, + this.removeFromCache.bind(this, envelope) + ); + return this.dispatchAndWait(ev); } async handleDataMessage( - envelope: EnvelopeClass, - msg: DataMessageClass + envelope: DecryptedEnvelope, + msg: Proto.IDataMessage ): Promise { window.log.info( 'MessageReceiver.handleDataMessage', @@ -1599,129 +1614,119 @@ class MessageReceiverInner extends EventTarget { return undefined; } - await this.deriveGroupV1Data(msg); - this.deriveGroupV2Data(msg); + await this.checkGroupV1Data(msg); - if ( - msg.flags && - msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION - ) { + if (msg.flags && msg.flags & Proto.DataMessage.Flags.END_SESSION) { p = this.handleEndSession(destination); } - if ( - msg.flags && - msg.flags & - window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE - ) { - const ev = new Event('profileKeyUpdate'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { - source: envelope.source, - sourceUuid: envelope.sourceUuid, - profileKey: msg.profileKey.toString('base64'), - }; - return this.dispatchAndWait(ev); - } + if (msg.flags && msg.flags & Proto.DataMessage.Flags.PROFILE_KEY_UPDATE) { + strictAssert(msg.profileKey, 'PROFILE_KEY_UPDATE without profileKey'); - return p.then(async () => - this.processDecrypted(envelope, msg).then(message => { - // prettier-ignore - const groupId = this.getGroupId(message); - const isBlocked = this.isGroupBlocked(groupId); - const { source, sourceUuid } = envelope; - const ourE164 = window.textsecure.storage.user.getNumber(); - const ourUuid = window.textsecure.storage.user.getUuid(); - const isMe = - (source && ourE164 && source === ourE164) || - (sourceUuid && ourUuid && sourceUuid === ourUuid); - const isLeavingGroup = Boolean( - !message.groupV2 && - message.group && - message.group.type === - window.textsecure.protobuf.GroupContext.Type.QUIT - ); - - if (groupId && isBlocked && !(isMe && isLeavingGroup)) { - window.log.warn( - `Message ${this.getEnvelopeId( - envelope - )} ignored; destined for blocked group` - ); - this.removeFromCache(envelope); - return undefined; - } - - const ev = new Event('message'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.data = { + const ev = new ProfileKeyUpdateEvent( + { source: envelope.source, sourceUuid: envelope.sourceUuid, - sourceDevice: envelope.sourceDevice, - timestamp: envelope.timestamp.toNumber(), - serverGuid: envelope.serverGuid, - serverTimestamp: envelope.serverTimestamp, - unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived, - message, - receivedAtCounter: envelope.receivedAtCounter, - receivedAtDate: envelope.receivedAtDate, - }; - return this.dispatchAndWait(ev); - }) + profileKey: Bytes.toBase64(msg.profileKey), + }, + this.removeFromCache.bind(this, envelope) + ); + return this.dispatchAndWait(ev); + } + await p; + + const message = await this.processDecrypted(envelope, msg); + const groupId = this.getProcessedGroupId(message); + const isBlocked = groupId ? this.isGroupBlocked(groupId) : false; + const { source, sourceUuid } = envelope; + const ourE164 = window.textsecure.storage.user.getNumber(); + const ourUuid = window.textsecure.storage.user.getUuid(); + const isMe = + (source && ourE164 && source === ourE164) || + (sourceUuid && ourUuid && sourceUuid === ourUuid); + const isLeavingGroup = Boolean( + !message.groupV2 && + message.group && + message.group.type === Proto.GroupContext.Type.QUIT ); + + if (groupId && isBlocked && !(isMe && isLeavingGroup)) { + window.log.warn( + `Message ${this.getEnvelopeId( + envelope + )} ignored; destined for blocked group` + ); + this.removeFromCache(envelope); + return undefined; + } + + const ev = new MessageEvent( + { + source: envelope.source, + sourceUuid: envelope.sourceUuid, + sourceDevice: envelope.sourceDevice, + timestamp: envelope.timestamp, + serverGuid: envelope.serverGuid, + serverTimestamp: envelope.serverTimestamp, + unidentifiedDeliveryReceived: Boolean( + envelope.unidentifiedDeliveryReceived + ), + message, + receivedAtCounter: envelope.receivedAtCounter, + receivedAtDate: envelope.receivedAtDate, + }, + this.removeFromCache.bind(this, envelope) + ); + return this.dispatchAndWait(ev); } async decryptLegacyMessage( stores: LockedStores, - envelope: EnvelopeClass - ): Promise { + envelope: ProcessedEnvelope + ): Promise { window.log.info( 'MessageReceiver.decryptLegacyMessage', this.getEnvelopeId(envelope) ); - const plaintext = await this.decrypt( - stores, - envelope, - envelope.legacyMessage - ); - if (!plaintext) { + assert(envelope.legacyMessage, 'Should have `legacyMessage` field'); + const result = await this.decrypt(stores, envelope, envelope.legacyMessage); + if (!result.plaintext) { window.log.warn('decryptLegacyMessage: plaintext was falsey'); - return undefined; } - return plaintext; + return result; } async innerHandleLegacyMessage( - envelope: EnvelopeClass, - plaintext: ArrayBuffer + envelope: ProcessedEnvelope, + plaintext: Uint8Array ) { - const message = window.textsecure.protobuf.DataMessage.decode(plaintext); + const message = Proto.DataMessage.decode(plaintext); return this.handleDataMessage(envelope, message); } async decryptContentMessage( stores: LockedStores, - envelope: EnvelopeClass - ): Promise { + envelope: ProcessedEnvelope + ): Promise { window.log.info( 'MessageReceiver.decryptContentMessage', this.getEnvelopeId(envelope) ); - const plaintext = await this.decrypt(stores, envelope, envelope.content); - if (!plaintext) { + assert(envelope.content, 'Should have `content` field'); + const result = await this.decrypt(stores, envelope, envelope.content); + if (!result.plaintext) { window.log.warn('decryptContentMessage: plaintext was falsey'); - return undefined; } - return plaintext; + return result; } async innerHandleContentMessage( - envelope: EnvelopeClass, - plaintext: ArrayBuffer + envelope: ProcessedEnvelope, + plaintext: Uint8Array ): Promise { - const content = window.textsecure.protobuf.Content.decode(plaintext); + const content = Proto.Content.decode(plaintext); // Note: a distribution message can be tacked on to any other message, so we // make sure to process it first. If that fails, we still try to process @@ -1729,7 +1734,7 @@ class MessageReceiverInner extends EventTarget { try { if ( content.senderKeyDistributionMessage && - !isByteBufferEmpty(content.senderKeyDistributionMessage) + Bytes.isNotEmpty(content.senderKeyDistributionMessage) ) { await this.handleSenderKeyDistributionMessage( envelope, @@ -1745,7 +1750,7 @@ class MessageReceiverInner extends EventTarget { if ( content.decryptionErrorMessage && - !isByteBufferEmpty(content.decryptionErrorMessage) + Bytes.isNotEmpty(content.decryptionErrorMessage) ) { await this.handleDecryptionError( envelope, @@ -1754,7 +1759,10 @@ class MessageReceiverInner extends EventTarget { return; } if (content.syncMessage) { - await this.handleSyncMessage(envelope, content.syncMessage); + await this.handleSyncMessage( + envelope, + processSyncMessage(content.syncMessage) + ); return; } if (content.dataMessage) { @@ -1780,19 +1788,19 @@ class MessageReceiverInner extends EventTarget { this.removeFromCache(envelope); - if (isByteBufferEmpty(content.senderKeyDistributionMessage)) { + if (Bytes.isEmpty(content.senderKeyDistributionMessage)) { throw new Error('Unsupported content message'); } } async handleDecryptionError( - envelope: EnvelopeClass, - decryptionError: ByteBufferClass + envelope: DecryptedEnvelope, + decryptionError: Uint8Array ) { const logId = this.getEnvelopeId(envelope); window.log.info(`handleDecryptionError: ${logId}`); - const buffer = Buffer.from(decryptionError.toArrayBuffer()); + const buffer = Buffer.from(decryptionError); const request = DecryptionErrorMessage.deserialize(buffer); this.removeFromCache(envelope); @@ -1805,20 +1813,19 @@ class MessageReceiverInner extends EventTarget { return; } - const event = new Event('retry-request'); - event.retryRequest = { + const event = new RetryRequestEvent({ groupId: envelope.groupId, requesterDevice: sourceDevice, requesterUuid: sourceUuid, senderDevice: request.deviceId(), sentAt: request.timestamp(), - }; + }); await this.dispatchAndWait(event); } async handleSenderKeyDistributionMessage( - envelope: EnvelopeClass, - distributionMessage: ByteBufferClass + envelope: ProcessedEnvelope, + distributionMessage: Uint8Array ): Promise { const envelopeId = this.getEnvelopeId(envelope); window.log.info(`handleSenderKeyDistributionMessage/${envelopeId}`); @@ -1841,7 +1848,7 @@ class MessageReceiverInner extends EventTarget { const sender = ProtocolAddress.new(identifier, sourceDevice); const senderKeyDistributionMessage = SenderKeyDistributionMessage.deserialize( - Buffer.from(distributionMessage.toArrayBuffer()) + Buffer.from(distributionMessage) ); const senderKeyStore = new SenderKeys(); const address = `${identifier}.${sourceDevice}`; @@ -1856,8 +1863,8 @@ class MessageReceiverInner extends EventTarget { } async handleCallingMessage( - envelope: EnvelopeClass, - callingMessage: CallingMessageClass + envelope: ProcessedEnvelope, + callingMessage: Proto.ICallingMessage ): Promise { this.removeFromCache(envelope); await window.Signal.Services.calling.handleCallingMessage( @@ -1867,40 +1874,36 @@ class MessageReceiverInner extends EventTarget { } async handleReceiptMessage( - envelope: EnvelopeClass, - receiptMessage: ReceiptMessageClass + envelope: ProcessedEnvelope, + receiptMessage: Proto.IReceiptMessage ): Promise { const results = []; - if ( - receiptMessage.type === - window.textsecure.protobuf.ReceiptMessage.Type.DELIVERY - ) { + strictAssert(receiptMessage.timestamp, 'Receipt message without timestamp'); + if (receiptMessage.type === Proto.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(), - envelopeTimestamp: envelope.timestamp.toNumber(), - source: envelope.source, - sourceUuid: envelope.sourceUuid, - sourceDevice: envelope.sourceDevice, - }; + const ev = new DeliveryEvent( + { + timestamp: normalizeNumber(receiptMessage.timestamp[i]), + envelopeTimestamp: envelope.timestamp, + source: envelope.source, + sourceUuid: envelope.sourceUuid, + sourceDevice: envelope.sourceDevice, + }, + this.removeFromCache.bind(this, envelope) + ); results.push(this.dispatchAndWait(ev)); } - } else if ( - receiptMessage.type === - window.textsecure.protobuf.ReceiptMessage.Type.READ - ) { + } else if (receiptMessage.type === Proto.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(), - envelopeTimestamp: envelope.timestamp.toNumber(), - source: envelope.source, - sourceUuid: envelope.sourceUuid, - }; + const ev = new ReadEvent( + { + timestamp: normalizeNumber(receiptMessage.timestamp[i]), + envelopeTimestamp: envelope.timestamp, + source: envelope.source, + sourceUuid: envelope.sourceUuid, + }, + this.removeFromCache.bind(this, envelope) + ); results.push(this.dispatchAndWait(ev)); } } @@ -1908,16 +1911,14 @@ class MessageReceiverInner extends EventTarget { } async handleTypingMessage( - envelope: EnvelopeClass, - typingMessage: TypingMessageClass + envelope: ProcessedEnvelope, + typingMessage: Proto.ITypingMessage ): Promise { - const ev = new Event('typing'); - this.removeFromCache(envelope); if (envelope.timestamp && typingMessage.timestamp) { - const envelopeTimestamp = envelope.timestamp.toNumber(); - const typingTimestamp = typingMessage.timestamp.toNumber(); + const envelopeTimestamp = envelope.timestamp; + const typingTimestamp = normalizeNumber(typingMessage.timestamp); if (typingTimestamp !== envelopeTimestamp) { window.log.warn( @@ -1927,39 +1928,45 @@ class MessageReceiverInner extends EventTarget { } } + strictAssert( + envelope.sourceDevice !== undefined, + 'TypingMessage requires sourceDevice in the envelope' + ); + const { groupId, timestamp, action } = typingMessage; - ev.sender = envelope.source; - ev.senderUuid = envelope.sourceUuid; - ev.senderDevice = envelope.sourceDevice; - - ev.typing = { - typingMessage, - timestamp: timestamp ? timestamp.toNumber() : Date.now(), - started: - action === window.textsecure.protobuf.TypingMessage.Action.STARTED, - stopped: - action === window.textsecure.protobuf.TypingMessage.Action.STOPPED, - }; - - const groupIdBuffer = groupId ? groupId.toArrayBuffer() : null; - - if (groupIdBuffer && groupIdBuffer.byteLength > 0) { - if (groupIdBuffer.byteLength === GROUPV1_ID_LENGTH) { - ev.typing.groupId = groupId.toString('binary'); - ev.typing.groupV2Id = await this.deriveGroupV2FromV1(groupIdBuffer); - } else if (groupIdBuffer.byteLength === GROUPV2_ID_LENGTH) { - ev.typing.groupV2Id = groupId.toString('base64'); + let groupIdString: string | undefined; + let groupV2IdString: string | undefined; + if (groupId && groupId.byteLength > 0) { + if (groupId.byteLength === GROUPV1_ID_LENGTH) { + groupIdString = Bytes.toBinary(groupId); + groupV2IdString = await this.deriveGroupV2FromV1(groupId); + } else if (groupId.byteLength === GROUPV2_ID_LENGTH) { + groupV2IdString = Bytes.toBase64(groupId); } else { window.log.error('handleTypingMessage: Received invalid groupId value'); - this.removeFromCache(envelope); } } - await this.dispatchEvent(ev); + await this.dispatchEvent( + new TypingEvent({ + sender: envelope.source, + senderUuid: envelope.sourceUuid, + senderDevice: envelope.sourceDevice, + typing: { + typingMessage, + timestamp: timestamp ? normalizeNumber(timestamp) : Date.now(), + started: action === Proto.TypingMessage.Action.STARTED, + stopped: action === Proto.TypingMessage.Action.STOPPED, + + groupId: groupIdString, + groupV2Id: groupV2IdString, + }, + }) + ); } - handleNullMessage(envelope: EnvelopeClass): void { + handleNullMessage(envelope: ProcessedEnvelope): void { window.log.info( 'MessageReceiver.handleNullMessage', this.getEnvelopeId(envelope) @@ -1968,13 +1975,14 @@ class MessageReceiverInner extends EventTarget { } isInvalidGroupData( - message: DataMessageClass, - envelope: EnvelopeClass + message: Proto.IDataMessage, + envelope: ProcessedEnvelope ): boolean { const { group, groupV2 } = message; if (group) { - const id = group.id.toArrayBuffer(); + const { id } = group; + strictAssert(id, 'Group data has no id'); const isInvalid = id.byteLength !== GROUPV1_ID_LENGTH; if (isInvalid) { @@ -1988,7 +1996,8 @@ class MessageReceiverInner extends EventTarget { } if (groupV2) { - const masterKey = groupV2.masterKey.toArrayBuffer(); + const { masterKey } = groupV2; + strictAssert(masterKey, 'Group v2 data has no masterKey'); const isInvalid = masterKey.byteLength !== MASTER_KEY_LENGTH; if (isInvalid) { @@ -2003,19 +2012,21 @@ class MessageReceiverInner extends EventTarget { return false; } - async deriveGroupV2FromV1(groupId: ArrayBuffer): Promise { + async deriveGroupV2FromV1(groupId: Uint8Array): Promise { if (groupId.byteLength !== GROUPV1_ID_LENGTH) { throw new Error( `deriveGroupV2FromV1: had id with wrong byteLength: ${groupId.byteLength}` ); } - const masterKey = await deriveMasterKeyFromGroupV1(groupId); + const masterKey = await deriveMasterKeyFromGroupV1( + typedArrayToArrayBuffer(groupId) + ); const data = deriveGroupFields(new FIXMEU8(masterKey)); return Bytes.toBase64(data.id); } - async deriveGroupV1Data(message: DataMessageClass) { + async checkGroupV1Data(message: Readonly): Promise { const { group } = message; if (!group) { @@ -2026,87 +2037,52 @@ class MessageReceiverInner extends EventTarget { throw new Error('deriveGroupV1Data: had falsey id'); } - const id = group.id.toArrayBuffer(); + const { id } = group; if (id.byteLength !== GROUPV1_ID_LENGTH) { throw new Error( `deriveGroupV1Data: had id with wrong byteLength: ${id.byteLength}` ); } - group.derivedGroupV2Id = await this.deriveGroupV2FromV1(id); } - deriveGroupV2Data(message: DataMessageClass) { - const { groupV2 } = message; - - if (!groupV2) { - return; - } - - if (!isNumber(groupV2.revision)) { - throw new Error('deriveGroupV2Data: revision was not a number'); - } - if (!groupV2.masterKey) { - throw new Error('deriveGroupV2Data: had falsey masterKey'); - } - - const toBase64 = MessageReceiverInner.arrayBufferToStringBase64; - const masterKey: ArrayBuffer = groupV2.masterKey.toArrayBuffer(); - const length = masterKey.byteLength; - if (length !== MASTER_KEY_LENGTH) { - throw new Error( - `deriveGroupV2Data: masterKey had length ${length}, expected ${MASTER_KEY_LENGTH}` - ); - } - - const fields = deriveGroupFields(new FIXMEU8(masterKey)); - groupV2.masterKey = toBase64(masterKey); - groupV2.secretParams = Bytes.toBase64(fields.secretParams); - groupV2.publicParams = Bytes.toBase64(fields.publicParams); - groupV2.id = Bytes.toBase64(fields.id); - - if (groupV2.groupChange) { - groupV2.groupChange = groupV2.groupChange.toString('base64'); - } - } - - getGroupId(message: DataMessageClass) { + getProcessedGroupId(message: ProcessedDataMessage): string | undefined { if (message.groupV2) { return message.groupV2.id; } - if (message.group) { - return message.group.id.toString('binary'); + if (message.group && message.group.id) { + return message.group.id; } - - return null; + return undefined; } - getDestination(sentMessage: SyncMessageClass.Sent) { + getGroupId(message: Proto.IDataMessage): string | undefined { + if (message.groupV2) { + strictAssert(message.groupV2.masterKey, 'Missing groupV2.masterKey'); + const { id } = deriveGroupFields(message.groupV2.masterKey); + return Bytes.toBase64(id); + } + if (message.group && message.group.id) { + return Bytes.toBinary(message.group.id); + } + + return undefined; + } + + getDestination(sentMessage: Proto.SyncMessage.ISent) { if (sentMessage.message && sentMessage.message.groupV2) { - return `groupv2(${sentMessage.message.groupV2.id})`; + return `groupv2(${this.getGroupId(sentMessage.message)})`; } if (sentMessage.message && sentMessage.message.group) { - return `group(${sentMessage.message.group.id.toBinary()})`; + strictAssert(sentMessage.message.group.id, 'group without id'); + return `group(${this.getGroupId(sentMessage.message)})`; } return sentMessage.destination || sentMessage.destinationUuid; } async handleSyncMessage( - envelope: EnvelopeClass, - syncMessage: SyncMessageClass + envelope: ProcessedEnvelope, + syncMessage: ProcessedSyncMessage ): Promise { - const unidentified = syncMessage.sent - ? syncMessage.sent.unidentifiedStatus || [] - : []; - window.normalizeUuids( - syncMessage, - [ - 'sent.destinationUuid', - ...unidentified.map( - (_el, i) => `sent.unidentifiedStatus.${i}.destinationUuid` - ), - ], - 'message_receiver::handleSyncMessage' - ); const fromSelfSource = envelope.source && envelope.source === this.number_id; const fromSelfSourceUuid = @@ -2132,13 +2108,14 @@ class MessageReceiverInner extends EventTarget { return undefined; } - await this.deriveGroupV1Data(sentMessage.message); - this.deriveGroupV2Data(sentMessage.message); + await this.checkGroupV1Data(sentMessage.message); + + strictAssert(sentMessage.timestamp, 'sent message without timestamp'); window.log.info( 'sent message to', this.getDestination(sentMessage), - sentMessage.timestamp.toNumber(), + normalizeNumber(sentMessage.timestamp), 'from', this.getEnvelopeId(envelope) ); @@ -2202,59 +2179,53 @@ class MessageReceiverInner extends EventTarget { } async handleConfiguration( - envelope: EnvelopeClass, - configuration: SyncMessageClass.Configuration + envelope: ProcessedEnvelope, + configuration: Proto.SyncMessage.IConfiguration ) { window.log.info('got configuration sync message'); - const ev = new Event('configuration'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.configuration = configuration; + const ev = new ConfigurationEvent( + configuration, + this.removeFromCache.bind(this, envelope) + ); return this.dispatchAndWait(ev); } async handleViewOnceOpen( - envelope: EnvelopeClass, - sync: SyncMessageClass.ViewOnceOpen + envelope: ProcessedEnvelope, + sync: Proto.SyncMessage.IViewOnceOpen ) { window.log.info('got view once open sync message'); - const ev = new Event('viewSync'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.source = sync.sender; - ev.sourceUuid = sync.senderUuid; - ev.timestamp = sync.timestamp ? sync.timestamp.toNumber() : null; - - window.normalizeUuids( - ev, - ['sourceUuid'], - 'message_receiver::handleViewOnceOpen' + const ev = new ViewSyncEvent( + { + source: dropNull(sync.sender), + sourceUuid: sync.senderUuid + ? normalizeUuid(sync.senderUuid, 'handleViewOnceOpen.senderUuid') + : undefined, + timestamp: sync.timestamp ? normalizeNumber(sync.timestamp) : undefined, + }, + this.removeFromCache.bind(this, envelope) ); return this.dispatchAndWait(ev); } async handleMessageRequestResponse( - envelope: EnvelopeClass, - sync: SyncMessageClass.MessageRequestResponse + envelope: ProcessedEnvelope, + sync: Proto.SyncMessage.IMessageRequestResponse ) { window.log.info('got message request response sync message'); - const ev = new Event('messageRequestResponse'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.threadE164 = sync.threadE164; - ev.threadUuid = sync.threadUuid; - ev.messageRequestResponseType = sync.type; + const { groupId } = sync; - const idBuffer: ArrayBuffer = sync.groupId - ? sync.groupId.toArrayBuffer() - : null; - - if (idBuffer && idBuffer.byteLength > 0) { - if (idBuffer.byteLength === GROUPV1_ID_LENGTH) { - ev.groupId = sync.groupId.toString('binary'); - ev.groupV2Id = await this.deriveGroupV2FromV1(idBuffer); - } else if (idBuffer.byteLength === GROUPV2_ID_LENGTH) { - ev.groupV2Id = sync.groupId.toString('base64'); + let groupIdString: string | undefined; + let groupV2IdString: string | undefined; + if (groupId && groupId.byteLength > 0) { + if (groupId.byteLength === GROUPV1_ID_LENGTH) { + groupIdString = Bytes.toBinary(groupId); + groupV2IdString = await this.deriveGroupV2FromV1(groupId); + } else if (groupId.byteLength === GROUPV2_ID_LENGTH) { + groupV2IdString = Bytes.toBase64(groupId); } else { this.removeFromCache(envelope); window.log.error('Received message request with invalid groupId'); @@ -2262,104 +2233,123 @@ class MessageReceiverInner extends EventTarget { } } - window.normalizeUuids( - ev, - ['threadUuid'], - 'MessageReceiver::handleMessageRequestResponse' + const ev = new MessageRequestResponseEvent( + { + threadE164: dropNull(sync.threadE164), + threadUuid: sync.threadUuid + ? normalizeUuid( + sync.threadUuid, + 'handleMessageRequestResponse.threadUuid' + ) + : undefined, + messageRequestResponseType: sync.type, + groupId: groupIdString, + groupV2Id: groupV2IdString, + }, + this.removeFromCache.bind(this, envelope) ); return this.dispatchAndWait(ev); } async handleFetchLatest( - envelope: EnvelopeClass, - sync: SyncMessageClass.FetchLatest + envelope: ProcessedEnvelope, + sync: Proto.SyncMessage.IFetchLatest ) { window.log.info('got fetch latest sync message'); - const ev = new Event('fetchLatest'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.eventType = sync.type; + const ev = new FetchLatestEvent( + sync.type, + this.removeFromCache.bind(this, envelope) + ); return this.dispatchAndWait(ev); } - async handleKeys(envelope: EnvelopeClass, sync: SyncMessageClass.Keys) { + async handleKeys(envelope: ProcessedEnvelope, sync: Proto.SyncMessage.IKeys) { window.log.info('got keys sync message'); if (!sync.storageService) { return undefined; } - const ev = new Event('keys'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.storageServiceKey = sync.storageService.toArrayBuffer(); + const ev = new KeysEvent( + typedArrayToArrayBuffer(sync.storageService), + this.removeFromCache.bind(this, envelope) + ); return this.dispatchAndWait(ev); } async handleStickerPackOperation( - envelope: EnvelopeClass, - operations: Array + envelope: ProcessedEnvelope, + operations: Array ) { - const ENUM = - window.textsecure.protobuf.SyncMessage.StickerPackOperation.Type; + const ENUM = Proto.SyncMessage.StickerPackOperation.Type; window.log.info('got sticker pack operation sync message'); - const ev = new Event('sticker-pack'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.stickerPacks = operations.map(operation => ({ - id: operation.packId ? operation.packId.toString('hex') : null, - key: operation.packKey ? operation.packKey.toString('base64') : null, + + const stickerPacks = operations.map(operation => ({ + id: operation.packId ? Bytes.toHex(operation.packId) : undefined, + key: operation.packKey ? Bytes.toBase64(operation.packKey) : undefined, isInstall: operation.type === ENUM.INSTALL, isRemove: operation.type === ENUM.REMOVE, })); + + const ev = new StickerPackEvent( + stickerPacks, + this.removeFromCache.bind(this, envelope) + ); + return this.dispatchAndWait(ev); } - async handleVerified(envelope: EnvelopeClass, verified: VerifiedClass) { - const ev = new Event('verified'); - ev.confirm = this.removeFromCache.bind(this, envelope); - ev.verified = { - state: verified.state, - destination: verified.destination, - destinationUuid: verified.destinationUuid, - identityKey: verified.identityKey.toArrayBuffer(), - }; - window.normalizeUuids( - ev, - ['verified.destinationUuid'], - 'message_receiver::handleVerified' + async handleVerified(envelope: ProcessedEnvelope, verified: Proto.IVerified) { + const ev = new VerifiedEvent( + { + state: verified.state, + destination: dropNull(verified.destination), + destinationUuid: verified.destinationUuid + ? normalizeUuid( + verified.destinationUuid, + 'handleVerified.destinationUuid' + ) + : undefined, + identityKey: verified.identityKey + ? typedArrayToArrayBuffer(verified.identityKey) + : undefined, + }, + this.removeFromCache.bind(this, envelope) ); return this.dispatchAndWait(ev); } async handleRead( - envelope: EnvelopeClass, - read: Array + envelope: ProcessedEnvelope, + read: Array ): Promise { window.log.info('MessageReceiver.handleRead', this.getEnvelopeId(envelope)); 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 = { - envelopeTimestamp: envelope.timestamp.toNumber(), - timestamp: read[i].timestamp.toNumber(), - sender: read[i].sender, - senderUuid: read[i].senderUuid, - }; - window.normalizeUuids( - ev, - ['read.senderUuid'], - 'message_receiver::handleRead' + for (const { timestamp, sender, senderUuid } of read) { + const ev = new ReadSyncEvent( + { + envelopeTimestamp: envelope.timestamp, + timestamp: normalizeNumber(dropNull(timestamp)), + sender: dropNull(sender), + senderUuid: senderUuid + ? normalizeUuid(senderUuid, 'handleRead.senderUuid') + : undefined, + }, + this.removeFromCache.bind(this, envelope) ); results.push(this.dispatchAndWait(ev)); } await Promise.all(results); } - handleContacts(envelope: EnvelopeClass, contacts: SyncMessageClass.Contacts) { + async handleContacts( + envelope: ProcessedEnvelope, + contacts: Proto.SyncMessage.IContacts + ) { window.log.info('contact sync'); const { blob } = contacts; if (!blob) { @@ -2370,33 +2360,29 @@ class MessageReceiverInner extends EventTarget { // Note: we do not return here because we don't want to block the next message on // this attachment download and a lot of processing of that attachment. - this.handleAttachment(blob).then(async attachmentPointer => { - const results = []; - const contactBuffer = new ContactBuffer(attachmentPointer.data); - let contactDetails = contactBuffer.next(); - while (contactDetails !== undefined) { - const contactEvent = new Event('contact'); - contactEvent.contactDetails = contactDetails; - window.normalizeUuids( - contactEvent, - ['contactDetails.verified.destinationUuid'], - 'message_receiver::handleContacts::handleAttachment' - ); - results.push(this.dispatchAndWait(contactEvent)); + const attachmentPointer = await this.handleAttachment(blob); + const results = []; + const contactBuffer = new ContactBuffer(attachmentPointer.data); + let contactDetails = contactBuffer.next(); + while (contactDetails !== undefined) { + const contactEvent = new ContactEvent(contactDetails); + results.push(this.dispatchAndWait(contactEvent)); - contactDetails = contactBuffer.next(); - } + contactDetails = contactBuffer.next(); + } - const finalEvent = new Event('contactsync'); - results.push(this.dispatchAndWait(finalEvent)); + const finalEvent = new ContactSyncEvent(); + results.push(this.dispatchAndWait(finalEvent)); - return Promise.all(results).then(() => { - window.log.info('handleContacts: finished'); - }); - }); + await Promise.all(results); + + window.log.info('handleContacts: finished'); } - handleGroups(envelope: EnvelopeClass, groups: SyncMessageClass.Groups) { + async handleGroups( + envelope: ProcessedEnvelope, + groups: Proto.SyncMessage.IGroups + ): Promise { window.log.info('group sync'); const { blob } = groups; @@ -2408,47 +2394,44 @@ class MessageReceiverInner extends EventTarget { // Note: we do not return here because we don't want to block the next message on // this attachment download and a lot of processing of that attachment. - this.handleAttachment(blob).then(async attachmentPointer => { - const groupBuffer = new GroupBuffer(attachmentPointer.data); - let groupDetails = groupBuffer.next() as any; - const promises = []; - while (groupDetails) { - groupDetails.id = groupDetails.id.toBinary(); - const ev = new Event('group'); - ev.groupDetails = groupDetails; - const promise = this.dispatchAndWait(ev).catch(e => { - window.log.error('error processing group', e); - }); - groupDetails = groupBuffer.next(); - promises.push(promise); - } - - return Promise.all(promises).then(async () => { - const ev = new Event('groupsync'); - return this.dispatchAndWait(ev); + const attachmentPointer = await this.handleAttachment(blob); + const groupBuffer = new GroupBuffer(attachmentPointer.data); + let groupDetails = groupBuffer.next() as any; + const promises = []; + while (groupDetails) { + strictAssert(groupDetails.id, 'Group details without id'); + groupDetails.id = Bytes.toBinary(groupDetails.id); + const ev = new GroupEvent(groupDetails); + const promise = this.dispatchAndWait(ev).catch(e => { + window.log.error('error processing group', e); }); - }); + groupDetails = groupBuffer.next(); + promises.push(promise); + } + + await Promise.all(promises); + + const ev = new GroupSyncEvent(); + return this.dispatchAndWait(ev); } async handleBlocked( - envelope: EnvelopeClass, - blocked: SyncMessageClass.Blocked + envelope: ProcessedEnvelope, + blocked: Proto.SyncMessage.IBlocked ) { window.log.info('Setting these numbers as blocked:', blocked.numbers); if (blocked.numbers) { await window.textsecure.storage.put('blocked', blocked.numbers); } if (blocked.uuids) { - window.normalizeUuids( - blocked, - blocked.uuids.map((_uuid: string, i: number) => `uuids.${i}`), - 'message_receiver::handleBlocked' - ); - window.log.info('Setting these uuids as blocked:', blocked.uuids); - await window.textsecure.storage.put('blocked-uuids', blocked.uuids); + const uuids = blocked.uuids.map((uuid, index) => { + return normalizeUuid(uuid, `handleBlocked.uuids.${index}`); + }); + window.log.info('Setting these uuids as blocked:', uuids); + await window.textsecure.storage.put('blocked-uuids', uuids); } - const groupIds = map(blocked.groupIds, groupId => groupId.toBinary()); + const groupIds = map(blocked.groupIds, groupId => Bytes.toBinary(groupId)); window.log.info( 'Setting these groups as blocked:', groupIds.map(groupId => `group(${groupId})`) @@ -2470,42 +2453,8 @@ class MessageReceiverInner extends EventTarget { return window.textsecure.storage.blocked.isGroupBlocked(groupId); } - cleanAttachment(attachment: AttachmentPointerClass) { - return { - ...omit(attachment, 'thumbnail'), - cdnId: attachment.cdnId?.toString(), - key: attachment.key ? attachment.key.toString('base64') : null, - digest: attachment.digest ? attachment.digest.toString('base64') : null, - }; - } - - private isLinkPreviewDateValid(value: unknown): value is number { - return ( - typeof value === 'number' && - !Number.isNaN(value) && - Number.isFinite(value) && - value > 0 - ); - } - - private cleanLinkPreviewDate(value: unknown): number | null { - if (this.isLinkPreviewDateValid(value)) { - return value; - } - if (!value) { - return null; - } - let result: unknown; - try { - result = (value as any).toNumber(); - } catch (err) { - return null; - } - return this.isLinkPreviewDateValid(result) ? result : null; - } - async downloadAttachment( - attachment: AttachmentPointerClass + attachment: ProcessedAttachment ): Promise { const cdnId = attachment.cdnId || attachment.cdnKey; const { cdnNumber } = attachment; @@ -2514,13 +2463,20 @@ class MessageReceiverInner extends EventTarget { throw new Error('downloadAttachment: Attachment was missing cdnId!'); } - const encrypted = await this.server.getAttachment(cdnId, cdnNumber); + strictAssert(cdnId, 'attachment without cdnId'); + const encrypted = await this.server.getAttachment( + cdnId, + dropNull(cdnNumber) + ); const { key, digest, size } = attachment; if (!digest) { throw new Error('Failure: Ask sender to update Signal and resend.'); } + strictAssert(key, 'attachment has no key'); + strictAssert(digest, 'attachment has no digest'); + const paddedData = await Crypto.decryptAttachment( encrypted, MessageReceiverInner.stringToArrayBufferBase64(key), @@ -2542,9 +2498,9 @@ class MessageReceiverInner extends EventTarget { } async handleAttachment( - attachment: AttachmentPointerClass + attachment: Proto.IAttachmentPointer ): Promise { - const cleaned = this.cleanAttachment(attachment); + const cleaned = processAttachment(attachment); return this.downloadAttachment(cleaned); } @@ -2553,192 +2509,17 @@ class MessageReceiverInner extends EventTarget { await window.textsecure.storage.protocol.archiveAllSessions(identifier); } - async processDecrypted(envelope: EnvelopeClass, decrypted: DataMessageClass) { - /* eslint-disable no-bitwise, no-param-reassign */ - const FLAGS = window.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 (!envelope.timestamp || !decrypted.timestamp) { - throw new Error('Missing timestamp on dataMessage or envelope'); - } - - const envelopeTimestamp = envelope.timestamp.toNumber(); - const decryptedTimestamp = decrypted.timestamp.toNumber(); - - if (envelopeTimestamp !== decryptedTimestamp) { - throw new Error( - `Timestamp ${decrypted.timestamp} in DataMessage did not match envelope timestamp ${envelope.timestamp}` - ); - } - - if (decrypted.flags == null) { - decrypted.flags = 0; - } - if (decrypted.expireTimer == null) { - decrypted.expireTimer = 0; - } - - const isEndSession = Boolean(decrypted.flags & FLAGS.END_SESSION); - const isExpirationTimerUpdate = Boolean( - decrypted.flags & FLAGS.EXPIRATION_TIMER_UPDATE - ); - const isProfileKeyUpdate = Boolean( - decrypted.flags & FLAGS.PROFILE_KEY_UPDATE - ); - // The following assertion codifies an assumption: 0 or 1 flags are set, but never - // more. This assumption is fine as of this writing, but may not always be. - const flagCount = [ - isEndSession, - isExpirationTimerUpdate, - isProfileKeyUpdate, - ].filter(Boolean).length; - assert( - flagCount <= 1, - `Expected exactly <=1 flags to be set, but got ${flagCount}` - ); - - if (isEndSession) { - decrypted.body = null; - decrypted.attachments = []; - decrypted.group = null; - return Promise.resolve(decrypted); - } - if (isExpirationTimerUpdate) { - decrypted.body = null; - decrypted.attachments = []; - } else if (isProfileKeyUpdate) { - decrypted.body = null; - decrypted.attachments = []; - } else if (decrypted.flags !== 0) { - throw new Error('Unknown flags in message'); - } - - if (decrypted.group) { - decrypted.group.id = decrypted.group.id.toBinary(); - - switch (decrypted.group.type) { - case window.textsecure.protobuf.GroupContext.Type.UPDATE: - decrypted.body = null; - decrypted.attachments = []; - break; - case window.textsecure.protobuf.GroupContext.Type.QUIT: - decrypted.body = null; - decrypted.attachments = []; - break; - case window.textsecure.protobuf.GroupContext.Type.DELIVER: - decrypted.group.name = null; - decrypted.group.membersE164 = []; - decrypted.group.avatar = null; - break; - default: { - this.removeFromCache(envelope); - const err = new Error('Unknown group message type'); - err.warn = true; - throw err; - } - } - } - - const attachmentCount = (decrypted.attachments || []).length; - const ATTACHMENT_MAX = 32; - if (attachmentCount > ATTACHMENT_MAX) { - throw new Error( - `Too many attachments: ${attachmentCount} included in one message, max is ${ATTACHMENT_MAX}` - ); - } - - // Here we go from binary to string/base64 in all AttachmentPointer digest/key fields - - if ( - decrypted.group && - decrypted.group.type === - window.textsecure.protobuf.GroupContext.Type.UPDATE - ) { - if (decrypted.group.avatar) { - decrypted.group.avatar = this.cleanAttachment(decrypted.group.avatar); - } - } - - decrypted.attachments = (decrypted.attachments || []).map( - this.cleanAttachment.bind(this) - ); - decrypted.preview = (decrypted.preview || []).map(item => ({ - ...item, - date: this.cleanLinkPreviewDate(item.date), - ...(item.image ? { image: this.cleanAttachment(item.image) } : {}), - })); - decrypted.contact = (decrypted.contact || []).map(item => { - const { avatar } = item; - - if (!avatar || !avatar.avatar) { - return item; - } - - return { - ...item, - avatar: { - ...item.avatar, - avatar: this.cleanAttachment(item.avatar.avatar), - }, - }; - }); - - if (decrypted.quote && decrypted.quote.id) { - decrypted.quote.id = decrypted.quote.id.toNumber(); - } - - if (decrypted.quote) { - decrypted.quote.attachments = (decrypted.quote.attachments || []).map( - item => { - if (!item.thumbnail) { - return item; - } - - return { - ...item, - thumbnail: this.cleanAttachment(item.thumbnail), - }; - } - ); - } - - const { sticker } = decrypted; - if (sticker) { - if (sticker.packId) { - sticker.packId = sticker.packId.toString('hex'); - } - if (sticker.packKey) { - sticker.packKey = sticker.packKey.toString('base64'); - } - if (sticker.data) { - sticker.data = this.cleanAttachment(sticker.data); - } - } - - const { delete: del } = decrypted; - if (del) { - if (del.targetSentTimestamp) { - del.targetSentTimestamp = del.targetSentTimestamp.toNumber(); - } - } - - const { reaction } = decrypted; - if (reaction) { - if (reaction.targetTimestamp) { - reaction.targetTimestamp = reaction.targetTimestamp.toNumber(); - } - } - - return Promise.resolve(decrypted); - /* eslint-enable no-bitwise, no-param-reassign */ + async processDecrypted( + envelope: ProcessedEnvelope, + decrypted: Proto.IDataMessage + ): Promise { + return processDataMessage(decrypted, envelope.timestamp); } } export default class MessageReceiver { + private readonly inner: MessageReceiverInner; + constructor( oldUsername: string, username: string, @@ -2747,6 +2528,7 @@ export default class MessageReceiver { options: { serverTrustRoot: string; retryCached?: string; + socket?: WebSocket; } ) { const inner = new MessageReceiverInner( @@ -2756,35 +2538,149 @@ export default class MessageReceiver { signalingKey, options ); + this.inner = inner; - this.addEventListener = inner.addEventListener.bind(inner); this.close = inner.close.bind(inner); this.downloadAttachment = inner.downloadAttachment.bind(inner); this.getStatus = inner.getStatus.bind(inner); this.hasEmptied = inner.hasEmptied.bind(inner); - this.removeEventListener = inner.removeEventListener.bind(inner); this.stopProcessing = inner.stopProcessing.bind(inner); this.checkSocket = inner.checkSocket.bind(inner); this.unregisterBatchers = inner.unregisterBatchers.bind(inner); - inner.connect(); + inner.connect(options.socket); this.getProcessedCount = () => inner.processedCount; } - addEventListener: (name: string, handler: Function) => void; + public addEventListener( + name: 'reconnect', + handler: (ev: ReconnectEvent) => void + ): void; + + public addEventListener( + name: 'empty', + handler: (ev: EmptyEvent) => void + ): void; + + public addEventListener( + name: 'progress', + handler: (ev: ProgressEvent) => void + ): void; + + public addEventListener( + name: 'typing', + handler: (ev: TypingEvent) => void + ): void; + + public addEventListener( + name: 'error', + handler: (ev: ErrorEvent) => void + ): void; + + public addEventListener( + name: 'delivery', + handler: (ev: DeliveryEvent) => void + ): void; + + public addEventListener( + name: 'decryption-error', + handler: (ev: DecryptionErrorEvent) => void + ): void; + + public addEventListener(name: 'sent', handler: (ev: SentEvent) => void): void; + + public addEventListener( + name: 'profileKeyUpdate', + handler: (ev: ProfileKeyUpdateEvent) => void + ): void; + + public addEventListener( + name: 'message', + handler: (ev: MessageEvent) => void + ): void; + + public addEventListener( + name: 'retry-request', + handler: (ev: RetryRequestEvent) => void + ): void; + + public addEventListener(name: 'read', handler: (ev: ReadEvent) => void): void; + + public addEventListener( + name: 'configuration', + handler: (ev: ConfigurationEvent) => void + ): void; + + public addEventListener( + name: 'viewSync', + handler: (ev: ViewSyncEvent) => void + ): void; + + public addEventListener( + name: 'messageRequestResponse', + handler: (ev: MessageRequestResponseEvent) => void + ): void; + + public addEventListener( + name: 'fetchLatest', + handler: (ev: FetchLatestEvent) => void + ): void; + + public addEventListener(name: 'keys', handler: (ev: KeysEvent) => void): void; + + public addEventListener( + name: 'sticker-pack', + handler: (ev: StickerPackEvent) => void + ): void; + + public addEventListener( + name: 'verified', + handler: (ev: VerifiedEvent) => void + ): void; + + public addEventListener( + name: 'readSync', + handler: (ev: ReadSyncEvent) => void + ): void; + + public addEventListener( + name: 'contact', + handler: (ev: ContactEvent) => void + ): void; + + public addEventListener( + name: 'contactSync', + handler: (ev: ContactSyncEvent) => void + ): void; + + public addEventListener( + name: 'group', + handler: (ev: GroupEvent) => void + ): void; + + public addEventListener( + name: 'groupSync', + handler: (ev: GroupSyncEvent) => void + ): void; + + public addEventListener(name: string, handler: EventHandler): void { + return this.inner.addEventListener(name, handler); + } + + public removeEventListener(name: string, handler: EventHandler): void { + return this.inner.removeEventListener(name, handler); + } close: () => Promise; downloadAttachment: ( - attachment: AttachmentPointerClass + attachment: ProcessedAttachment ) => Promise; getStatus: () => SocketStatus; hasEmptied: () => boolean; - removeEventListener: (name: string, handler: Function) => void; - stopProcessing: () => Promise; unregisterBatchers: () => void; diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index 033201e98..17f2bd682 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -23,7 +23,6 @@ import { } from '@signalapp/signal-client'; import { WebAPIType } from './WebAPI'; -import { ContentClass, DataMessageClass } from '../textsecure.d'; import { CallbackResultType, SendMetadataType, @@ -42,6 +41,7 @@ import { Sessions, IdentityKeys } from '../LibSignalStores'; import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup'; import { getKeysForIdentifier } from './getKeysForIdentifier'; +import { SignalService as Proto } from '../protobuf'; export const enum SenderCertificateMode { WithE164, @@ -72,13 +72,13 @@ type OutgoingMessageOptionsType = SendOptionsType & { function ciphertextMessageTypeToEnvelopeType(type: number) { if (type === CiphertextMessageType.PreKey) { - return window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE; + return Proto.Envelope.Type.PREKEY_BUNDLE; } if (type === CiphertextMessageType.Whisper) { - return window.textsecure.protobuf.Envelope.Type.CIPHERTEXT; + return Proto.Envelope.Type.CIPHERTEXT; } if (type === CiphertextMessageType.Plaintext) { - return window.textsecure.protobuf.Envelope.Type.PLAINTEXT_CONTENT; + return Proto.Envelope.Type.PLAINTEXT_CONTENT; } throw new Error( `ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}` @@ -96,11 +96,11 @@ function getPaddedMessageLength(messageLength: number): number { return messagePartCount * 160; } -export function padMessage(messageBuffer: ArrayBuffer): Uint8Array { +export function padMessage(messageBuffer: Uint8Array): Uint8Array { const plaintext = new Uint8Array( getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 ); - plaintext.set(new Uint8Array(messageBuffer)); + plaintext.set(messageBuffer); plaintext[messageBuffer.byteLength] = 0x80; return plaintext; @@ -113,7 +113,7 @@ export default class OutgoingMessage { identifiers: Array; - message: ContentClass | PlaintextContent; + message: Proto.Content | PlaintextContent; callback: (result: CallbackResultType) => void; @@ -141,14 +141,14 @@ export default class OutgoingMessage { server: WebAPIType, timestamp: number, identifiers: Array, - message: ContentClass | DataMessageClass | PlaintextContent, + message: Proto.Content | Proto.DataMessage | PlaintextContent, contentHint: number, groupId: string | undefined, callback: (result: CallbackResultType) => void, options: OutgoingMessageOptionsType = {} ) { - if (message instanceof window.textsecure.protobuf.DataMessage) { - const content = new window.textsecure.protobuf.Content(); + if (message instanceof Proto.DataMessage) { + const content = new Proto.Content(); content.dataMessage = message; // eslint-disable-next-line no-param-reassign this.message = content; @@ -304,8 +304,8 @@ export default class OutgoingMessage { if (!this.plaintext) { const { message } = this; - if (message instanceof window.textsecure.protobuf.Content) { - this.plaintext = padMessage(message.toArrayBuffer()); + if (message instanceof Proto.Content) { + this.plaintext = padMessage(Proto.Content.encode(message).finish()); } else { this.plaintext = message.serialize(); } @@ -324,7 +324,7 @@ export default class OutgoingMessage { }): Promise { const { message } = this; - if (message instanceof window.textsecure.protobuf.Content) { + if (message instanceof Proto.Content) { return signalEncrypt( Buffer.from(this.getPlaintext()), protocolAddress, @@ -421,8 +421,7 @@ export default class OutgoingMessage { ); return { - type: - window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER, + type: Proto.Envelope.Type.UNIDENTIFIED_SENDER, destinationDeviceId, destinationRegistrationId, content: buffer.toString('base64'), diff --git a/ts/textsecure/ProvisioningCipher.ts b/ts/textsecure/ProvisioningCipher.ts index 8cac39bb2..1831a9d27 100644 --- a/ts/textsecure/ProvisioningCipher.ts +++ b/ts/textsecure/ProvisioningCipher.ts @@ -14,7 +14,8 @@ import { } from '../Crypto'; import { calculateAgreement, createKeyPair, generateKeyPair } from '../Curve'; import { SignalService as Proto } from '../protobuf'; -import { assert } from '../util/assert'; +import { strictAssert } from '../util/assert'; +import { normalizeUuid } from '../util/normalizeUuid'; // TODO: remove once we move away from ArrayBuffers const FIXMEU8 = Uint8Array; @@ -35,7 +36,7 @@ class ProvisioningCipherInner { async decrypt( provisionEnvelope: Proto.ProvisionEnvelope ): Promise { - assert( + strictAssert( provisionEnvelope.publicKey && provisionEnvelope.body, 'Missing required fields in ProvisionEnvelope' ); @@ -79,19 +80,17 @@ class ProvisioningCipherInner { new FIXMEU8(plaintext) ); const privKey = provisionMessage.identityKeyPrivate; - assert(privKey, 'Missing identityKeyPrivate in ProvisionMessage'); + strictAssert(privKey, 'Missing identityKeyPrivate in ProvisionMessage'); const keyPair = createKeyPair(typedArrayToArrayBuffer(privKey)); - window.normalizeUuids( - provisionMessage, - ['uuid'], - 'ProvisioningCipher.decrypt' - ); + + const { uuid } = provisionMessage; + strictAssert(uuid, 'Missing uuid in provisioning message'); const ret: ProvisionDecryptResult = { identityKeyPair: keyPair, number: provisionMessage.number, - uuid: provisionMessage.uuid, + uuid: normalizeUuid(uuid, 'ProvisionMessage.uuid'), provisioningCode: provisionMessage.provisioningCode, userAgent: provisionMessage.userAgent, readReceipts: provisionMessage.readReceipts, diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index acf89a3d1..4917c0834 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -30,22 +30,16 @@ import { import createTaskWithTimeout from './TaskWithTimeout'; import OutgoingMessage, { SerializedCertificateType } from './OutgoingMessage'; import Crypto from './Crypto'; +import * as Bytes from '../Bytes'; import { - base64ToArrayBuffer, concatenateBytes, getRandomBytes, getZeroes, - hexToArrayBuffer, typedArrayToArrayBuffer, } from '../Crypto'; import { - AttachmentPointerClass, - CallingMessageClass, - ContentClass, - DataMessageClass, StorageServiceCallOptionsType, StorageServiceCredentials, - SyncMessageClass, } from '../textsecure.d'; import { MessageError, SignedPreKeyRotationError } from './Errors'; import { BodyRangesType } from '../types/Util'; @@ -56,18 +50,6 @@ import { import { concat } from '../util/iterables'; import { SignalService as Proto } from '../protobuf'; -function stringToArrayBuffer(str: string): ArrayBuffer { - if (typeof str !== 'string') { - throw new Error('Passed non-string to stringToArrayBuffer'); - } - const res = new ArrayBuffer(str.length); - const uint = new Uint8Array(res); - for (let i = 0; i < str.length; i += 1) { - uint[i] = str.charCodeAt(i); - } - return res; -} - export type SendMetadataType = { [identifier: string]: { accessKey: string; @@ -101,7 +83,7 @@ type PreviewType = { type QuoteAttachmentType = { thumbnail?: AttachmentType; - attachmentPointer?: AttachmentPointerClass; + attachmentPointer?: Proto.IAttachmentPointer; }; export type GroupV2InfoType = { @@ -130,7 +112,7 @@ export type AttachmentType = { height: number; caption: string; - attachmentPointer?: AttachmentPointerClass; + attachmentPointer?: Proto.IAttachmentPointer; blurHash?: string; }; @@ -174,6 +156,9 @@ export type GroupSendOptionsType = { groupCallUpdate?: GroupCallUpdateType; }; +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + class Message { attachments: Array; @@ -219,7 +204,7 @@ class Message { dataMessage: any; - attachmentPointers?: Array; + attachmentPointers: Array = []; deletedForEveryoneTimestamp?: number; @@ -301,17 +286,14 @@ class Message { } isEndSession() { - return ( - (this.flags || 0) & - window.textsecure.protobuf.DataMessage.Flags.END_SESSION - ); + return (this.flags || 0) & Proto.DataMessage.Flags.END_SESSION; } - toProto(): DataMessageClass { - if (this.dataMessage instanceof window.textsecure.protobuf.DataMessage) { + toProto(): Proto.DataMessage { + if (this.dataMessage instanceof Proto.DataMessage) { return this.dataMessage; } - const proto = new window.textsecure.protobuf.DataMessage(); + const proto = new Proto.DataMessage(); proto.timestamp = this.timestamp; proto.attachments = this.attachmentPointers; @@ -330,19 +312,19 @@ class Message { proto.flags = this.flags; } if (this.groupV2) { - proto.groupV2 = new window.textsecure.protobuf.GroupContextV2(); + proto.groupV2 = new Proto.GroupContextV2(); proto.groupV2.masterKey = this.groupV2.masterKey; proto.groupV2.revision = this.groupV2.revision; proto.groupV2.groupChange = this.groupV2.groupChange || null; } else if (this.group) { - proto.group = new window.textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(this.group.id); + proto.group = new Proto.GroupContext(); + proto.group.id = Bytes.fromString(this.group.id); proto.group.type = this.group.type; } if (this.sticker) { - proto.sticker = new window.textsecure.protobuf.DataMessage.Sticker(); - proto.sticker.packId = hexToArrayBuffer(this.sticker.packId); - proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey); + proto.sticker = new Proto.DataMessage.Sticker(); + proto.sticker.packId = Bytes.fromHex(this.sticker.packId); + proto.sticker.packKey = Bytes.fromBase64(this.sticker.packKey); proto.sticker.stickerId = this.sticker.stickerId; if (this.sticker.attachmentPointer) { @@ -350,7 +332,7 @@ class Message { } } if (this.reaction) { - proto.reaction = new window.textsecure.protobuf.DataMessage.Reaction(); + proto.reaction = new Proto.DataMessage.Reaction(); proto.reaction.emoji = this.reaction.emoji || null; proto.reaction.remove = this.reaction.remove || false; proto.reaction.targetAuthorUuid = this.reaction.targetAuthorUuid || null; @@ -359,7 +341,7 @@ class Message { if (Array.isArray(this.preview)) { proto.preview = this.preview.map(preview => { - const item = new window.textsecure.protobuf.DataMessage.Preview(); + const item = new Proto.DataMessage.Preview(); item.title = preview.title; item.url = preview.url; item.description = preview.description || null; @@ -369,8 +351,8 @@ class Message { }); } if (this.quote) { - const { QuotedAttachment } = window.textsecure.protobuf.DataMessage.Quote; - const { BodyRange, Quote } = window.textsecure.protobuf.DataMessage; + const { QuotedAttachment } = Proto.DataMessage.Quote; + const { BodyRange, Quote } = Proto.DataMessage; proto.quote = new Quote(); const { quote } = proto; @@ -396,24 +378,26 @@ class Message { const bodyRange = new BodyRange(); bodyRange.start = range.start; bodyRange.length = range.length; - bodyRange.mentionUuid = range.mentionUuid; + if (range.mentionUuid !== undefined) { + bodyRange.mentionUuid = range.mentionUuid; + } return bodyRange; }); if ( quote.bodyRanges.length && (!proto.requiredProtocolVersion || proto.requiredProtocolVersion < - window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS) + Proto.DataMessage.ProtocolVersion.MENTIONS) ) { proto.requiredProtocolVersion = - window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS; + Proto.DataMessage.ProtocolVersion.MENTIONS; } } if (this.expireTimer) { proto.expireTimer = this.expireTimer; } if (this.profileKey) { - proto.profileKey = this.profileKey; + proto.profileKey = new FIXMEU8(this.profileKey); } if (this.deletedForEveryoneTimestamp) { proto.delete = { @@ -422,7 +406,7 @@ class Message { } if (this.mentions) { proto.requiredProtocolVersion = - window.textsecure.protobuf.DataMessage.ProtocolVersion.MENTIONS; + Proto.DataMessage.ProtocolVersion.MENTIONS; proto.bodyRanges = this.mentions.map( ({ start, length, mentionUuid }) => ({ start, @@ -433,7 +417,7 @@ class Message { } if (this.groupCallUpdate) { - const { GroupCallUpdate } = window.textsecure.protobuf.DataMessage; + const { GroupCallUpdate } = Proto.DataMessage; const groupCallUpdate = new GroupCallUpdate(); groupCallUpdate.eraId = this.groupCallUpdate.eraId; @@ -446,7 +430,9 @@ class Message { } toArrayBuffer() { - return this.toProto().toArrayBuffer(); + return typedArrayToArrayBuffer( + Proto.DataMessage.encode(this.toProto()).finish() + ); } } @@ -492,13 +478,13 @@ export default class MessageSender { ); } - getRandomPadding(): ArrayBuffer { + getRandomPadding(): Uint8Array { // Generate a random int from 1 and 512 const buffer = getRandomBytes(2); const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1; // Generate a random padding buffer of the chosen size - return getRandomBytes(paddingLength); + return new FIXMEU8(getRandomBytes(paddingLength)); } getPaddedAttachment(data: ArrayBuffer): ArrayBuffer { @@ -511,10 +497,11 @@ export default class MessageSender { async makeAttachmentPointer( attachment: AttachmentType - ): Promise { - if (typeof attachment !== 'object' || attachment == null) { - return Promise.resolve(undefined); - } + ): Promise { + assert( + typeof attachment === 'object' && attachment !== null, + 'Got null attachment in `makeAttachmentPointer`' + ); const { data, size } = attachment; if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) { @@ -535,12 +522,12 @@ export default class MessageSender { const result = await Crypto.encryptAttachment(padded, key, iv); const id = await this.server.putAttachment(result.ciphertext); - const proto = new window.textsecure.protobuf.AttachmentPointer(); + const proto = new Proto.AttachmentPointer(); proto.cdnId = id; proto.contentType = attachment.contentType; - proto.key = key; + proto.key = new FIXMEU8(key); proto.size = attachment.size; - proto.digest = result.digest; + proto.digest = new FIXMEU8(result.digest); if (attachment.fileName) { proto.fileName = attachment.fileName; @@ -657,11 +644,11 @@ export default class MessageSender { return message.toArrayBuffer(); } - async getContentMessage(options: MessageOptionsType): Promise { + async getContentMessage(options: MessageOptionsType): Promise { const message = await this.getHydratedMessage(options); const dataMessage = message.toProto(); - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.dataMessage = dataMessage; return contentMessage; @@ -685,8 +672,8 @@ export default class MessageSender { groupMembers: Array; isTyping: boolean; timestamp?: number; - }): ContentClass { - const ACTION_ENUM = window.textsecure.protobuf.TypingMessage.Action; + }): Proto.Content { + const ACTION_ENUM = Proto.TypingMessage.Action; const { recipientId, groupId, isTyping, timestamp } = options; if (!recipientId && !groupId) { @@ -698,12 +685,14 @@ export default class MessageSender { const finalTimestamp = timestamp || Date.now(); const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED; - const typingMessage = new window.textsecure.protobuf.TypingMessage(); - typingMessage.groupId = groupId || null; + const typingMessage = new Proto.TypingMessage(); + if (groupId) { + typingMessage.groupId = new FIXMEU8(groupId); + } typingMessage.action = action; typingMessage.timestamp = finalTimestamp; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.typingMessage = typingMessage; return contentMessage; @@ -767,7 +756,7 @@ export default class MessageSender { group: groupV1 ? { id: groupV1.id, - type: window.textsecure.protobuf.GroupContext.Type.DELIVER, + type: Proto.GroupContext.Type.DELIVER, } : undefined, mentions, @@ -781,8 +770,8 @@ export default class MessageSender { }; } - createSyncMessage(): SyncMessageClass { - const syncMessage = new window.textsecure.protobuf.SyncMessage(); + createSyncMessage(): Proto.SyncMessage { + const syncMessage = new Proto.SyncMessage(); syncMessage.padding = this.getRandomPadding(); @@ -843,7 +832,7 @@ export default class MessageSender { }: { timestamp: number; recipients: Array; - proto: ContentClass | DataMessageClass | PlaintextContent; + proto: Proto.Content | Proto.DataMessage | PlaintextContent; contentHint: number; groupId: string | undefined; callback: (result: CallbackResultType) => void; @@ -885,7 +874,7 @@ export default class MessageSender { }: { timestamp: number; recipients: Array; - proto: ContentClass | DataMessageClass | PlaintextContent; + proto: Proto.Content | Proto.DataMessage | PlaintextContent; contentHint: number; groupId: string | undefined; options?: SendOptionsType; @@ -920,7 +909,7 @@ export default class MessageSender { options, }: { identifier: string | undefined; - proto: DataMessageClass | ContentClass | PlaintextContent; + proto: Proto.DataMessage | Proto.Content | PlaintextContent; timestamp: number; contentHint: number; options?: SendOptionsType; @@ -1030,10 +1019,10 @@ export default class MessageSender { return Promise.resolve(); } - const dataMessage = window.textsecure.protobuf.DataMessage.decode( - encodedDataMessage + const dataMessage = Proto.DataMessage.decode( + new FIXMEU8(encodedDataMessage) ); - const sentMessage = new window.textsecure.protobuf.SyncMessage.Sent(); + const sentMessage = new Proto.SyncMessage.Sent(); sentMessage.timestamp = timestamp; sentMessage.message = dataMessage; if (destination) { @@ -1063,13 +1052,17 @@ export default class MessageSender { // number we sent to. if (sentTo && sentTo.length) { sentMessage.unidentifiedStatus = sentTo.map(identifier => { - const status = new window.textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus(); + const status = new Proto.SyncMessage.Sent.UnidentifiedDeliveryStatus(); const conv = window.ConversationController.get(identifier); - if (conv && conv.get('e164')) { - status.destination = conv.get('e164'); - } - if (conv && conv.get('uuid')) { - status.destinationUuid = conv.get('uuid'); + if (conv) { + const e164 = conv.get('e164'); + if (e164) { + status.destination = e164; + } + const uuid = conv.get('uuid'); + if (uuid) { + status.destinationUuid = uuid; + } } status.unidentified = Boolean(unidentifiedLookup[identifier]); return status; @@ -1078,12 +1071,10 @@ export default class MessageSender { const syncMessage = this.createSyncMessage(); syncMessage.sent = sentMessage; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1101,17 +1092,14 @@ export default class MessageSender { const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); if (myDevice !== 1) { - const request = new window.textsecure.protobuf.SyncMessage.Request(); - request.type = - window.textsecure.protobuf.SyncMessage.Request.Type.BLOCKED; + const request = new Proto.SyncMessage.Request(); + request.type = Proto.SyncMessage.Request.Type.BLOCKED; const syncMessage = this.createSyncMessage(); syncMessage.request = request; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1132,17 +1120,14 @@ export default class MessageSender { const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); if (myDevice !== 1) { - const request = new window.textsecure.protobuf.SyncMessage.Request(); - request.type = - window.textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION; + const request = new Proto.SyncMessage.Request(); + request.type = Proto.SyncMessage.Request.Type.CONFIGURATION; const syncMessage = this.createSyncMessage(); syncMessage.request = request; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1163,16 +1148,14 @@ export default class MessageSender { const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); if (myDevice !== 1) { - const request = new window.textsecure.protobuf.SyncMessage.Request(); - request.type = window.textsecure.protobuf.SyncMessage.Request.Type.GROUPS; + const request = new Proto.SyncMessage.Request(); + request.type = Proto.SyncMessage.Request.Type.GROUPS; const syncMessage = this.createSyncMessage(); syncMessage.request = request; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1194,17 +1177,14 @@ export default class MessageSender { const myDevice = window.textsecure.storage.user.getDeviceId(); if (myDevice !== 1) { - const request = new window.textsecure.protobuf.SyncMessage.Request(); - request.type = - window.textsecure.protobuf.SyncMessage.Request.Type.CONTACTS; + const request = new Proto.SyncMessage.Request(); + request.type = Proto.SyncMessage.Request.Type.CONTACTS; const syncMessage = this.createSyncMessage(); syncMessage.request = request; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1229,18 +1209,15 @@ export default class MessageSender { return; } - const fetchLatest = new window.textsecure.protobuf.SyncMessage.FetchLatest(); - fetchLatest.type = - window.textsecure.protobuf.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST; + const fetchLatest = new Proto.SyncMessage.FetchLatest(); + fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST; const syncMessage = this.createSyncMessage(); syncMessage.fetchLatest = fetchLatest; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; await this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1262,17 +1239,15 @@ export default class MessageSender { return; } - const request = new window.textsecure.protobuf.SyncMessage.Request(); - request.type = window.textsecure.protobuf.SyncMessage.Request.Type.KEYS; + const request = new Proto.SyncMessage.Request(); + request.type = Proto.SyncMessage.Request.Type.KEYS; const syncMessage = this.createSyncMessage(); syncMessage.request = request; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; await this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1295,25 +1270,19 @@ export default class MessageSender { const myUuid = window.textsecure.storage.user.getUuid(); const myDevice = window.textsecure.storage.user.getDeviceId(); if (myDevice === 1) { - return Promise.resolve(); + return; } - const syncMessage = this.createSyncMessage(); syncMessage.read = []; for (let i = 0; i < reads.length; i += 1) { - const read = new window.textsecure.protobuf.SyncMessage.Read(); - read.timestamp = reads[i].timestamp; - read.sender = reads[i].senderE164 || null; - read.senderUuid = reads[i].senderUuid || null; + const proto = new Proto.SyncMessage.Read(reads[i]); - syncMessage.read.push(read); + syncMessage.read.push(proto); } - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1339,18 +1308,18 @@ export default class MessageSender { const syncMessage = this.createSyncMessage(); - const viewOnceOpen = new window.textsecure.protobuf.SyncMessage.ViewOnceOpen(); - viewOnceOpen.sender = sender || null; - viewOnceOpen.senderUuid = senderUuid || null; - viewOnceOpen.timestamp = timestamp || null; + const viewOnceOpen = new Proto.SyncMessage.ViewOnceOpen(); + if (sender !== undefined) { + viewOnceOpen.sender = sender; + } + viewOnceOpen.senderUuid = senderUuid; + viewOnceOpen.timestamp = timestamp; syncMessage.viewOnceOpen = viewOnceOpen; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1379,19 +1348,23 @@ export default class MessageSender { const syncMessage = this.createSyncMessage(); - const response = new window.textsecure.protobuf.SyncMessage.MessageRequestResponse(); - response.threadE164 = responseArgs.threadE164 || null; - response.threadUuid = responseArgs.threadUuid || null; - response.groupId = responseArgs.groupId || null; + const response = new Proto.SyncMessage.MessageRequestResponse(); + if (responseArgs.threadE164 !== undefined) { + response.threadE164 = responseArgs.threadE164; + } + if (responseArgs.threadUuid !== undefined) { + response.threadUuid = responseArgs.threadUuid; + } + if (responseArgs.groupId) { + response.groupId = new FIXMEU8(responseArgs.groupId); + } response.type = responseArgs.type; syncMessage.messageRequestResponse = response; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1417,15 +1390,14 @@ export default class MessageSender { const myNumber = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); - const ENUM = - window.textsecure.protobuf.SyncMessage.StickerPackOperation.Type; + const ENUM = Proto.SyncMessage.StickerPackOperation.Type; const packOperations = operations.map(item => { const { packId, packKey, installed } = item; - const operation = new window.textsecure.protobuf.SyncMessage.StickerPackOperation(); - operation.packId = hexToArrayBuffer(packId); - operation.packKey = base64ToArrayBuffer(packKey); + const operation = new Proto.SyncMessage.StickerPackOperation(); + operation.packId = Bytes.fromHex(packId); + operation.packKey = Bytes.fromBase64(packKey); operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE; return operation; @@ -1434,12 +1406,10 @@ export default class MessageSender { const syncMessage = this.createSyncMessage(); syncMessage.stickerPackOperation = packOperations; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1476,7 +1446,7 @@ export default class MessageSender { ); return promise.then(async () => { - const verified = new window.textsecure.protobuf.Verified(); + const verified = new Proto.Verified(); verified.state = state; if (destinationE164) { verified.destination = destinationE164; @@ -1484,18 +1454,16 @@ export default class MessageSender { if (destinationUuid) { verified.destinationUuid = destinationUuid; } - verified.identityKey = identityKey; + verified.identityKey = new FIXMEU8(identityKey); verified.nullMessage = padding; const syncMessage = this.createSyncMessage(); syncMessage.verified = verified; - const secondMessage = new window.textsecure.protobuf.Content(); + const secondMessage = new Proto.Content(); secondMessage.syncMessage = syncMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; await this.sendIndividualProto({ identifier: myUuid || myNumber, @@ -1515,21 +1483,19 @@ export default class MessageSender { options: SendOptionsType, groupId?: string ): Promise { - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendMessage({ messageOptions: { recipients, timestamp: Date.now(), profileKey, - flags: window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE, + flags: Proto.DataMessage.Flags.PROFILE_KEY_UPDATE, ...(groupId ? { group: { id: groupId, - type: window.textsecure.protobuf.GroupContext.Type.DELIVER, + type: Proto.GroupContext.Type.DELIVER, }, } : {}), @@ -1542,18 +1508,16 @@ export default class MessageSender { async sendCallingMessage( recipientId: string, - callingMessage: CallingMessageClass, + callingMessage: Proto.ICallingMessage, options?: SendOptionsType ): Promise { const recipients = [recipientId]; const finalTimestamp = Date.now(); - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.callingMessage = callingMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; await this.sendMessageProtoAndWait({ timestamp: finalTimestamp, @@ -1583,17 +1547,14 @@ export default class MessageSender { return Promise.resolve(); } - const receiptMessage = new window.textsecure.protobuf.ReceiptMessage(); - receiptMessage.type = - window.textsecure.protobuf.ReceiptMessage.Type.DELIVERY; + const receiptMessage = new Proto.ReceiptMessage(); + receiptMessage.type = Proto.ReceiptMessage.Type.DELIVERY; receiptMessage.timestamp = timestamps; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.receiptMessage = receiptMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: uuid || e164, @@ -1615,16 +1576,14 @@ export default class MessageSender { timestamps: Array; options?: SendOptionsType; }): Promise { - const receiptMessage = new window.textsecure.protobuf.ReceiptMessage(); - receiptMessage.type = window.textsecure.protobuf.ReceiptMessage.Type.READ; + const receiptMessage = new Proto.ReceiptMessage(); + receiptMessage.type = Proto.ReceiptMessage.Type.READ; receiptMessage.timestamp = timestamps; - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.receiptMessage = receiptMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendIndividualProto({ identifier: senderUuid || senderE164, @@ -1640,10 +1599,10 @@ export default class MessageSender { uuid, e164, padding, - }: { uuid?: string; e164?: string; padding?: ArrayBuffer }, + }: { uuid?: string; e164?: string; padding?: Uint8Array }, options?: SendOptionsType ): Promise { - const nullMessage = new window.textsecure.protobuf.NullMessage(); + const nullMessage = new Proto.NullMessage(); const identifier = uuid || e164; if (!identifier) { @@ -1652,12 +1611,10 @@ export default class MessageSender { nullMessage.padding = padding || this.getRandomPadding(); - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); contentMessage.nullMessage = nullMessage; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; // We want the NullMessage to look like a normal outgoing message const timestamp = Date.now(); @@ -1679,9 +1636,9 @@ export default class MessageSender { CallbackResultType | void | Array> > { window.log.info('resetSession: start'); - const proto = new window.textsecure.protobuf.DataMessage(); + const proto = new Proto.DataMessage(); proto.body = 'TERMINATE'; - proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION; + proto.flags = Proto.DataMessage.Flags.END_SESSION; proto.timestamp = timestamp; const identifier = uuid || e164; @@ -1691,9 +1648,7 @@ export default class MessageSender { throw error; }; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const sendToContactPromise = window.textsecure.storage.protocol .archiveAllSessions(identifier) @@ -1723,7 +1678,9 @@ export default class MessageSender { return sendToContactPromise; } - const buffer = proto.toArrayBuffer(); + const buffer = typedArrayToArrayBuffer( + Proto.DataMessage.encode(proto).finish() + ); const sendSyncPromise = this.sendSyncMessage({ encodedDataMessage: buffer, timestamp, @@ -1745,9 +1702,7 @@ export default class MessageSender { profileKey?: ArrayBuffer, options?: SendOptionsType ): Promise { - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendMessage({ messageOptions: { @@ -1755,8 +1710,7 @@ export default class MessageSender { timestamp, expireTimer, profileKey, - flags: - window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, }, contentHint: ContentHint.DEFAULT, groupId: undefined, @@ -1773,9 +1727,7 @@ export default class MessageSender { plaintext: PlaintextContent; uuid: string; }): Promise { - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendMessageProtoAndWait({ timestamp: Date.now(), @@ -1799,13 +1751,17 @@ export default class MessageSender { options, }: { recipients: Array; - proto: ContentClass; + proto: Proto.Content; timestamp: number; contentHint: number; groupId: string | undefined; options?: SendOptionsType; }): Promise { - const dataMessage = proto.dataMessage?.toArrayBuffer(); + const dataMessage = proto.dataMessage + ? typedArrayToArrayBuffer( + Proto.DataMessage.encode(proto.dataMessage).finish() + ) + : undefined; const myE164 = window.textsecure.storage.user.getNumber(); const myUuid = window.textsecure.storage.user.getUuid(); @@ -1887,14 +1843,12 @@ export default class MessageSender { }, options?: SendOptionsType ): Promise { - const contentMessage = new window.textsecure.protobuf.Content(); + const contentMessage = new Proto.Content(); const senderKeyDistributionMessage = await this.getSenderKeyDistributionMessage( distributionId ); - contentMessage.senderKeyDistributionMessage = window.dcodeIO.ByteBuffer.wrap( - typedArrayToArrayBuffer(senderKeyDistributionMessage.serialize()) - ); + contentMessage.senderKeyDistributionMessage = senderKeyDistributionMessage.serialize(); return this.sendGroupProto({ recipients: identifiers, @@ -1913,14 +1867,16 @@ export default class MessageSender { groupIdentifiers: Array, options?: SendOptionsType ): Promise { - const proto = new window.textsecure.protobuf.DataMessage(); - proto.group = new window.textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = window.textsecure.protobuf.GroupContext.Type.QUIT; + const proto = new Proto.Content({ + dataMessage: { + group: { + id: Bytes.fromString(groupId), + type: Proto.GroupContext.Type.QUIT, + }, + }, + }); - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendGroupProto({ recipients: groupIdentifiers, proto, @@ -1949,11 +1905,10 @@ export default class MessageSender { timestamp, expireTimer, profileKey, - flags: - window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, group: { id: groupId, - type: window.textsecure.protobuf.GroupContext.Type.DELIVER, + type: Proto.GroupContext.Type.DELIVER, }, }; @@ -1967,9 +1922,7 @@ export default class MessageSender { }); } - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; return this.sendMessage({ messageOptions, contentHint: ContentHint.DEFAULT, diff --git a/ts/textsecure/SyncRequest.ts b/ts/textsecure/SyncRequest.ts index 527a5faee..66f8ee980 100644 --- a/ts/textsecure/SyncRequest.ts +++ b/ts/textsecure/SyncRequest.ts @@ -6,8 +6,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable max-classes-per-file */ -import EventTarget from './EventTarget'; +import EventTarget, { EventHandler } from './EventTarget'; import MessageReceiver from './MessageReceiver'; +import { ContactSyncEvent, GroupSyncEvent } from './messageReceiverEvents'; import MessageSender from './SendMessage'; import { assert } from '../util/assert'; @@ -20,9 +21,9 @@ class SyncRequestInner extends EventTarget { timeout: any; - oncontact: Function; + oncontact: (event: ContactSyncEvent) => void; - ongroup: Function; + ongroup: (event: GroupSyncEvent) => void; timeoutMillis: number; @@ -43,10 +44,10 @@ class SyncRequestInner extends EventTarget { } this.oncontact = this.onContactSyncComplete.bind(this); - receiver.addEventListener('contactsync', this.oncontact); + receiver.addEventListener('contactSync', this.oncontact); this.ongroup = this.onGroupSyncComplete.bind(this); - receiver.addEventListener('groupsync', this.ongroup); + receiver.addEventListener('groupSync', this.ongroup); this.timeoutMillis = timeoutMillis || 60000; } @@ -126,9 +127,15 @@ class SyncRequestInner extends EventTarget { export default class SyncRequest { private inner: SyncRequestInner; - addEventListener: (name: string, handler: Function) => void; + addEventListener: ( + name: 'success' | 'timeout', + handler: EventHandler + ) => void; - removeEventListener: (name: string, handler: Function) => void; + removeEventListener: ( + name: 'success' | 'timeout', + handler: EventHandler + ) => void; constructor( sender: MessageSender, diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index a13a9a115..0662fcf41 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -1,6 +1,8 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { SignalService as Proto } from '../protobuf'; + export { IdentityKeyType, PreKeyType, @@ -56,3 +58,152 @@ export type OuterSignedPrekeyType = { }; export type SessionResetsType = Record; + +export type ProcessedEnvelope = Readonly<{ + id: string; + receivedAtCounter: number; + receivedAtDate: number; + messageAgeSec: number; + + // Mostly from Proto.Envelope except for null/undefined + type: Proto.Envelope.Type; + source?: string; + sourceUuid?: string; + sourceDevice?: number; + timestamp: number; + legacyMessage?: Uint8Array; + content?: Uint8Array; + serverGuid: string; + serverTimestamp: number; +}>; + +export type ProcessedAttachment = { + cdnId?: string; + cdnKey?: string; + digest?: string; + contentType?: string; + key?: string; + size?: number; + fileName?: string; + flags?: number; + width?: number; + height?: number; + caption?: string; + blurHash?: string; + cdnNumber?: number; +}; + +export type ProcessedGroupContext = { + id: string; + type: Proto.GroupContext.Type; + name?: string; + membersE164: ReadonlyArray; + avatar?: ProcessedAttachment; + + // Computed fields + derivedGroupV2Id: string; +}; + +export type ProcessedGroupV2Context = { + masterKey: string; + revision?: number; + groupChange?: string; + + // Computed fields + id: string; + secretParams: string; + publicParams: string; +}; + +export type ProcessedQuoteAttachment = { + contentType?: string; + fileName?: string; + thumbnail?: ProcessedAttachment; +}; + +export type ProcessedQuote = { + id?: number; + authorUuid?: string; + text?: string; + attachments: ReadonlyArray; + bodyRanges: ReadonlyArray; +}; + +export type ProcessedAvatar = { + avatar?: ProcessedAttachment; + isProfile: boolean; +}; + +export type ProcessedContact = Omit & { + avatar?: ProcessedAvatar; +}; + +export type ProcessedPreview = { + url?: string; + title?: string; + image?: ProcessedAttachment; + description?: string; + date?: number; +}; + +export type ProcessedSticker = { + packId?: string; + packKey?: string; + stickerId?: number; + data?: ProcessedAttachment; +}; + +export type ProcessedReaction = { + emoji?: string; + remove: boolean; + targetAuthorUuid?: string; + targetTimestamp?: number; +}; + +export type ProcessedDelete = { + targetSentTimestamp?: number; +}; + +export type ProcessedBodyRange = Proto.DataMessage.IBodyRange; + +export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate; + +export type ProcessedDataMessage = { + body?: string; + attachments: ReadonlyArray; + group?: ProcessedGroupContext; + groupV2?: ProcessedGroupV2Context; + flags: number; + expireTimer: number; + profileKey?: string; + timestamp: number; + quote?: ProcessedQuote; + contact?: ReadonlyArray; + preview?: ReadonlyArray; + sticker?: ProcessedSticker; + requiredProtocolVersion?: number; + isViewOnce: boolean; + reaction?: ProcessedReaction; + delete?: ProcessedDelete; + bodyRanges?: ReadonlyArray; + groupCallUpdate?: ProcessedGroupCallUpdate; +}; + +export type ProcessedUnidentifiedDeliveryStatus = Omit< + Proto.SyncMessage.Sent.IUnidentifiedDeliveryStatus, + 'destinationUuid' +> & { + destinationUuid?: string; +}; + +export type ProcessedSent = Omit< + Proto.SyncMessage.ISent, + 'destinationId' | 'unidentifiedStatus' +> & { + destinationId?: string; + unidentifiedStatus?: Array; +}; + +export type ProcessedSyncMessage = Omit & { + sent?: ProcessedSent; +}; diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 4003fc3fa..7289054e8 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -33,7 +33,7 @@ import { Long } from '../window.d'; import { assert } from '../util/assert'; import { getUserAgent } from '../util/getUserAgent'; import { toWebSafeBase64 } from '../util/webSafeBase64'; -import { isPackIdValid, redactPackId } from '../../js/modules/stickers'; +import { isPackIdValid, redactPackId } from '../types/Stickers'; import { arrayBufferToBase64, base64ToArrayBuffer, @@ -53,7 +53,6 @@ import { calculateAgreement, generateKeyPair } from '../Curve'; import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch'; import { - AvatarUploadAttributesClass, StorageServiceCallOptionsType, StorageServiceCredentials, } from '../textsecure.d'; @@ -2161,7 +2160,7 @@ export function initialize({ return Proto.GroupExternalCredential.decode(new FIXMEU8(response)); } - function verifyAttributes(attributes: AvatarUploadAttributesClass) { + function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) { const { key, credential, @@ -2213,8 +2212,8 @@ export function initialize({ responseType: 'arraybuffer', host: storageUrl, }); - const attributes = window.textsecure.protobuf.AvatarUploadAttributes.decode( - response + const attributes = Proto.AvatarUploadAttributes.decode( + new FIXMEU8(response) ); const verified = verifyAttributes(attributes); diff --git a/ts/textsecure/WebsocketResources.ts b/ts/textsecure/WebsocketResources.ts index 010cfe4f0..59a8bb103 100644 --- a/ts/textsecure/WebsocketResources.ts +++ b/ts/textsecure/WebsocketResources.ts @@ -26,7 +26,7 @@ import { connection as WebSocket, IMessage } from 'websocket'; -import EventTarget from './EventTarget'; +import EventTarget, { EventHandler } from './EventTarget'; import { dropNull } from '../util/dropNull'; import { isOlderThan } from '../util/timestamp'; @@ -120,6 +120,12 @@ export type WebSocketResourceOptions = { keepalive?: KeepAliveOptionsType | true; }; +export class CloseEvent extends Event { + constructor(public readonly code: number, public readonly reason: string) { + super('close'); + } +} + export default class WebSocketResource extends EventTarget { private outgoingId = 1; @@ -159,6 +165,15 @@ export default class WebSocketResource extends EventTarget { }); } + public addEventListener( + name: 'close', + handler: (ev: CloseEvent) => void + ): void; + + public addEventListener(name: string, handler: EventHandler): void { + return super.addEventListener(name, handler); + } + public sendRequest( options: OutgoingWebSocketRequestOptions ): OutgoingWebSocketRequest { @@ -204,10 +219,7 @@ export default class WebSocketResource extends EventTarget { } window.log.warn('Dispatching our own socket close event'); - const ev = new Event('close'); - ev.code = code; - ev.reason = reason; - this.dispatchEvent(ev); + this.dispatchEvent(new CloseEvent(code, reason || 'normal')); }, 5000); } diff --git a/ts/textsecure/messageReceiverEvents.ts b/ts/textsecure/messageReceiverEvents.ts new file mode 100644 index 000000000..d7c8fbfff --- /dev/null +++ b/ts/textsecure/messageReceiverEvents.ts @@ -0,0 +1,370 @@ +// Copyright 2020-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable max-classes-per-file */ + +import { SignalService as Proto } from '../protobuf'; +import { ProcessedDataMessage, ProcessedSent } from './Types.d'; +import type { + ModifiedContactDetails, + ModifiedGroupDetails, +} from './ContactsParser'; + +export class ReconnectEvent extends Event { + constructor() { + super('reconnect'); + } +} + +export class EmptyEvent extends Event { + constructor() { + super('empty'); + } +} + +export class ProgressEvent extends Event { + public readonly count: number; + + constructor({ count }: { count: number }) { + super('progress'); + + this.count = count; + } +} + +export type TypingEventData = Readonly<{ + typingMessage: Proto.ITypingMessage; + timestamp: number; + started: boolean; + stopped: boolean; + groupId?: string; + groupV2Id?: string; +}>; + +export type TypingEventConfig = { + sender?: string; + senderUuid?: string; + senderDevice: number; + typing: TypingEventData; +}; + +export class TypingEvent extends Event { + public readonly sender?: string; + + public readonly senderUuid?: string; + + public readonly senderDevice: number; + + public readonly typing: TypingEventData; + + constructor({ sender, senderUuid, senderDevice, typing }: TypingEventConfig) { + super('typing'); + + this.sender = sender; + this.senderUuid = senderUuid; + this.senderDevice = senderDevice; + this.typing = typing; + } +} + +export class ErrorEvent extends Event { + constructor(public readonly error: Error) { + super('error'); + } +} + +export type DecryptionErrorEventData = Readonly<{ + cipherTextBytes?: ArrayBuffer; + cipherTextType?: number; + contentHint?: number; + groupId?: string; + receivedAtCounter: number; + receivedAtDate: number; + senderDevice: number; + senderUuid: string; + timestamp: number; +}>; + +export class DecryptionErrorEvent extends Event { + constructor(public readonly decryptionError: DecryptionErrorEventData) { + super('decryption-error'); + } +} + +export type RetryRequestEventData = Readonly<{ + groupId?: string; + requesterUuid: string; + requesterDevice: number; + senderDevice: number; + sentAt: number; +}>; + +export class RetryRequestEvent extends Event { + constructor(public readonly retryRequest: RetryRequestEventData) { + super('retry-request'); + } +} + +export class ContactEvent extends Event { + constructor(public readonly contactDetails: ModifiedContactDetails) { + super('contact'); + } +} + +export class ContactSyncEvent extends Event { + constructor() { + super('contactSync'); + } +} + +export class GroupEvent extends Event { + constructor(public readonly groupDetails: ModifiedGroupDetails) { + super('group'); + } +} + +export class GroupSyncEvent extends Event { + constructor() { + super('groupSync'); + } +} + +// +// Confirmable events below +// + +export type ConfirmCallback = () => void; + +export class ConfirmableEvent extends Event { + constructor(type: string, public readonly confirm: ConfirmCallback) { + super(type); + } +} + +export type DeliveryEventData = Readonly<{ + timestamp: number; + envelopeTimestamp?: number; + source?: string; + sourceUuid?: string; + sourceDevice?: number; +}>; + +export class DeliveryEvent extends ConfirmableEvent { + constructor( + public readonly deliveryReceipt: DeliveryEventData, + confirm: ConfirmCallback + ) { + super('delivery', confirm); + } +} + +export type SentEventData = Readonly<{ + destination?: string; + destinationUuid?: string; + timestamp?: number; + serverTimestamp?: number; + device?: number; + unidentifiedStatus: ProcessedSent['unidentifiedStatus']; + message: ProcessedDataMessage; + isRecipientUpdate: boolean; + receivedAtCounter: number; + receivedAtDate: number; + expirationStartTimestamp?: number; +}>; + +export class SentEvent extends ConfirmableEvent { + constructor(public readonly data: SentEventData, confirm: ConfirmCallback) { + super('sent', confirm); + } +} + +export type ProfileKeyUpdateData = Readonly<{ + source?: string; + sourceUuid?: string; + profileKey: string; +}>; + +export class ProfileKeyUpdateEvent extends ConfirmableEvent { + constructor( + public readonly data: ProfileKeyUpdateData, + confirm: ConfirmCallback + ) { + super('profileKeyUpdate', confirm); + } +} + +export type MessageEventData = Readonly<{ + source?: string; + sourceUuid?: string; + sourceDevice?: number; + timestamp: number; + serverGuid?: string; + serverTimestamp?: number; + unidentifiedDeliveryReceived: boolean; + message: ProcessedDataMessage; + receivedAtCounter: number; + receivedAtDate: number; +}>; + +export class MessageEvent extends ConfirmableEvent { + constructor( + public readonly data: MessageEventData, + confirm: ConfirmCallback + ) { + super('message', confirm); + } +} + +export type ReadEventData = Readonly<{ + timestamp: number; + envelopeTimestamp: number; + source?: string; + sourceUuid?: string; +}>; + +export class ReadEvent extends ConfirmableEvent { + constructor(public readonly read: ReadEventData, confirm: ConfirmCallback) { + super('read', confirm); + } +} + +export class ConfigurationEvent extends ConfirmableEvent { + constructor( + public readonly configuration: Proto.SyncMessage.IConfiguration, + confirm: ConfirmCallback + ) { + super('configuration', confirm); + } +} + +export type ViewSyncOptions = { + source?: string; + sourceUuid?: string; + timestamp?: number; +}; + +export class ViewSyncEvent extends ConfirmableEvent { + public readonly source?: string; + + public readonly sourceUuid?: string; + + public readonly timestamp?: number; + + constructor( + { source, sourceUuid, timestamp }: ViewSyncOptions, + confirm: ConfirmCallback + ) { + super('viewSync', confirm); + + this.source = source; + this.sourceUuid = sourceUuid; + this.timestamp = timestamp; + } +} + +export type MessageRequestResponseOptions = { + threadE164?: string; + threadUuid?: string; + messageRequestResponseType: Proto.SyncMessage.IMessageRequestResponse['type']; + groupId?: string; + groupV2Id?: string; +}; + +export class MessageRequestResponseEvent extends ConfirmableEvent { + public readonly threadE164?: string; + + public readonly threadUuid?: string; + + public readonly messageRequestResponseType?: MessageRequestResponseOptions['messageRequestResponseType']; + + public readonly groupId?: string; + + public readonly groupV2Id?: string; + + constructor( + { + threadE164, + threadUuid, + messageRequestResponseType, + groupId, + groupV2Id, + }: MessageRequestResponseOptions, + confirm: ConfirmCallback + ) { + super('messageRequestResponse', confirm); + + this.threadE164 = threadE164; + this.threadUuid = threadUuid; + this.messageRequestResponseType = messageRequestResponseType; + this.groupId = groupId; + this.groupV2Id = groupV2Id; + } +} + +export class FetchLatestEvent extends ConfirmableEvent { + constructor( + public readonly eventType: Proto.SyncMessage.IFetchLatest['type'], + confirm: ConfirmCallback + ) { + super('fetchLatest', confirm); + } +} + +export class KeysEvent extends ConfirmableEvent { + constructor( + public readonly storageServiceKey: ArrayBuffer, + confirm: ConfirmCallback + ) { + super('keys', confirm); + } +} + +export type StickerPackEventData = Readonly<{ + id?: string; + key?: string; + isInstall: boolean; + isRemove: boolean; +}>; + +export class StickerPackEvent extends ConfirmableEvent { + constructor( + public readonly stickerPacks: ReadonlyArray, + confirm: ConfirmCallback + ) { + super('sticker-pack', confirm); + } +} + +export type VerifiedEventData = Readonly<{ + state: Proto.IVerified['state']; + destination?: string; + destinationUuid?: string; + identityKey?: ArrayBuffer; + + // Used in `ts/background.ts` + viaContactSync?: boolean; +}>; + +export class VerifiedEvent extends ConfirmableEvent { + constructor( + public readonly verified: VerifiedEventData, + confirm: ConfirmCallback + ) { + super('verified', confirm); + } +} + +export type ReadSyncEventData = Readonly<{ + timestamp?: number; + envelopeTimestamp: number; + sender?: string; + senderUuid?: string; +}>; + +export class ReadSyncEvent extends ConfirmableEvent { + constructor( + public readonly read: ReadSyncEventData, + confirm: ConfirmCallback + ) { + super('readSync', confirm); + } +} diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts new file mode 100644 index 000000000..f018502b5 --- /dev/null +++ b/ts/textsecure/processDataMessage.ts @@ -0,0 +1,352 @@ +// Copyright 2020-2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import Long from 'long'; + +import { assert, strictAssert } from '../util/assert'; +import { dropNull, shallowDropNull } from '../util/dropNull'; +import { normalizeNumber } from '../util/normalizeNumber'; +import { SignalService as Proto } from '../protobuf'; +import { deriveGroupFields } from '../groups'; +import * as Bytes from '../Bytes'; +import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto'; + +import { + ProcessedAttachment, + ProcessedDataMessage, + ProcessedGroupContext, + ProcessedGroupV2Context, + ProcessedQuote, + ProcessedContact, + ProcessedPreview, + ProcessedSticker, + ProcessedReaction, + ProcessedDelete, +} from './Types.d'; + +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + +const FLAGS = Proto.DataMessage.Flags; +export const ATTACHMENT_MAX = 32; + +export function processAttachment( + attachment: Proto.IAttachmentPointer +): ProcessedAttachment; +export function processAttachment( + attachment?: Proto.IAttachmentPointer | null +): ProcessedAttachment | undefined; + +export function processAttachment( + attachment?: Proto.IAttachmentPointer | null +): ProcessedAttachment | undefined { + if (!attachment) { + return undefined; + } + return { + ...shallowDropNull(attachment), + + cdnId: attachment.cdnId ? attachment.cdnId.toString() : undefined, + key: attachment.key ? Bytes.toBase64(attachment.key) : undefined, + digest: attachment.digest ? Bytes.toBase64(attachment.digest) : undefined, + }; +} + +async function processGroupContext( + group?: Proto.IGroupContext | null +): Promise { + if (!group) { + return undefined; + } + + strictAssert(group.id, 'group context without id'); + strictAssert( + group.type !== undefined && group.type !== null, + 'group context without type' + ); + + const masterKey = await deriveMasterKeyFromGroupV1( + typedArrayToArrayBuffer(group.id) + ); + const data = deriveGroupFields(new FIXMEU8(masterKey)); + + const derivedGroupV2Id = Bytes.toBase64(data.id); + + const result: ProcessedGroupContext = { + id: Bytes.toBinary(group.id), + type: group.type, + name: dropNull(group.name), + membersE164: group.membersE164 ?? [], + avatar: processAttachment(group.avatar), + derivedGroupV2Id, + }; + + if (result.type === Proto.GroupContext.Type.DELIVER) { + result.name = undefined; + result.membersE164 = []; + result.avatar = undefined; + } + + return result; +} + +export function processGroupV2Context( + groupV2?: Proto.IGroupContextV2 | null +): ProcessedGroupV2Context | undefined { + if (!groupV2) { + return undefined; + } + + strictAssert(groupV2.masterKey, 'groupV2 context without masterKey'); + const data = deriveGroupFields(groupV2.masterKey); + + return { + masterKey: Bytes.toBase64(groupV2.masterKey), + revision: dropNull(groupV2.revision), + groupChange: groupV2.groupChange + ? Bytes.toBase64(groupV2.groupChange) + : undefined, + id: Bytes.toBase64(data.id), + secretParams: Bytes.toBase64(data.secretParams), + publicParams: Bytes.toBase64(data.publicParams), + }; +} + +export function processQuote( + quote?: Proto.DataMessage.IQuote | null +): ProcessedQuote | undefined { + if (!quote) { + return undefined; + } + + return { + id: normalizeNumber(dropNull(quote.id)), + authorUuid: dropNull(quote.authorUuid), + text: dropNull(quote.text), + attachments: (quote.attachments ?? []).map(attachment => { + return { + contentType: dropNull(attachment.contentType), + fileName: dropNull(attachment.fileName), + thumbnail: processAttachment(attachment.thumbnail), + }; + }), + bodyRanges: quote.bodyRanges ?? [], + }; +} + +export function processContact( + contact?: ReadonlyArray | null +): ReadonlyArray | undefined { + if (!contact) { + return undefined; + } + + return contact.map(item => { + return { + ...item, + avatar: item.avatar + ? { + avatar: processAttachment(item.avatar.avatar), + isProfile: Boolean(item.avatar.isProfile), + } + : undefined, + }; + }); +} + +function isLinkPreviewDateValid(value: unknown): value is number { + return ( + typeof value === 'number' && + !Number.isNaN(value) && + Number.isFinite(value) && + value > 0 + ); +} + +function cleanLinkPreviewDate( + value?: Long | number | null +): number | undefined { + const result = normalizeNumber(value ?? undefined); + return isLinkPreviewDateValid(result) ? result : undefined; +} + +export function processPreview( + preview?: ReadonlyArray | null +): ReadonlyArray | undefined { + if (!preview) { + return undefined; + } + + return preview.map(item => { + return { + url: dropNull(item.url), + title: dropNull(item.title), + image: item.image ? processAttachment(item.image) : undefined, + description: dropNull(item.description), + date: cleanLinkPreviewDate(item.date), + }; + }); +} + +export function processSticker( + sticker?: Proto.DataMessage.ISticker | null +): ProcessedSticker | undefined { + if (!sticker) { + return undefined; + } + + return { + packId: sticker.packId ? Bytes.toHex(sticker.packId) : undefined, + packKey: sticker.packKey ? Bytes.toBase64(sticker.packKey) : undefined, + stickerId: normalizeNumber(dropNull(sticker.stickerId)), + data: processAttachment(sticker.data), + }; +} + +export function processReaction( + reaction?: Proto.DataMessage.IReaction | null +): ProcessedReaction | undefined { + if (!reaction) { + return undefined; + } + + return { + emoji: dropNull(reaction.emoji), + remove: Boolean(reaction.remove), + targetAuthorUuid: dropNull(reaction.targetAuthorUuid), + targetTimestamp: normalizeNumber(dropNull(reaction.targetTimestamp)), + }; +} + +export function processDelete( + del?: Proto.DataMessage.IDelete | null +): ProcessedDelete | undefined { + if (!del) { + return undefined; + } + + return { + targetSentTimestamp: normalizeNumber(dropNull(del.targetSentTimestamp)), + }; +} + +export async function processDataMessage( + message: Proto.IDataMessage, + envelopeTimestamp: number +): Promise { + /* eslint-disable no-bitwise */ + + // 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 (!message.timestamp) { + throw new Error('Missing timestamp on dataMessage'); + } + + const timestamp = normalizeNumber(message.timestamp); + + if (envelopeTimestamp !== timestamp) { + throw new Error( + `Timestamp ${timestamp} in DataMessage did not ` + + `match envelope timestamp ${envelopeTimestamp}` + ); + } + + const result: ProcessedDataMessage = { + body: dropNull(message.body), + attachments: ( + message.attachments ?? [] + ).map((attachment: Proto.IAttachmentPointer) => + processAttachment(attachment) + ), + group: await processGroupContext(message.group), + groupV2: processGroupV2Context(message.groupV2), + flags: message.flags ?? 0, + expireTimer: message.expireTimer ?? 0, + profileKey: message.profileKey + ? Bytes.toBase64(message.profileKey) + : undefined, + timestamp, + quote: processQuote(message.quote), + contact: processContact(message.contact), + preview: processPreview(message.preview), + sticker: processSticker(message.sticker), + requiredProtocolVersion: normalizeNumber( + dropNull(message.requiredProtocolVersion) + ), + isViewOnce: Boolean(message.isViewOnce), + reaction: processReaction(message.reaction), + delete: processDelete(message.delete), + bodyRanges: message.bodyRanges ?? [], + groupCallUpdate: dropNull(message.groupCallUpdate), + }; + + const isEndSession = Boolean(result.flags & FLAGS.END_SESSION); + const isExpirationTimerUpdate = Boolean( + result.flags & FLAGS.EXPIRATION_TIMER_UPDATE + ); + const isProfileKeyUpdate = Boolean(result.flags & FLAGS.PROFILE_KEY_UPDATE); + // The following assertion codifies an assumption: 0 or 1 flags are set, but never + // more. This assumption is fine as of this writing, but may not always be. + const flagCount = [ + isEndSession, + isExpirationTimerUpdate, + isProfileKeyUpdate, + ].filter(Boolean).length; + assert( + flagCount <= 1, + `Expected exactly <=1 flags to be set, but got ${flagCount}` + ); + + if (isEndSession) { + result.body = undefined; + result.attachments = []; + result.group = undefined; + return result; + } + + if (isExpirationTimerUpdate) { + result.body = undefined; + result.attachments = []; + } else if (isProfileKeyUpdate) { + result.body = undefined; + result.attachments = []; + } else if (result.flags !== 0) { + throw new Error(`Unknown flags in message: ${result.flags}`); + } + + if (result.group) { + switch (result.group.type) { + case Proto.GroupContext.Type.UPDATE: + result.body = undefined; + result.attachments = []; + break; + case Proto.GroupContext.Type.QUIT: + result.body = undefined; + result.attachments = []; + break; + case Proto.GroupContext.Type.DELIVER: + // Cleaned up in `processGroupContext` + break; + default: { + const err = new Error( + `Unknown group message type: ${result.group.type}` + ); + err.warn = true; + throw err; + } + } + } + + const attachmentCount = result.attachments.length; + if (attachmentCount > ATTACHMENT_MAX) { + throw new Error( + `Too many attachments: ${attachmentCount} included in one message, ` + + `max is ${ATTACHMENT_MAX}` + ); + } + + return result; +} diff --git a/ts/textsecure/processSyncMessage.ts b/ts/textsecure/processSyncMessage.ts new file mode 100644 index 000000000..3a111094a --- /dev/null +++ b/ts/textsecure/processSyncMessage.ts @@ -0,0 +1,60 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { SignalService as Proto } from '../protobuf'; +import { normalizeUuid } from '../util/normalizeUuid'; +import { + ProcessedUnidentifiedDeliveryStatus, + ProcessedSent, + ProcessedSyncMessage, +} from './Types.d'; + +import UnidentifiedDeliveryStatus = Proto.SyncMessage.Sent.IUnidentifiedDeliveryStatus; + +function processUnidentifiedDeliveryStatus( + status: UnidentifiedDeliveryStatus +): ProcessedUnidentifiedDeliveryStatus { + const { destinationUuid } = status; + + return { + ...status, + + destinationUuid: destinationUuid + ? normalizeUuid( + destinationUuid, + 'syncMessage.sent.unidentifiedStatus.destinationUuid' + ) + : undefined, + }; +} + +function processSent( + sent?: Proto.SyncMessage.ISent | null +): ProcessedSent | undefined { + if (!sent) { + return undefined; + } + + const { destinationUuid, unidentifiedStatus } = sent; + + return { + ...sent, + + destinationUuid: destinationUuid + ? normalizeUuid(destinationUuid, 'syncMessage.sent.destinationUuid') + : undefined, + + unidentifiedStatus: unidentifiedStatus + ? unidentifiedStatus.map(processUnidentifiedDeliveryStatus) + : undefined, + }; +} + +export function processSyncMessage( + syncMessage: Proto.ISyncMessage +): ProcessedSyncMessage { + return { + ...syncMessage, + sent: processSent(syncMessage.sent), + }; +} diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index f3e3234cf..810fc444e 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -51,6 +51,9 @@ export type AttachmentType = { cdnNumber?: number; cdnId?: string; cdnKey?: string; + + /** Legacy field. Used only for downloading old attachments */ + id?: number; }; type BaseAttachmentDraftType = { diff --git a/js/modules/stickers.js b/ts/types/Stickers.ts similarity index 64% rename from js/modules/stickers.js rename to ts/types/Stickers.ts index aafcb4bca..35cb37092 100644 --- a/js/modules/stickers.js +++ b/ts/types/Stickers.ts @@ -1,17 +1,50 @@ // Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-restricted-syntax */ -/* global - textsecure, - Signal, - log, - navigator, - reduxStore, - reduxActions, - URLSearchParams -*/ +import { isNumber, pick, reject, groupBy, values } from 'lodash'; +import pMap from 'p-map'; +import Queue from 'p-queue'; -const BLESSED_PACKS = { +import { strictAssert } from '../util/assert'; +import { dropNull } from '../util/dropNull'; +import { makeLookup } from '../util/makeLookup'; +import { maybeParseUrl } from '../util/url'; +import { base64ToArrayBuffer, deriveStickerPackKey } from '../Crypto'; +import type { + StickerType, + StickerPackType, + StickerPackStatusType, +} from '../sql/Interface'; +import Data from '../sql/Client'; +import { SignalService as Proto } from '../protobuf'; + +export type RecentStickerType = Readonly<{ + stickerId: number; + packId: string; +}>; + +export type BlessedType = Pick; + +export type InitialState = { + packs: Record; + recentStickers: Array; + blessedPacks: Record; +}; + +export type DownloadMap = Record< + string, + { + id: string; + key: string; + status?: StickerPackStatusType; + } +>; + +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + +export const BLESSED_PACKS: Record = { '9acc9e8aba563d26a4994e69263e3b25': { key: 'Wm3/OUjCjvubeq+T7MN1xp/DFueAd+0mhnoU0QoPahI=', status: 'downloaded', @@ -30,104 +63,81 @@ const BLESSED_PACKS = { }, }; -const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i; +const STICKER_PACK_DEFAULTS: StickerPackType = { + id: '', + key: '', -const { isNumber, pick, reject, groupBy, values } = require('lodash'); -const pMap = require('p-map'); -const Queue = require('p-queue').default; - -const { makeLookup } = require('../../ts/util/makeLookup'); -const { maybeParseUrl } = require('../../ts/util/url'); -const { - base64ToArrayBuffer, - deriveStickerPackKey, -} = require('../../ts/Crypto'); -const { - addStickerPackReference, - createOrUpdateSticker, - createOrUpdateStickerPack, - deleteStickerPack, - deleteStickerPackReference, - getAllStickerPacks, - getAllStickers, - getRecentStickers, - updateStickerPackStatus, -} = require('../../ts/sql/Client').default; - -module.exports = { - BLESSED_PACKS, - copyStickerToAttachments, - deletePack, - deletePackReference, - downloadStickerPack, - downloadEphemeralPack, - getDataFromLink, - getInitialState, - getInstalledStickerPacks, - getSticker, - getStickerPack, - getStickerPackStatus, - load, - maybeDeletePack, - downloadQueuedPacks, - isPackIdValid, - redactPackId, - removeEphemeralPack, - savePackMetadata, + author: '', + coverStickerId: 0, + createdAt: 0, + downloadAttempts: 0, + status: 'ephemeral', + stickerCount: 0, + stickers: {}, + title: '', }; -let initialState = null; -let packsToDownload = null; +const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i; + +let initialState: InitialState | undefined; +let packsToDownload: DownloadMap | undefined; const downloadQueue = new Queue({ concurrency: 1, timeout: 1000 * 60 * 2 }); -async function load() { +export async function load(): Promise { const [packs, recentStickers] = await Promise.all([ getPacksForRedux(), getRecentStickersForRedux(), ]); + const blessedPacks: Record = Object.create(null); + for (const key of Object.keys(BLESSED_PACKS)) { + blessedPacks[key] = true; + } + initialState = { packs, recentStickers, - blessedPacks: BLESSED_PACKS, + blessedPacks, }; packsToDownload = capturePacksToDownload(packs); } -function getDataFromLink(link) { +export function getDataFromLink( + link: string +): undefined | { id: string; key: string } { const url = maybeParseUrl(link); if (!url) { - return null; + return undefined; } const { hash } = url; if (!hash) { - return null; + return undefined; } let params; try { params = new URLSearchParams(hash.slice(1)); } catch (err) { - return null; + return undefined; } const id = params.get('pack_id'); if (!isPackIdValid(id)) { - return null; + return undefined; } const key = params.get('pack_key'); if (!key) { - return null; + return undefined; } return { id, key }; } -function getInstalledStickerPacks() { - const state = reduxStore.getState(); +export function getInstalledStickerPacks(): Array { + const state = window.reduxStore.getState(); const { stickers } = state; const { packs } = stickers; if (!packs) { @@ -138,20 +148,24 @@ function getInstalledStickerPacks() { return items.filter(pack => pack.status === 'installed'); } -function downloadQueuedPacks() { +export function downloadQueuedPacks(): void { + strictAssert(packsToDownload, 'Stickers not initialized'); + const ids = Object.keys(packsToDownload); - ids.forEach(id => { + for (const id of ids) { const { key, status } = packsToDownload[id]; // The queuing is done inside this function, no need to await here downloadStickerPack(id, key, { finalStatus: status }); - }); + } packsToDownload = {}; } -function capturePacksToDownload(existingPackLookup) { - const toDownload = Object.create(null); +function capturePacksToDownload( + existingPackLookup: Record +): DownloadMap { + const toDownload: DownloadMap = Object.create(null); // First, ensure that blessed packs are in good shape const blessedIds = Object.keys(BLESSED_PACKS); @@ -190,7 +204,7 @@ function capturePacksToDownload(existingPackLookup) { if (doesPackNeedDownload(existing)) { const status = - existing.attemptedStatus === 'installed' ? 'installed' : null; + existing.attemptedStatus === 'installed' ? 'installed' : undefined; toDownload[id] = { id, key: existing.key, @@ -202,7 +216,7 @@ function capturePacksToDownload(existingPackLookup) { return toDownload; } -function doesPackNeedDownload(pack) { +function doesPackNeedDownload(pack?: StickerPackType): boolean { if (!pack) { return true; } @@ -226,14 +240,14 @@ function doesPackNeedDownload(pack) { return true; } -async function getPacksForRedux() { +async function getPacksForRedux(): Promise> { const [packs, stickers] = await Promise.all([ - getAllStickerPacks(), - getAllStickers(), + Data.getAllStickerPacks(), + Data.getAllStickers(), ]); const stickersByPack = groupBy(stickers, sticker => sticker.packId); - const fullSet = packs.map(pack => ({ + const fullSet: Array = packs.map(pack => ({ ...pack, stickers: makeLookup(stickersByPack[pack.id] || [], 'id'), })); @@ -241,40 +255,41 @@ async function getPacksForRedux() { return makeLookup(fullSet, 'id'); } -async function getRecentStickersForRedux() { - const recent = await getRecentStickers(); +async function getRecentStickersForRedux(): Promise> { + const recent = await Data.getRecentStickers(); return recent.map(sticker => ({ packId: sticker.packId, stickerId: sticker.id, })); } -function getInitialState() { +export function getInitialState(): InitialState { + strictAssert(initialState !== undefined, 'Stickers not initialized'); return initialState; } -function isPackIdValid(packId) { +export function isPackIdValid(packId: unknown): packId is string { return typeof packId === 'string' && VALID_PACK_ID_REGEXP.test(packId); } -function redactPackId(packId) { +export function redactPackId(packId: string): string { return `[REDACTED]${packId.slice(-3)}`; } function getReduxStickerActions() { - const actions = reduxActions; + const actions = window.reduxActions; + strictAssert(actions && actions.stickers, 'Redux not ready'); - if (actions && actions.stickers) { - return actions.stickers; - } - - return {}; + return actions.stickers; } -async function decryptSticker(packKey, ciphertext) { +async function decryptSticker( + packKey: string, + ciphertext: ArrayBuffer +): Promise { const binaryKey = base64ToArrayBuffer(packKey); const derivedKey = await deriveStickerPackKey(binaryKey); - const plaintext = await textsecure.crypto.decryptAttachment( + const plaintext = await window.textsecure.crypto.decryptAttachment( ciphertext, derivedKey ); @@ -282,26 +297,35 @@ async function decryptSticker(packKey, ciphertext) { return plaintext; } -async function downloadSticker(packId, packKey, proto, options) { - const { ephemeral } = options || {}; +async function downloadSticker( + packId: string, + packKey: string, + proto: Proto.StickerPack.ISticker, + { ephemeral }: { ephemeral?: boolean } = {} +): Promise> { + const { id, emoji } = proto; + strictAssert(id !== undefined && id !== null, "Sticker id can't be null"); - const ciphertext = await textsecure.messaging.getSticker(packId, proto.id); + const ciphertext = await window.textsecure.messaging.getSticker(packId, id); const plaintext = await decryptSticker(packKey, ciphertext); const sticker = ephemeral - ? await Signal.Migrations.processNewEphemeralSticker(plaintext, options) - : await Signal.Migrations.processNewSticker(plaintext, options); + ? await window.Signal.Migrations.processNewEphemeralSticker(plaintext) + : await window.Signal.Migrations.processNewSticker(plaintext); return { - ...pick(proto, ['id', 'emoji']), + id, + emoji: dropNull(emoji), ...sticker, packId, }; } -async function savePackMetadata(packId, packKey, options = {}) { - const { messageId } = options; - +export async function savePackMetadata( + packId: string, + packKey: string, + { messageId }: { messageId?: string } = {} +): Promise { const existing = getStickerPack(packId); if (existing) { return; @@ -309,20 +333,23 @@ async function savePackMetadata(packId, packKey, options = {}) { const { stickerPackAdded } = getReduxStickerActions(); const pack = { + ...STICKER_PACK_DEFAULTS, + id: packId, key: packKey, - status: 'known', + status: 'known' as const, }; stickerPackAdded(pack); - await createOrUpdateStickerPack(pack); + await Data.createOrUpdateStickerPack(pack); if (messageId) { - await addStickerPackReference(messageId, packId); + await Data.addStickerPackReference(messageId, packId); } } -async function removeEphemeralPack(packId) { +export async function removeEphemeralPack(packId: string): Promise { const existing = getStickerPack(packId); + strictAssert(existing, `No existing sticker pack with id: ${packId}`); if ( existing.status !== 'ephemeral' && !(existing.status === 'error' && existing.attemptedStatus === 'ephemeral') @@ -335,16 +362,18 @@ async function removeEphemeralPack(packId) { const stickers = values(existing.stickers); const paths = stickers.map(sticker => sticker.path); - await pMap(paths, Signal.Migrations.deleteTempFile, { + await pMap(paths, window.Signal.Migrations.deleteTempFile, { concurrency: 3, - timeout: 1000 * 60 * 2, }); // Remove it from database in case it made it there - await deleteStickerPack(packId); + await Data.deleteStickerPack(packId); } -async function downloadEphemeralPack(packId, packKey) { +export async function downloadEphemeralPack( + packId: string, + packKey: string +): Promise { const { stickerAdded, stickerPackAdded, @@ -358,7 +387,7 @@ async function downloadEphemeralPack(packId, packKey) { existingPack.status === 'installed' || existingPack.status === 'pending') ) { - log.warn( + window.log.warn( `Ephemeral download for pack ${redactPackId( packId )} requested, we already know about it. Skipping.` @@ -369,17 +398,19 @@ async function downloadEphemeralPack(packId, packKey) { try { // Synchronous placeholder to help with race conditions const placeholder = { + ...STICKER_PACK_DEFAULTS, + id: packId, key: packKey, - status: 'ephemeral', + status: 'ephemeral' as const, }; stickerPackAdded(placeholder); - const ciphertext = await textsecure.messaging.getStickerPackManifest( + const ciphertext = await window.textsecure.messaging.getStickerPackManifest( packId ); const plaintext = await decryptSticker(packKey, ciphertext); - const proto = textsecure.protobuf.StickerPack.decode(plaintext); + const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext)); const firstStickerProto = proto.stickers ? proto.stickers[0] : null; const stickerCount = proto.stickers.length; @@ -402,16 +433,20 @@ async function downloadEphemeralPack(packId, packKey) { const coverIncludedInList = nonCoverStickers.length < stickerCount; const pack = { + ...STICKER_PACK_DEFAULTS, + id: packId, key: packKey, coverStickerId, stickerCount, - status: 'ephemeral', + status: 'ephemeral' as const, ...pick(proto, ['title', 'author']), }; stickerPackAdded(pack); - const downloadStickerJob = async stickerProto => { + const downloadStickerJob = async ( + stickerProto: Proto.StickerPack.ISticker + ): Promise => { const stickerInfo = await downloadSticker(packId, packKey, stickerProto, { ephemeral: true, }); @@ -438,7 +473,6 @@ async function downloadEphemeralPack(packId, packKey) { // Then the rest await pMap(nonCoverStickers, downloadStickerJob, { concurrency: 3, - timeout: 1000 * 60 * 2, }); } catch (error) { // Because the user could install this pack while we are still downloading this @@ -451,20 +485,30 @@ async function downloadEphemeralPack(packId, packKey) { status: 'error', }); } - log.error( + window.log.error( `Ephemeral download error for sticker pack ${redactPackId(packId)}:`, error && error.stack ? error.stack : error ); } } -async function downloadStickerPack(packId, packKey, options = {}) { +export type DownloadStickerPackOptions = Readonly<{ + messageId?: string; + fromSync?: boolean; + finalStatus?: StickerPackStatusType; +}>; + +export async function downloadStickerPack( + packId: string, + packKey: string, + options: DownloadStickerPackOptions = {} +): Promise { // This will ensure that only one download process is in progress at any given time return downloadQueue.add(async () => { try { await doDownloadStickerPack(packId, packKey, options); } catch (error) { - log.error( + window.log.error( 'doDownloadStickerPack threw an error:', error && error.stack ? error.stack : error ); @@ -472,8 +516,15 @@ async function downloadStickerPack(packId, packKey, options = {}) { }); } -async function doDownloadStickerPack(packId, packKey, options = {}) { - const { messageId, fromSync } = options; +async function doDownloadStickerPack( + packId: string, + packKey: string, + { + finalStatus = 'downloaded', + messageId, + fromSync = false, + }: DownloadStickerPackOptions +): Promise { const { stickerAdded, stickerPackAdded, @@ -481,7 +532,6 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { installStickerPack, } = getReduxStickerActions(); - const finalStatus = options.finalStatus || 'downloaded'; if (finalStatus !== 'downloaded' && finalStatus !== 'installed') { throw new Error( `doDownloadStickerPack: invalid finalStatus of ${finalStatus} requested.` @@ -490,7 +540,7 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { const existing = getStickerPack(packId); if (!doesPackNeedDownload(existing)) { - log.warn( + window.log.warn( `Download for pack ${redactPackId( packId )} requested, but it does not need re-download. Skipping.` @@ -503,14 +553,14 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { const downloadAttempts = (existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement; if (downloadAttempts > 3) { - log.warn( + window.log.warn( `Refusing to attempt another download for pack ${redactPackId( packId )}, attempt number ${downloadAttempts}` ); - if (existing.status !== 'error') { - await updateStickerPackStatus(packId, 'error'); + if (existing && existing.status !== 'error') { + await Data.updateStickerPackStatus(packId, 'error'); stickerPackUpdated(packId, { status: 'error', }); @@ -519,32 +569,34 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { return; } - let coverProto; - let coverStickerId; - let coverIncludedInList; - let nonCoverStickers; + let coverProto: Proto.StickerPack.ISticker | undefined; + let coverStickerId: number | undefined; + let coverIncludedInList = false; + let nonCoverStickers: Array = []; try { // Synchronous placeholder to help with race conditions const placeholder = { + ...STICKER_PACK_DEFAULTS, + id: packId, key: packKey, attemptedStatus: finalStatus, downloadAttempts, - status: 'pending', + status: 'pending' as const, }; stickerPackAdded(placeholder); - const ciphertext = await textsecure.messaging.getStickerPackManifest( + const ciphertext = await window.textsecure.messaging.getStickerPackManifest( packId ); const plaintext = await decryptSticker(packKey, ciphertext); - const proto = textsecure.protobuf.StickerPack.decode(plaintext); - const firstStickerProto = proto.stickers ? proto.stickers[0] : null; + const proto = Proto.StickerPack.decode(new FIXMEU8(plaintext)); + const firstStickerProto = proto.stickers ? proto.stickers[0] : undefined; const stickerCount = proto.stickers.length; coverProto = proto.cover || firstStickerProto; - coverStickerId = coverProto ? coverProto.id : null; + coverStickerId = dropNull(coverProto ? coverProto.id : undefined); if (!coverProto || !isNumber(coverStickerId)) { throw new Error( @@ -568,7 +620,7 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { // - 'downloaded' // - 'error' // - 'installed' - const pack = { + const pack: StickerPackType = { id: packId, key: packKey, attemptedStatus: finalStatus, @@ -576,28 +628,32 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { downloadAttempts, stickerCount, status: 'pending', + createdAt: Date.now(), + stickers: {}, ...pick(proto, ['title', 'author']), }; - await createOrUpdateStickerPack(pack); + await Data.createOrUpdateStickerPack(pack); stickerPackAdded(pack); if (messageId) { - await addStickerPackReference(messageId, packId); + await Data.addStickerPackReference(messageId, packId); } } catch (error) { - log.error( + window.log.error( `Error downloading manifest for sticker pack ${redactPackId(packId)}:`, error && error.stack ? error.stack : error ); const pack = { + ...STICKER_PACK_DEFAULTS, + id: packId, key: packKey, attemptedStatus: finalStatus, downloadAttempts, - status: 'error', + status: 'error' as const, }; - await createOrUpdateStickerPack(pack); + await Data.createOrUpdateStickerPack(pack); stickerPackAdded(pack); return; @@ -606,13 +662,15 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { // We have a separate try/catch here because we're starting to download stickers here // and we want to preserve more of the pack on an error. try { - const downloadStickerJob = async stickerProto => { + const downloadStickerJob = async ( + stickerProto: Proto.StickerPack.ISticker + ): Promise => { const stickerInfo = await downloadSticker(packId, packKey, stickerProto); const sticker = { ...stickerInfo, isCoverOnly: !coverIncludedInList && stickerInfo.id === coverStickerId, }; - await createOrUpdateSticker(sticker); + await Data.createOrUpdateSticker(sticker); stickerAdded(sticker); }; @@ -622,7 +680,6 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { // Then the rest await pMap(nonCoverStickers, downloadStickerJob, { concurrency: 3, - timeout: 1000 * 60 * 2, }); // Allow for the user marking this pack as installed in the middle of our download; @@ -636,19 +693,19 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { await installStickerPack(packId, packKey, { fromSync }); } else { // Mark the pack as complete - await updateStickerPackStatus(packId, finalStatus); + await Data.updateStickerPackStatus(packId, finalStatus); stickerPackUpdated(packId, { status: finalStatus, }); } } catch (error) { - log.error( + window.log.error( `Error downloading stickers for sticker pack ${redactPackId(packId)}:`, error && error.stack ? error.stack : error ); const errorStatus = 'error'; - await updateStickerPackStatus(packId, errorStatus); + await Data.updateStickerPackStatus(packId, errorStatus); if (stickerPackUpdated) { stickerPackUpdated(packId, { attemptedStatus: finalStatus, @@ -658,45 +715,53 @@ async function doDownloadStickerPack(packId, packKey, options = {}) { } } -function getStickerPack(packId) { - const state = reduxStore.getState(); +export function getStickerPack(packId: string): StickerPackType | undefined { + const state = window.reduxStore.getState(); const { stickers } = state; const { packs } = stickers; if (!packs) { - return null; + return undefined; } return packs[packId]; } -function getStickerPackStatus(packId) { +export function getStickerPackStatus( + packId: string +): StickerPackStatusType | undefined { const pack = getStickerPack(packId); if (!pack) { - return null; + return undefined; } return pack.status; } -function getSticker(packId, stickerId) { +export function getSticker( + packId: string, + stickerId: number +): StickerType | undefined { const pack = getStickerPack(packId); if (!pack || !pack.stickers) { - return null; + return undefined; } return pack.stickers[stickerId]; } -async function copyStickerToAttachments(packId, stickerId) { +export async function copyStickerToAttachments( + packId: string, + stickerId: number +): Promise { const sticker = getSticker(packId, stickerId); if (!sticker) { - return null; + return undefined; } const { path } = sticker; - const absolutePath = Signal.Migrations.getAbsoluteStickerPath(path); - const newPath = await Signal.Migrations.copyIntoAttachmentsDirectory( + const absolutePath = window.Signal.Migrations.getAbsoluteStickerPath(path); + const newPath = await window.Signal.Migrations.copyIntoAttachmentsDirectory( absolutePath ); @@ -709,7 +774,7 @@ async function copyStickerToAttachments(packId, stickerId) { // In the case where a sticker pack is uninstalled, we want to delete it if there are no // more references left. We'll delete a nonexistent reference, then check if there are // any references left, just like usual. -async function maybeDeletePack(packId) { +export async function maybeDeletePack(packId: string): Promise { // This hardcoded string is fine because message ids are GUIDs await deletePackReference('NOT-USED', packId); } @@ -717,7 +782,10 @@ async function maybeDeletePack(packId) { // We don't generally delete packs outright; we just remove references to them, and if // the last reference is deleted, we finally then remove the pack itself from database // and from disk. -async function deletePackReference(messageId, packId) { +export async function deletePackReference( + messageId: string, + packId: string +): Promise { const isBlessed = Boolean(BLESSED_PACKS[packId]); if (isBlessed) { return; @@ -725,7 +793,7 @@ async function deletePackReference(messageId, packId) { // This call uses locking to prevent race conditions with other reference removals, // or an incoming message creating a new message->pack reference - const paths = await deleteStickerPackReference(messageId, packId); + const paths = await Data.deleteStickerPackReference(messageId, packId); // If we don't get a list of paths back, then the sticker pack was not deleted if (!paths || !paths.length) { @@ -735,14 +803,13 @@ async function deletePackReference(messageId, packId) { const { removeStickerPack } = getReduxStickerActions(); removeStickerPack(packId); - await pMap(paths, Signal.Migrations.deleteSticker, { + await pMap(paths, window.Signal.Migrations.deleteSticker, { concurrency: 3, - timeout: 1000 * 60 * 2, }); } // The override; doesn't honor our ref-counting scheme - just deletes it all. -async function deletePack(packId) { +export async function deletePack(packId: string): Promise { const isBlessed = Boolean(BLESSED_PACKS[packId]); if (isBlessed) { return; @@ -750,13 +817,12 @@ async function deletePack(packId) { // This call uses locking to prevent race conditions with other reference removals, // or an incoming message creating a new message->pack reference - const paths = await deleteStickerPack(packId); + const paths = await Data.deleteStickerPack(packId); const { removeStickerPack } = getReduxStickerActions(); removeStickerPack(packId); - await pMap(paths, Signal.Migrations.deleteSticker, { + await pMap(paths, window.Signal.Migrations.deleteSticker, { concurrency: 3, - timeout: 1000 * 60 * 2, }); } diff --git a/ts/types/Util.ts b/ts/types/Util.ts index 3ef77d823..d83cadd35 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -4,8 +4,8 @@ export type BodyRangeType = { start: number; length: number; - mentionUuid: string; - replacementText: string; + mentionUuid?: string; + replacementText?: string; conversationID?: string; }; diff --git a/ts/util/downloadAttachment.ts b/ts/util/downloadAttachment.ts index 6cd28db41..f5eab016f 100644 --- a/ts/util/downloadAttachment.ts +++ b/ts/util/downloadAttachment.ts @@ -1,26 +1,29 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { - AttachmentPointerClass, - DownloadAttachmentType, -} from '../textsecure.d'; +import { DownloadAttachmentType } from '../textsecure.d'; -type AttachmentData = AttachmentPointerClass & { - id?: string; -}; +import { AttachmentType } from '../types/Attachment'; export async function downloadAttachment( - attachmentData: AttachmentData + attachmentData: AttachmentType ): Promise { + let migratedAttachment: AttachmentType; + + const { id: legacyId } = attachmentData; + if (legacyId === undefined) { + migratedAttachment = attachmentData; + } else { + migratedAttachment = { + ...attachmentData, + cdnId: String(legacyId), + }; + } + let downloaded; try { - if (attachmentData.id) { - // eslint-disable-next-line no-param-reassign - attachmentData.cdnId = attachmentData.id; - } downloaded = await window.textsecure.messageReceiver.downloadAttachment( - attachmentData + migratedAttachment ); } catch (error) { // Attachments on the server expire after 30 days, then start returning 404 diff --git a/ts/util/dropNull.ts b/ts/util/dropNull.ts index f4651b1ea..b68b4a414 100644 --- a/ts/util/dropNull.ts +++ b/ts/util/dropNull.ts @@ -1,5 +1,10 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-restricted-syntax */ + +export type NullToUndefined = Extract extends never + ? T + : Exclude | undefined; export function dropNull( value: NonNullable | null | undefined @@ -9,3 +14,25 @@ export function dropNull( } return value; } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function shallowDropNull( + value: O | null | undefined +): + | { + [Property in keyof O]: NullToUndefined; + } + | undefined { + if (value === null || value === undefined) { + return undefined; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any = {}; + + for (const [key, propertyValue] of Object.entries(value)) { + result[key] = dropNull(propertyValue); + } + + return result; +} diff --git a/ts/util/getAccessControlOptions.ts b/ts/util/getAccessControlOptions.ts index 6320cfdb2..7a41359b1 100644 --- a/ts/util/getAccessControlOptions.ts +++ b/ts/util/getAccessControlOptions.ts @@ -2,7 +2,9 @@ // SPDX-License-Identifier: AGPL-3.0-only import { LocalizerType } from '../types/Util'; -import { AccessControlClass } from '../textsecure.d'; +import { SignalService as Proto } from '../protobuf'; + +const AccessControlEnum = Proto.AccessControl.AccessRequired; type AccessControlOption = { text: string; @@ -10,17 +12,16 @@ type AccessControlOption = { }; export function getAccessControlOptions( - accessEnum: typeof AccessControlClass.AccessRequired, i18n: LocalizerType ): Array { return [ { text: i18n('GroupV2--all-members'), - value: accessEnum.MEMBER, + value: AccessControlEnum.MEMBER, }, { text: i18n('GroupV2--only-admins'), - value: accessEnum.ADMINISTRATOR, + value: AccessControlEnum.ADMINISTRATOR, }, ]; } diff --git a/ts/util/isConversationAccepted.ts b/ts/util/isConversationAccepted.ts index 07a34f289..94dd953c5 100644 --- a/ts/util/isConversationAccepted.ts +++ b/ts/util/isConversationAccepted.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { ConversationAttributesType } from '../model-types.d'; +import { SignalService as Proto } from '../protobuf'; import { isDirectConversation, isMe } from './whatTypeOfConversation'; import { isInSystemContacts } from './isInSystemContacts'; @@ -24,8 +25,7 @@ export function isConversationAccepted( return true; } - const messageRequestEnum = - window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type; + const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; const { messageRequestResponseType } = conversationAttrs; if (messageRequestResponseType === messageRequestEnum.ACCEPT) { diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index a7db6d7c1..5454ef571 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -198,13 +198,6 @@ "reasonCategory": "falseMatch", "updated": "2020-07-21T18:34:59.251Z" }, - { - "rule": "jQuery-load(", - "path": "js/modules/stickers.js", - "line": "async function load() {", - "reasonCategory": "falseMatch", - "updated": "2019-04-26T17:48:30.675Z" - }, { "rule": "jQuery-$(", "path": "js/permissions_popup_start.js", @@ -14141,6 +14134,20 @@ "reasonCategory": "falseMatch", "updated": "2020-02-07T19:52:28.522Z" }, + { + "rule": "jQuery-load(", + "path": "ts/types/Stickers.js", + "line": "async function load() {", + "reasonCategory": "falseMatch", + "updated": "2021-07-02T02:57:58.052Z" + }, + { + "rule": "jQuery-load(", + "path": "ts/types/Stickers.ts", + "line": "export async function load(): Promise {", + "reasonCategory": "falseMatch", + "updated": "2019-04-26T17:48:30.675Z" + }, { "rule": "React-useRef", "path": "ts/util/hooks.js", @@ -14157,4 +14164,4 @@ "updated": "2021-03-18T21:41:28.361Z", "reasonDetail": "A generic hook. Typically not to be used with non-DOM values." } -] \ No newline at end of file +] diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index b2f21814e..4eafd3e4a 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -57,6 +57,7 @@ const excludedFilesRegexps = [ '^sticker-creator/dist/bundle.js', '^test/test.js', '^ts/test[^/]*/.+', + '^ts/sql/mainWorker.bundle.js', // Copied from dependency '^js/Mp3LameEncoder.min.js', diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index 22ea4af79..c04545e10 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -37,7 +37,7 @@ import { multiRecipient409ResponseSchema, multiRecipient410ResponseSchema, } from '../textsecure/WebAPI'; -import { ContentClass } from '../textsecure.d'; +import { SignalService as Proto } from '../protobuf'; import { assert } from './assert'; import { isGroupV2 } from './whatTypeOfConversation'; @@ -53,6 +53,9 @@ const MAX_CONCURRENCY = 5; // sendWithSenderKey is recursive, but we don't want to loop back too many times. const MAX_RECURSION = 5; +// TODO: remove once we move away from ArrayBuffers +const FIXMEU8 = Uint8Array; + // Public API: export async function sendToGroup({ @@ -106,7 +109,7 @@ export async function sendContentMessageToGroup({ timestamp, }: { contentHint: number; - contentMessage: ContentClass; + contentMessage: Proto.Content; conversation: ConversationModel; isPartialSend?: boolean; online?: boolean; @@ -165,7 +168,7 @@ export async function sendContentMessageToGroup({ export async function sendToGroupViaSenderKey(options: { contentHint: number; - contentMessage: ContentClass; + contentMessage: Proto.Content; conversation: ConversationModel; isPartialSend?: boolean; online?: boolean; @@ -185,9 +188,7 @@ export async function sendToGroupViaSenderKey(options: { sendOptions, timestamp, } = options; - const { - ContentHint, - } = window.textsecure.protobuf.UnidentifiedSenderMessage.Message; + const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const logId = conversation.idForLogging(); window.log.info( @@ -372,7 +373,9 @@ export async function sendToGroupViaSenderKey(options: { contentHint, devices: devicesForSenderKey, distributionId, - contentMessage: contentMessage.toArrayBuffer(), + contentMessage: toArrayBuffer( + Proto.Content.encode(contentMessage).finish() + ), groupId, }); const accessKeys = getXorOfAccessKeys(devicesForSenderKey); @@ -431,7 +434,11 @@ export async function sendToGroupViaSenderKey(options: { const normalRecipients = getUuidsFromDevices(devicesForNormalSend); if (normalRecipients.length === 0) { return { - dataMessage: contentMessage.dataMessage?.toArrayBuffer(), + dataMessage: contentMessage.dataMessage + ? toArrayBuffer( + Proto.DataMessage.encode(contentMessage.dataMessage).finish() + ) + : undefined, successfulIdentifiers: senderKeyRecipients, unidentifiedDeliveries: senderKeyRecipients, }; @@ -449,7 +456,11 @@ export async function sendToGroupViaSenderKey(options: { }); return { - dataMessage: contentMessage.dataMessage?.toArrayBuffer(), + dataMessage: contentMessage.dataMessage + ? toArrayBuffer( + Proto.DataMessage.encode(contentMessage.dataMessage).finish() + ) + : undefined, errors: normalSendResult.errors, failoverIdentifiers: normalSendResult.failoverIdentifiers, successfulIdentifiers: [ @@ -669,7 +680,7 @@ async function encryptForSenderKey({ ); const ourAddress = getOurAddress(); const senderKeyStore = new SenderKeys(); - const message = Buffer.from(padMessage(contentMessage)); + const message = Buffer.from(padMessage(new FIXMEU8(contentMessage))); const ciphertextMessage = await window.textsecure.storage.protocol.enqueueSenderKeyJob( ourAddress, diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 366b7db9e..62aed043d 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -9,7 +9,9 @@ import { InMemoryAttachmentDraftType, OnDiskAttachmentDraftType, } from '../types/Attachment'; -import { IMAGE_JPEG } from '../types/MIME'; +import type { StickerPackType as StickerPackDBType } from '../sql/Interface'; +import * as Stickers from '../types/Stickers'; +import { IMAGE_JPEG, IMAGE_WEBP } from '../types/MIME'; import { ConversationModel } from '../models/conversations'; import { GroupV2PendingMemberType, @@ -48,6 +50,7 @@ import { LinkPreviewWithDomain, } from '../types/LinkPreview'; import * as LinkPreview from '../types/LinkPreview'; +import { SignalService as Proto } from '../protobuf'; type AttachmentOptions = { messageId: string; @@ -616,8 +619,7 @@ Whisper.ConversationView = Whisper.View.extend({
`)[0]; - const messageRequestEnum = - window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type; + const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; const props = { id: model.id, @@ -845,8 +847,7 @@ Whisper.ConversationView = Whisper.View.extend({ const { model }: { model: ConversationModel } = this; const { id } = model; - const messageRequestEnum = - window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type; + const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; const contactSupport = () => { const baseUrl = @@ -1564,8 +1565,7 @@ Whisper.ConversationView = Whisper.View.extend({ }, blockAndReportSpam(model: ConversationModel): Promise { - const messageRequestEnum = - window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type; + const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; return this.longRunningTaskWrapper({ name: 'blockAndReportSpam', @@ -2202,7 +2202,7 @@ Whisper.ConversationView = Whisper.View.extend({ contentType: blob.type, data, size: data.byteLength, - flags: window.textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE, + flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE, }; // Note: The RecorderView removes itself on send @@ -2443,12 +2443,12 @@ Whisper.ConversationView = Whisper.View.extend({ : undefined; conversation.sendMessage( - null, + undefined, // body [], - null, + undefined, // quote [], stickerNoPath, - undefined, + undefined, // BodyRanges { ...sendMessageOptions, timestamp } ); } else { @@ -2469,11 +2469,11 @@ Whisper.ConversationView = Whisper.View.extend({ ); conversation.sendMessage( - messageBody || null, + messageBody || undefined, attachmentsToSend, - null, // quote + undefined, // quote preview, - null, // sticker + undefined, // sticker undefined, // BodyRanges { ...sendMessageOptions, timestamp } ); @@ -2953,14 +2953,14 @@ Whisper.ConversationView = Whisper.View.extend({ }, showStickerPackPreview(packId: string, packKey: string) { - window.Signal.Stickers.downloadEphemeralPack(packId, packKey); + Stickers.downloadEphemeralPack(packId, packKey); const props = { packId, onClose: async () => { this.stickerPreviewModalView.remove(); this.stickerPreviewModalView = null; - await window.Signal.Stickers.removeEphemeralPack(packId); + await Stickers.removeEphemeralPack(packId); }, }; @@ -3199,7 +3199,6 @@ Whisper.ConversationView = Whisper.View.extend({ JSX: window.Signal.State.Roots.createGroupLinkManagement( window.reduxStore, { - accessEnum: window.textsecure.protobuf.AccessControl.AccessRequired, changeHasGroupLink: this.changeHasGroupLink.bind(this), conversationId: model.id, copyGroupLink: this.copyGroupLink.bind(this), @@ -3224,7 +3223,6 @@ Whisper.ConversationView = Whisper.View.extend({ JSX: window.Signal.State.Roots.createGroupV2Permissions( window.reduxStore, { - accessEnum: window.textsecure.protobuf.AccessControl.AccessRequired, conversationId: model.id, setAccessControlAttributesSetting: this.setAccessControlAttributesSetting.bind( this @@ -3282,8 +3280,7 @@ Whisper.ConversationView = Whisper.View.extend({ showConversationDetails() { const { model }: { model: ConversationModel } = this; - const messageRequestEnum = - window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type; + const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; // these methods are used in more than one place and should probably be // dried up and hoisted to methods on ConversationView @@ -3303,7 +3300,7 @@ Whisper.ConversationView = Whisper.View.extend({ ); }; - const ACCESS_ENUM = window.textsecure.protobuf.AccessControl.AccessRequired; + const ACCESS_ENUM = Proto.AccessControl.AccessRequired; const hasGroupLink = Boolean( model.get('groupInviteLinkPassword') && @@ -4029,15 +4026,29 @@ Whisper.ConversationView = Whisper.View.extend({ url: string, abortSignal: Readonly ): Promise { - const isPackDownloaded = (pack: any) => - pack && (pack.status === 'downloaded' || pack.status === 'installed'); - const isPackValid = (pack: any) => - pack && - (pack.status === 'ephemeral' || - pack.status === 'downloaded' || - pack.status === 'installed'); + const isPackDownloaded = ( + pack?: StickerPackDBType + ): pack is StickerPackDBType => { + if (!pack) { + return false; + } - const dataFromLink = window.Signal.Stickers.getDataFromLink(url); + return pack.status === 'downloaded' || pack.status === 'installed'; + }; + const isPackValid = ( + pack?: StickerPackDBType + ): pack is StickerPackDBType => { + if (!pack) { + return false; + } + return ( + pack.status === 'ephemeral' || + pack.status === 'downloaded' || + pack.status === 'installed' + ); + }; + + const dataFromLink = Stickers.getDataFromLink(url); if (!dataFromLink) { return null; } @@ -4047,16 +4058,16 @@ Whisper.ConversationView = Whisper.View.extend({ const keyBytes = window.Signal.Crypto.bytesFromHexString(key); const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes); - const existing = window.Signal.Stickers.getStickerPack(id); + const existing = Stickers.getStickerPack(id); if (!isPackDownloaded(existing)) { - await window.Signal.Stickers.downloadEphemeralPack(id, keyBase64); + await Stickers.downloadEphemeralPack(id, keyBase64); } if (abortSignal.aborted) { return null; } - const pack = window.Signal.Stickers.getStickerPack(id); + const pack = Stickers.getStickerPack(id); if (!isPackValid(pack)) { return null; @@ -4083,7 +4094,7 @@ Whisper.ConversationView = Whisper.View.extend({ ...sticker, data, size: data.byteLength, - contentType: 'image/webp', + contentType: IMAGE_WEBP, }, description: null, date: null, @@ -4096,7 +4107,7 @@ Whisper.ConversationView = Whisper.View.extend({ return null; } finally { if (id) { - await window.Signal.Stickers.removeEphemeralPack(id); + await Stickers.removeEphemeralPack(id); } } }, diff --git a/ts/window.d.ts b/ts/window.d.ts index 819573a88..5c5e763ee 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -235,7 +235,6 @@ declare global { }; log: LoggerType; nodeSetImmediate: typeof setImmediate; - normalizeUuids: (obj: any, paths: Array, context: string) => void; onFullScreenChange: (fullScreen: boolean) => void; platform: string; preloadedImages: Array; @@ -311,14 +310,31 @@ declare global { loadPreviewData: (preview: unknown) => WhatIsThis; loadStickerData: (sticker: unknown) => WhatIsThis; readStickerData: (path: string) => Promise; + deleteSticker: (path: string) => Promise; + getAbsoluteStickerPath: (path: string) => string; + processNewEphemeralSticker: ( + stickerData: ArrayBuffer + ) => { + path: string; + width: number; + height: number; + }; + processNewSticker: ( + stickerData: ArrayBuffer + ) => { + path: string; + width: number; + height: number; + }; + copyIntoAttachmentsDirectory: (path: string) => Promise; upgradeMessageSchema: (attributes: unknown) => WhatIsThis; processNewAttachment: ( attachment: DownloadAttachmentType ) => Promise; copyIntoTempDirectory: any; - deleteDraftFile: any; - deleteTempFile: any; + deleteDraftFile: (path: string) => Promise; + deleteTempFile: (path: string) => Promise; getAbsoluteDraftPath: any; getAbsoluteTempPath: any; openFileInFolder: any; @@ -327,36 +343,6 @@ declare global { saveAttachmentToDisk: any; writeNewDraftData: any; }; - Stickers: { - getDataFromLink: any; - copyStickerToAttachments: ( - packId: string, - stickerId: number - ) => Promise; - deletePackReference: (id: string, packId: string) => Promise; - downloadEphemeralPack: (packId: string, key: string) => Promise; - downloadQueuedPacks: () => void; - downloadStickerPack: ( - id: string, - key: string, - options: WhatIsThis - ) => void; - getInitialState: () => WhatIsThis; - load: () => void; - removeEphemeralPack: (packId: string) => Promise; - savePackMetadata: ( - packId: string, - packKey: string, - metadata: unknown - ) => void; - getStickerPackStatus: (packId: string) => 'downloaded' | 'installed'; - getSticker: ( - packId: string, - stickerId: number - ) => typeof window.Signal.Types.Sticker; - getStickerPack: (packId: string) => WhatIsThis; - getInstalledStickerPacks: () => WhatIsThis; - }; Types: { Attachment: { save: any; @@ -548,6 +534,8 @@ declare global { // eslint-disable-next-line no-restricted-syntax interface Error { originalError?: Event; + reason?: any; + stackForLog?: string; } // Uint8Array and ArrayBuffer are type-compatible in TypeScript's covariant @@ -702,7 +690,11 @@ export type WhisperType = { TapToViewMessagesListener: WhatIsThis; deliveryReceiptQueue: PQueue; - deliveryReceiptBatcher: BatcherType; + deliveryReceiptBatcher: BatcherType<{ + source?: string; + sourceUuid?: string; + timestamp: number; + }>; RotateSignedPreKeyListener: WhatIsThis; AlreadyGroupMemberToast: typeof window.Whisper.ToastView;