From b7d56def82fd47e1c457cc6c020f112040f48bc7 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 13 Apr 2020 10:37:29 -0700 Subject: [PATCH] Moves libtextsecure to Typescript * Starting to work through lint errors * libsignal-protocol: Update changes for primary repo compatibility * Step 1: task_with_timeout rename * Step 2: Apply the changes to TaskWithTimeout.ts * Step 1: All to-be-converted libtextsecure/*.js files moved * Step 2: No Typescript errors! * Get libtextsecure tests passing again * TSLint errors down to 1 * Compilation succeeds, no lint errors or test failures * WebSocketResources - update import for case-sensitive filesystems * Fixes for lint-deps * Remove unnecessary @ts-ignore * Fix inability to message your own contact after link * Add log message for the end of migration 20 * lint fix --- Gruntfile.js | 16 - js/background.js | 2 +- libtextsecure/ProvisioningCipher.js | 78 - libtextsecure/account_manager.js | 623 ------- libtextsecure/crypto.js | 251 --- libtextsecure/errors.js | 144 -- libtextsecure/event_target.js | 82 - libtextsecure/helpers.js | 70 - libtextsecure/libsignal-protocol.js | 19 +- libtextsecure/sendmessage.js | 1364 -------------- libtextsecure/storage.js | 37 - libtextsecure/stringview.js | 104 -- libtextsecure/sync_request.js | 97 - libtextsecure/task_with_timeout.js | 72 - libtextsecure/test/contacts_parser_test.js | 10 +- libtextsecure/test/helpers_test.js | 9 +- libtextsecure/test/index.html | 15 +- .../test/websocket-resources_test.js | 58 +- libtextsecure/websocket-resources.js | 243 --- preload.js | 4 +- sticker-creator/preload.js | 2 +- ts/libsignal.d.ts | 215 +++ ts/sql/Server.ts | 1 + ts/textsecure.d.ts | 647 +++++++ ts/textsecure/AccountManager.ts | 699 ++++++++ .../textsecure/ContactsParser.ts | 67 +- ts/textsecure/Crypto.ts | 269 +++ ts/textsecure/Errors.ts | 164 ++ ts/textsecure/EventTarget.ts | 81 + ts/textsecure/Helpers.ts | 98 + .../textsecure/MessageReceiver.ts | 1020 +++++++---- .../textsecure/OutgoingMessage.ts | 398 +++-- ts/textsecure/ProvisioningCipher.ts | 111 ++ ts/textsecure/SendMessage.ts | 1583 +++++++++++++++++ ts/textsecure/Storage.ts | 49 + ts/textsecure/StringView.ts | 97 + ts/textsecure/SyncRequest.ts | 100 ++ ts/textsecure/TaskWithTimeout.ts | 78 + ts/{ => textsecure}/WebAPI.ts | 187 +- ts/textsecure/WebsocketResources.ts | 304 ++++ ts/textsecure/index.ts | 35 + ts/util/batcher.ts | 4 +- ts/util/lint/exceptions.json | 376 ++-- ts/window.d.ts | 135 +- tslint.json | 7 + 45 files changed, 5983 insertions(+), 4042 deletions(-) delete mode 100644 libtextsecure/ProvisioningCipher.js delete mode 100644 libtextsecure/account_manager.js delete mode 100644 libtextsecure/crypto.js delete mode 100644 libtextsecure/errors.js delete mode 100644 libtextsecure/event_target.js delete mode 100644 libtextsecure/helpers.js delete mode 100644 libtextsecure/sendmessage.js delete mode 100644 libtextsecure/storage.js delete mode 100644 libtextsecure/stringview.js delete mode 100644 libtextsecure/sync_request.js delete mode 100644 libtextsecure/task_with_timeout.js delete mode 100644 libtextsecure/websocket-resources.js create mode 100644 ts/libsignal.d.ts create mode 100644 ts/textsecure.d.ts create mode 100644 ts/textsecure/AccountManager.ts rename libtextsecure/contacts_parser.js => ts/textsecure/ContactsParser.ts (54%) create mode 100644 ts/textsecure/Crypto.ts create mode 100644 ts/textsecure/Errors.ts create mode 100644 ts/textsecure/EventTarget.ts create mode 100644 ts/textsecure/Helpers.ts rename libtextsecure/message_receiver.js => ts/textsecure/MessageReceiver.ts (64%) rename libtextsecure/outgoing_message.js => ts/textsecure/OutgoingMessage.ts (57%) create mode 100644 ts/textsecure/ProvisioningCipher.ts create mode 100644 ts/textsecure/SendMessage.ts create mode 100644 ts/textsecure/Storage.ts create mode 100644 ts/textsecure/StringView.ts create mode 100644 ts/textsecure/SyncRequest.ts create mode 100644 ts/textsecure/TaskWithTimeout.ts rename ts/{ => textsecure}/WebAPI.ts (90%) create mode 100644 ts/textsecure/WebsocketResources.ts create mode 100644 ts/textsecure/index.ts diff --git a/Gruntfile.js b/Gruntfile.js index 8d8724fcb6..9e5e2ccb5d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -53,28 +53,12 @@ module.exports = grunt => { footer: '})();\n', }, src: [ - 'libtextsecure/errors.js', 'libtextsecure/libsignal-protocol.js', 'libtextsecure/protocol_wrapper.js', - 'libtextsecure/crypto.js', - 'libtextsecure/storage.js', 'libtextsecure/storage/user.js', - 'libtextsecure/storage/groups.js', 'libtextsecure/storage/unprocessed.js', 'libtextsecure/protobufs.js', - 'libtextsecure/helpers.js', - 'libtextsecure/stringview.js', - 'libtextsecure/event_target.js', - 'libtextsecure/account_manager.js', - 'libtextsecure/websocket-resources.js', - 'libtextsecure/message_receiver.js', - 'libtextsecure/outgoing_message.js', - 'libtextsecure/sendmessage.js', - 'libtextsecure/sync_request.js', - 'libtextsecure/contacts_parser.js', - 'libtextsecure/ProvisioningCipher.js', - 'libtextsecure/task_with_timeout.js', ], dest: 'js/libtextsecure.js', }, diff --git a/js/background.js b/js/background.js index c616eaafed..03ca38c7b8 100644 --- a/js/background.js +++ b/js/background.js @@ -2349,7 +2349,7 @@ sourceUuid: data.sourceUuid, sourceDevice: data.sourceDevice, sent_at: data.timestamp, - received_at: data.receivedAt || Date.now(), + received_at: Date.now(), conversationId, unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, type: 'incoming', diff --git a/libtextsecure/ProvisioningCipher.js b/libtextsecure/ProvisioningCipher.js deleted file mode 100644 index 1bd61d547f..0000000000 --- a/libtextsecure/ProvisioningCipher.js +++ /dev/null @@ -1,78 +0,0 @@ -/* global libsignal, textsecure */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - function ProvisioningCipher() {} - - ProvisioningCipher.prototype = { - decrypt(provisionEnvelope) { - const masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer(); - const message = provisionEnvelope.body.toArrayBuffer(); - if (new Uint8Array(message)[0] !== 1) { - throw new Error('Bad version number on ProvisioningMessage'); - } - - const iv = message.slice(1, 16 + 1); - const mac = message.slice(message.byteLength - 32, message.byteLength); - const ivAndCiphertext = message.slice(0, message.byteLength - 32); - const ciphertext = message.slice(16 + 1, message.byteLength - 32); - - return libsignal.Curve.async - .calculateAgreement(masterEphemeral, this.keyPair.privKey) - .then(ecRes => - libsignal.HKDF.deriveSecrets( - ecRes, - new ArrayBuffer(32), - 'TextSecure Provisioning Message' - ) - ) - .then(keys => - libsignal.crypto - .verifyMAC(ivAndCiphertext, keys[1], mac, 32) - .then(() => libsignal.crypto.decrypt(keys[0], ciphertext, iv)) - ) - .then(plaintext => { - const provisionMessage = textsecure.protobuf.ProvisionMessage.decode( - plaintext - ); - const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer(); - - return libsignal.Curve.async.createKeyPair(privKey).then(keyPair => { - const ret = { - identityKeyPair: keyPair, - number: provisionMessage.number, - provisioningCode: provisionMessage.provisioningCode, - userAgent: provisionMessage.userAgent, - readReceipts: provisionMessage.readReceipts, - }; - if (provisionMessage.profileKey) { - ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); - } - return ret; - }); - }); - }, - getPublicKey() { - return Promise.resolve() - .then(() => { - if (!this.keyPair) { - return libsignal.Curve.async.generateKeyPair().then(keyPair => { - this.keyPair = keyPair; - }); - } - - return null; - }) - .then(() => this.keyPair.pubKey); - }, - }; - - libsignal.ProvisioningCipher = function ProvisioningCipherWrapper() { - const cipher = new ProvisioningCipher(); - - this.decrypt = cipher.decrypt.bind(cipher); - this.getPublicKey = cipher.getPublicKey.bind(cipher); - }; -})(); diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js deleted file mode 100644 index 9f367ed922..0000000000 --- a/libtextsecure/account_manager.js +++ /dev/null @@ -1,623 +0,0 @@ -/* global - window, - textsecure, - libsignal, - WebSocketResource, - btoa, - Signal, - getString, - libphonenumber, - Event, - ConversationController -*/ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - window.textsecure = window.textsecure || {}; - - const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000; - - function AccountManager(username, password) { - this.server = window.WebAPI.connect({ username, password }); - this.pending = Promise.resolve(); - } - - function getIdentifier(id) { - if (!id || !id.length) { - return id; - } - - const parts = id.split('.'); - if (!parts.length) { - return id; - } - - return parts[0]; - } - - AccountManager.prototype = new textsecure.EventTarget(); - AccountManager.prototype.extend({ - constructor: AccountManager, - requestVoiceVerification(number) { - return this.server.requestVerificationVoice(number); - }, - requestSMSVerification(number) { - return this.server.requestVerificationSMS(number); - }, - async encryptDeviceName(name, providedIdentityKey) { - if (!name) { - return null; - } - const identityKey = - providedIdentityKey || - (await textsecure.storage.protocol.getIdentityKeyPair()); - if (!identityKey) { - throw new Error( - 'Identity key was not provided and is not in database!' - ); - } - const encrypted = await Signal.Crypto.encryptDeviceName( - name, - identityKey.pubKey - ); - - const proto = new textsecure.protobuf.DeviceName(); - proto.ephemeralPublic = encrypted.ephemeralPublic; - proto.syntheticIv = encrypted.syntheticIv; - proto.ciphertext = encrypted.ciphertext; - - const arrayBuffer = proto.encode().toArrayBuffer(); - return Signal.Crypto.arrayBufferToBase64(arrayBuffer); - }, - async decryptDeviceName(base64) { - const identityKey = await textsecure.storage.protocol.getIdentityKeyPair(); - - const arrayBuffer = Signal.Crypto.base64ToArrayBuffer(base64); - const proto = textsecure.protobuf.DeviceName.decode(arrayBuffer); - const encrypted = { - ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(), - syntheticIv: proto.syntheticIv.toArrayBuffer(), - ciphertext: proto.ciphertext.toArrayBuffer(), - }; - - const name = await Signal.Crypto.decryptDeviceName( - encrypted, - identityKey.privKey - ); - - return name; - }, - async maybeUpdateDeviceName() { - const isNameEncrypted = textsecure.storage.user.getDeviceNameEncrypted(); - if (isNameEncrypted) { - return; - } - const deviceName = await textsecure.storage.user.getDeviceName(); - const base64 = await this.encryptDeviceName(deviceName); - - await this.server.updateDeviceName(base64); - }, - async deviceNameIsEncrypted() { - await textsecure.storage.user.setDeviceNameEncrypted(); - }, - async maybeDeleteSignalingKey() { - const key = await textsecure.storage.user.getSignalingKey(); - if (key) { - await this.server.removeSignalingKey(); - } - }, - registerSingleDevice(number, verificationCode) { - const registerKeys = this.server.registerKeys.bind(this.server); - const createAccount = this.createAccount.bind(this); - const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - const generateKeys = this.generateKeys.bind(this, 100); - const confirmKeys = this.confirmKeys.bind(this); - const registrationDone = this.registrationDone.bind(this); - return this.queueTask(() => - libsignal.KeyHelper.generateIdentityKeyPair().then( - async identityKeyPair => { - const profileKey = textsecure.crypto.getRandomBytes(32); - const accessKey = await window.Signal.Crypto.deriveAccessKey( - profileKey - ); - - return createAccount( - number, - verificationCode, - identityKeyPair, - profileKey, - null, - null, - null, - { accessKey } - ) - .then(clearSessionsAndPreKeys) - .then(generateKeys) - .then(keys => registerKeys(keys).then(() => confirmKeys(keys))) - .then(() => registrationDone({ number })); - } - ) - ); - }, - registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) { - const createAccount = this.createAccount.bind(this); - const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); - const generateKeys = this.generateKeys.bind(this, 100, progressCallback); - const confirmKeys = this.confirmKeys.bind(this); - const registrationDone = this.registrationDone.bind(this); - const registerKeys = this.server.registerKeys.bind(this.server); - const getSocket = this.server.getProvisioningSocket.bind(this.server); - const queueTask = this.queueTask.bind(this); - const provisioningCipher = new libsignal.ProvisioningCipher(); - let gotProvisionEnvelope = false; - return provisioningCipher.getPublicKey().then( - pubKey => - new Promise((resolve, reject) => { - const socket = getSocket(); - socket.onclose = event => { - window.log.info('provisioning socket closed. Code:', event.code); - if (!gotProvisionEnvelope) { - reject(new Error('websocket closed')); - } - }; - socket.onopen = () => { - window.log.info('provisioning socket open'); - }; - const wsr = new WebSocketResource(socket, { - keepalive: { path: '/v1/keepalive/provisioning' }, - handleRequest(request) { - if (request.path === '/v1/address' && request.verb === 'PUT') { - const proto = textsecure.protobuf.ProvisioningUuid.decode( - request.body - ); - setProvisioningUrl( - [ - 'tsdevice:/?uuid=', - proto.uuid, - '&pub_key=', - encodeURIComponent(btoa(getString(pubKey))), - ].join('') - ); - request.respond(200, 'OK'); - } else if ( - request.path === '/v1/message' && - request.verb === 'PUT' - ) { - const envelope = textsecure.protobuf.ProvisionEnvelope.decode( - request.body, - 'binary' - ); - request.respond(200, 'OK'); - gotProvisionEnvelope = true; - wsr.close(); - resolve( - provisioningCipher - .decrypt(envelope) - .then(provisionMessage => - queueTask(() => - confirmNumber(provisionMessage.number).then( - deviceName => { - if ( - typeof deviceName !== 'string' || - deviceName.length === 0 - ) { - throw new Error('Invalid device name'); - } - return createAccount( - provisionMessage.number, - provisionMessage.provisioningCode, - provisionMessage.identityKeyPair, - provisionMessage.profileKey, - deviceName, - provisionMessage.userAgent, - provisionMessage.readReceipts, - { uuid: provisionMessage.uuid } - ) - .then(clearSessionsAndPreKeys) - .then(generateKeys) - .then(keys => - registerKeys(keys).then(() => - confirmKeys(keys) - ) - ) - .then(() => registrationDone(provisionMessage)); - } - ) - ) - ) - ); - } else { - window.log.error('Unknown websocket message', request.path); - } - }, - }); - }) - ); - }, - refreshPreKeys() { - const generateKeys = this.generateKeys.bind(this, 100); - const registerKeys = this.server.registerKeys.bind(this.server); - - return this.queueTask(() => - this.server.getMyKeys().then(preKeyCount => { - window.log.info(`prekey count ${preKeyCount}`); - if (preKeyCount < 10) { - return generateKeys().then(registerKeys); - } - return null; - }) - ); - }, - rotateSignedPreKey() { - return this.queueTask(() => { - const signedKeyId = textsecure.storage.get('signedKeyId', 1); - if (typeof signedKeyId !== 'number') { - throw new Error('Invalid signedKeyId'); - } - - const store = textsecure.storage.protocol; - const { server, cleanSignedPreKeys } = this; - - return store - .getIdentityKeyPair() - .then( - identityKey => - libsignal.KeyHelper.generateSignedPreKey( - identityKey, - signedKeyId - ), - () => { - // We swallow any error here, because we don't want to get into - // a loop of repeated retries. - window.log.error( - 'Failed to get identity key. Canceling key rotation.' - ); - } - ) - .then(res => { - if (!res) { - return null; - } - window.log.info('Saving new signed prekey', res.keyId); - return Promise.all([ - textsecure.storage.put('signedKeyId', signedKeyId + 1), - store.storeSignedPreKey(res.keyId, res.keyPair), - server.setSignedPreKey({ - keyId: res.keyId, - publicKey: res.keyPair.pubKey, - signature: res.signature, - }), - ]) - .then(() => { - const confirmed = true; - window.log.info('Confirming new signed prekey', res.keyId); - return Promise.all([ - textsecure.storage.remove('signedKeyRotationRejected'), - store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), - ]); - }) - .then(() => cleanSignedPreKeys()); - }) - .catch(e => { - window.log.error( - 'rotateSignedPrekey error:', - e && e.stack ? e.stack : e - ); - - if ( - e instanceof Error && - e.name === 'HTTPError' && - e.code >= 400 && - e.code <= 599 - ) { - const rejections = - 1 + textsecure.storage.get('signedKeyRotationRejected', 0); - textsecure.storage.put('signedKeyRotationRejected', rejections); - window.log.error( - 'Signed key rotation rejected count:', - rejections - ); - } else { - throw e; - } - }); - }); - }, - queueTask(task) { - this.pendingQueue = - this.pendingQueue || new window.PQueue({ concurrency: 1 }); - const taskWithTimeout = textsecure.createTaskWithTimeout(task); - - return this.pendingQueue.add(taskWithTimeout); - }, - cleanSignedPreKeys() { - const MINIMUM_KEYS = 3; - const store = textsecure.storage.protocol; - return store.loadSignedPreKeys().then(allKeys => { - allKeys.sort((a, b) => (a.created_at || 0) - (b.created_at || 0)); - allKeys.reverse(); // we want the most recent first - const confirmed = allKeys.filter(key => key.confirmed); - const unconfirmed = allKeys.filter(key => !key.confirmed); - - const recent = allKeys[0] ? allKeys[0].keyId : 'none'; - const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none'; - window.log.info(`Most recent signed key: ${recent}`); - window.log.info(`Most recent confirmed signed key: ${recentConfirmed}`); - window.log.info( - 'Total signed key count:', - allKeys.length, - '-', - confirmed.length, - 'confirmed' - ); - - let confirmedCount = confirmed.length; - - // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week - confirmed.forEach((key, index) => { - if (index < MINIMUM_KEYS) { - return; - } - const createdAt = key.created_at || 0; - const age = Date.now() - createdAt; - - if (age > ARCHIVE_AGE) { - window.log.info( - 'Removing confirmed signed prekey:', - key.keyId, - 'with timestamp:', - new Date(createdAt).toJSON() - ); - store.removeSignedPreKey(key.keyId); - confirmedCount -= 1; - } - }); - - const stillNeeded = MINIMUM_KEYS - confirmedCount; - - // If we still don't have enough total keys, we keep as many unconfirmed - // keys as necessary. If not necessary, and over a week old, we drop. - unconfirmed.forEach((key, index) => { - if (index < stillNeeded) { - return; - } - - const createdAt = key.created_at || 0; - const age = Date.now() - createdAt; - if (age > ARCHIVE_AGE) { - window.log.info( - 'Removing unconfirmed signed prekey:', - key.keyId, - 'with timestamp:', - new Date(createdAt).toJSON() - ); - store.removeSignedPreKey(key.keyId); - } - }); - }); - }, - async createAccount( - number, - verificationCode, - identityKeyPair, - profileKey, - deviceName, - userAgent, - readReceipts, - options = {} - ) { - const { accessKey } = options; - let password = btoa(getString(libsignal.crypto.getRandomBytes(16))); - password = password.substring(0, password.length - 2); - const registrationId = libsignal.KeyHelper.generateRegistrationId(); - - const previousNumber = getIdentifier(textsecure.storage.get('number_id')); - const previousUuid = getIdentifier(textsecure.storage.get('uuid_id')); - - const encryptedDeviceName = await this.encryptDeviceName( - deviceName, - identityKeyPair - ); - await this.deviceNameIsEncrypted(); - - window.log.info( - `createAccount: Number is ${number}, password has length: ${ - password ? password.length : 'none' - }` - ); - - const response = await this.server.confirmCode( - number, - verificationCode, - password, - registrationId, - encryptedDeviceName, - { accessKey } - ); - - const numberChanged = previousNumber && previousNumber !== number; - const uuidChanged = - previousUuid && response.uuid && previousUuid !== response.uuid; - - if (numberChanged || uuidChanged) { - if (numberChanged) { - window.log.warn( - 'New number is different from old number; deleting all previous data' - ); - } - if (uuidChanged) { - window.log.warn( - 'New uuid is different from old uuid; deleting all previous data' - ); - } - - try { - await textsecure.storage.protocol.removeAllData(); - window.log.info('Successfully deleted previous data'); - } catch (error) { - window.log.error( - 'Something went wrong deleting data from previous number', - error && error.stack ? error.stack : error - ); - } - } - - await Promise.all([ - textsecure.storage.remove('identityKey'), - textsecure.storage.remove('password'), - textsecure.storage.remove('registrationId'), - textsecure.storage.remove('number_id'), - textsecure.storage.remove('device_name'), - textsecure.storage.remove('regionCode'), - textsecure.storage.remove('userAgent'), - textsecure.storage.remove('profileKey'), - textsecure.storage.remove('read-receipts-setting'), - ]); - - // `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called - // before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes` - // indirectly calls `ConversationController.getConverationId()` which - // initializes the conversation for the given number (our number) which - // calls out to the user storage API to get the stored UUID and number - // information. - await textsecure.storage.user.setNumberAndDeviceId( - number, - response.deviceId || 1, - deviceName - ); - - const setUuid = response.uuid; - if (setUuid) { - await textsecure.storage.user.setUuidAndDeviceId( - setUuid, - response.deviceId || 1 - ); - } - - // update our own identity key, which may have changed - // if we're relinking after a reinstall on the master device - await textsecure.storage.protocol.saveIdentityWithAttributes(number, { - publicKey: identityKeyPair.pubKey, - firstUse: true, - timestamp: Date.now(), - verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED, - nonblockingApproval: true, - }); - - await textsecure.storage.put('identityKey', identityKeyPair); - await textsecure.storage.put('password', password); - await textsecure.storage.put('registrationId', registrationId); - if (profileKey) { - await textsecure.storage.put('profileKey', profileKey); - } - if (userAgent) { - await textsecure.storage.put('userAgent', userAgent); - } - - await textsecure.storage.put( - 'read-receipt-setting', - Boolean(readReceipts) - ); - - const regionCode = libphonenumber.util.getRegionCodeForNumber(number); - await textsecure.storage.put('regionCode', regionCode); - await textsecure.storage.protocol.hydrateCaches(); - }, - async clearSessionsAndPreKeys() { - const store = textsecure.storage.protocol; - - window.log.info('clearing all sessions, prekeys, and signed prekeys'); - await Promise.all([ - store.clearPreKeyStore(), - store.clearSignedPreKeysStore(), - store.clearSessionStore(), - ]); - }, - // Takes the same object returned by generateKeys - async confirmKeys(keys) { - const store = textsecure.storage.protocol; - const key = keys.signedPreKey; - const confirmed = true; - - window.log.info('confirmKeys: confirming key', key.keyId); - await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); - }, - generateKeys(count, providedProgressCallback) { - const progressCallback = - typeof providedProgressCallback === 'function' - ? providedProgressCallback - : null; - const startId = textsecure.storage.get('maxPreKeyId', 1); - const signedKeyId = textsecure.storage.get('signedKeyId', 1); - - if (typeof startId !== 'number') { - throw new Error('Invalid maxPreKeyId'); - } - if (typeof signedKeyId !== 'number') { - throw new Error('Invalid signedKeyId'); - } - - const store = textsecure.storage.protocol; - return store.getIdentityKeyPair().then(identityKey => { - const result = { preKeys: [], identityKey: identityKey.pubKey }; - const promises = []; - - for (let keyId = startId; keyId < startId + count; keyId += 1) { - promises.push( - libsignal.KeyHelper.generatePreKey(keyId).then(res => { - store.storePreKey(res.keyId, res.keyPair); - result.preKeys.push({ - keyId: res.keyId, - publicKey: res.keyPair.pubKey, - }); - if (progressCallback) { - progressCallback(); - } - }) - ); - } - - promises.push( - libsignal.KeyHelper.generateSignedPreKey( - identityKey, - signedKeyId - ).then(res => { - store.storeSignedPreKey(res.keyId, res.keyPair); - result.signedPreKey = { - keyId: res.keyId, - publicKey: res.keyPair.pubKey, - signature: res.signature, - // server.registerKeys doesn't use keyPair, confirmKeys does - keyPair: res.keyPair, - }; - }) - ); - - textsecure.storage.put('maxPreKeyId', startId + count); - textsecure.storage.put('signedKeyId', signedKeyId + 1); - return Promise.all(promises).then(() => - // This is primarily for the signed prekey summary it logs out - this.cleanSignedPreKeys().then(() => result) - ); - }); - }, - async registrationDone({ uuid, number }) { - window.log.info('registration done'); - - // Ensure that we always have a conversation for ourself - const conversation = await ConversationController.getOrCreateAndWait( - number || uuid, - 'private' - ); - conversation.updateE164(number); - conversation.updateUuid(uuid); - - window.log.info('dispatching registration event'); - - this.dispatchEvent(new Event('registration')); - }, - }); - textsecure.AccountManager = AccountManager; -})(); diff --git a/libtextsecure/crypto.js b/libtextsecure/crypto.js deleted file mode 100644 index d90400280f..0000000000 --- a/libtextsecure/crypto.js +++ /dev/null @@ -1,251 +0,0 @@ -/* global libsignal, crypto, textsecure, dcodeIO, window */ - -/* eslint-disable more/no-then, no-bitwise */ - -// eslint-disable-next-line func-names -(function() { - const { encrypt, decrypt, calculateMAC, verifyMAC } = libsignal.crypto; - - const PROFILE_IV_LENGTH = 12; // bytes - const PROFILE_KEY_LENGTH = 32; // bytes - const PROFILE_TAG_LENGTH = 128; // bits - const PROFILE_NAME_PADDED_LENGTH = 53; // bytes - - function verifyDigest(data, theirDigest) { - return crypto.subtle.digest({ name: 'SHA-256' }, data).then(ourDigest => { - const a = new Uint8Array(ourDigest); - const b = new Uint8Array(theirDigest); - let result = 0; - for (let i = 0; i < theirDigest.byteLength; i += 1) { - result |= a[i] ^ b[i]; - } - if (result !== 0) { - throw new Error('Bad digest'); - } - }); - } - function calculateDigest(data) { - return crypto.subtle.digest({ name: 'SHA-256' }, data); - } - - window.textsecure = window.textsecure || {}; - window.textsecure.crypto = { - // Decrypts message into a raw string - decryptWebsocketMessage(message, signalingKey) { - const decodedMessage = message.toArrayBuffer(); - - if (signalingKey.byteLength !== 52) { - throw new Error('Got invalid length signalingKey'); - } - if (decodedMessage.byteLength < 1 + 16 + 10) { - throw new Error('Got invalid length message'); - } - if (new Uint8Array(decodedMessage)[0] !== 1) { - throw new Error(`Got bad version number: ${decodedMessage[0]}`); - } - - const aesKey = signalingKey.slice(0, 32); - const macKey = signalingKey.slice(32, 32 + 20); - - const iv = decodedMessage.slice(1, 1 + 16); - const ciphertext = decodedMessage.slice( - 1 + 16, - decodedMessage.byteLength - 10 - ); - const ivAndCiphertext = decodedMessage.slice( - 0, - decodedMessage.byteLength - 10 - ); - const mac = decodedMessage.slice( - decodedMessage.byteLength - 10, - decodedMessage.byteLength - ); - - return verifyMAC(ivAndCiphertext, macKey, mac, 10).then(() => - decrypt(aesKey, ciphertext, iv) - ); - }, - - decryptAttachment(encryptedBin, keys, theirDigest) { - if (keys.byteLength !== 64) { - throw new Error('Got invalid length attachment keys'); - } - if (encryptedBin.byteLength < 16 + 32) { - throw new Error('Got invalid length attachment'); - } - - const aesKey = keys.slice(0, 32); - const macKey = keys.slice(32, 64); - - const iv = encryptedBin.slice(0, 16); - const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); - const ivAndCiphertext = encryptedBin.slice( - 0, - encryptedBin.byteLength - 32 - ); - const mac = encryptedBin.slice( - encryptedBin.byteLength - 32, - encryptedBin.byteLength - ); - - return verifyMAC(ivAndCiphertext, macKey, mac, 32) - .then(() => { - if (theirDigest) { - return verifyDigest(encryptedBin, theirDigest); - } - - return null; - }) - .then(() => decrypt(aesKey, ciphertext, iv)); - }, - - encryptAttachment(plaintext, keys, iv) { - if ( - !(plaintext instanceof ArrayBuffer) && - !ArrayBuffer.isView(plaintext) - ) { - throw new TypeError( - `\`plaintext\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof plaintext}` - ); - } - - if (keys.byteLength !== 64) { - throw new Error('Got invalid length attachment keys'); - } - if (iv.byteLength !== 16) { - throw new Error('Got invalid length attachment iv'); - } - const aesKey = keys.slice(0, 32); - const macKey = keys.slice(32, 64); - - return encrypt(aesKey, plaintext, iv).then(ciphertext => { - const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); - ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set(new Uint8Array(ciphertext), 16); - - return calculateMAC(macKey, ivAndCiphertext.buffer).then(mac => { - const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32); - encryptedBin.set(ivAndCiphertext); - encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); - return calculateDigest(encryptedBin.buffer).then(digest => ({ - ciphertext: encryptedBin.buffer, - digest, - })); - }); - }); - }, - encryptProfile(data, key) { - const iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH); - if (key.byteLength !== PROFILE_KEY_LENGTH) { - throw new Error('Got invalid length profile key'); - } - if (iv.byteLength !== PROFILE_IV_LENGTH) { - throw new Error('Got invalid length profile iv'); - } - return crypto.subtle - .importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt']) - .then(keyForEncryption => - crypto.subtle - .encrypt( - { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH }, - keyForEncryption, - data - ) - .then(ciphertext => { - const ivAndCiphertext = new Uint8Array( - PROFILE_IV_LENGTH + ciphertext.byteLength - ); - ivAndCiphertext.set(new Uint8Array(iv)); - ivAndCiphertext.set( - new Uint8Array(ciphertext), - PROFILE_IV_LENGTH - ); - return ivAndCiphertext.buffer; - }) - ); - }, - decryptProfile(data, key) { - if (data.byteLength < 12 + 16 + 1) { - throw new Error(`Got too short input: ${data.byteLength}`); - } - const iv = data.slice(0, PROFILE_IV_LENGTH); - const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); - if (key.byteLength !== PROFILE_KEY_LENGTH) { - throw new Error('Got invalid length profile key'); - } - if (iv.byteLength !== PROFILE_IV_LENGTH) { - throw new Error('Got invalid length profile iv'); - } - const error = new Error(); // save stack - return crypto.subtle - .importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']) - .then(keyForEncryption => - crypto.subtle - .decrypt( - { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH }, - keyForEncryption, - ciphertext - ) - .catch(e => { - if (e.name === 'OperationError') { - // bad mac, basically. - error.message = - 'Failed to decrypt profile data. Most likely the profile key has changed.'; - error.name = 'ProfileDecryptError'; - throw error; - } - }) - ); - }, - encryptProfileName(name, key) { - const padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH); - padded.set(new Uint8Array(name)); - return textsecure.crypto.encryptProfile(padded.buffer, key); - }, - decryptProfileName(encryptedProfileName, key) { - const data = dcodeIO.ByteBuffer.wrap( - encryptedProfileName, - 'base64' - ).toArrayBuffer(); - return textsecure.crypto.decryptProfile(data, key).then(decrypted => { - const padded = new Uint8Array(decrypted); - - // Given name is the start of the string to the first null character - let givenEnd; - for (givenEnd = 0; givenEnd < padded.length; givenEnd += 1) { - if (padded[givenEnd] === 0x00) { - break; - } - } - - // Family name is the next chunk of non-null characters after that first null - let familyEnd; - for ( - familyEnd = givenEnd + 1; - familyEnd < padded.length; - familyEnd += 1 - ) { - if (padded[familyEnd] === 0x00) { - break; - } - } - const foundFamilyName = familyEnd > givenEnd + 1; - - return { - given: dcodeIO.ByteBuffer.wrap(padded) - .slice(0, givenEnd) - .toArrayBuffer(), - family: foundFamilyName - ? dcodeIO.ByteBuffer.wrap(padded) - .slice(givenEnd + 1, familyEnd) - .toArrayBuffer() - : null, - }; - }); - }, - - getRandomBytes(size) { - return libsignal.crypto.getRandomBytes(size); - }, - }; -})(); diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js deleted file mode 100644 index affa1d42db..0000000000 --- a/libtextsecure/errors.js +++ /dev/null @@ -1,144 +0,0 @@ -/* global window */ - -// eslint-disable-next-line func-names -(function() { - window.textsecure = window.textsecure || {}; - - function inherit(Parent, Child) { - // eslint-disable-next-line no-param-reassign - Child.prototype = Object.create(Parent.prototype, { - constructor: { - value: Child, - writable: true, - configurable: true, - }, - }); - } - function appendStack(newError, originalError) { - // eslint-disable-next-line no-param-reassign - newError.stack += `\nOriginal stack:\n${originalError.stack}`; - } - - function ReplayableError(options = {}) { - this.name = options.name || 'ReplayableError'; - this.message = options.message; - - Error.call(this, options.message); - - // Maintains proper stack trace, where our error was thrown (only available on V8) - // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } - - this.functionCode = options.functionCode; - } - inherit(Error, ReplayableError); - - function IncomingIdentityKeyError(identifier, message, key) { - // eslint-disable-next-line prefer-destructuring - this.identifier = identifier.split('.')[0]; - this.identityKey = key; - - ReplayableError.call(this, { - name: 'IncomingIdentityKeyError', - message: `The identity of ${this.identifier} has changed.`, - }); - } - inherit(ReplayableError, IncomingIdentityKeyError); - - function OutgoingIdentityKeyError( - identifier, - message, - timestamp, - identityKey - ) { - // eslint-disable-next-line prefer-destructuring - this.identifier = identifier.split('.')[0]; - this.identityKey = identityKey; - - ReplayableError.call(this, { - name: 'OutgoingIdentityKeyError', - message: `The identity of ${this.identifier} has changed.`, - }); - } - inherit(ReplayableError, OutgoingIdentityKeyError); - - function OutgoingMessageError(identifier, message, timestamp, httpError) { - // eslint-disable-next-line prefer-destructuring - this.identifier = identifier.split('.')[0]; - - ReplayableError.call(this, { - name: 'OutgoingMessageError', - message: httpError ? httpError.message : 'no http error', - }); - - if (httpError) { - this.code = httpError.code; - appendStack(this, httpError); - } - } - inherit(ReplayableError, OutgoingMessageError); - - function SendMessageNetworkError(identifier, jsonData, httpError) { - // eslint-disable-next-line prefer-destructuring - this.identifier = identifier.split('.')[0]; - this.code = httpError.code; - - ReplayableError.call(this, { - name: 'SendMessageNetworkError', - message: httpError.message, - }); - - appendStack(this, httpError); - } - inherit(ReplayableError, SendMessageNetworkError); - - function SignedPreKeyRotationError() { - ReplayableError.call(this, { - name: 'SignedPreKeyRotationError', - message: 'Too many signed prekey rotation failures', - }); - } - inherit(ReplayableError, SignedPreKeyRotationError); - - function MessageError(message, httpError) { - this.code = httpError.code; - - ReplayableError.call(this, { - name: 'MessageError', - message: httpError.message, - }); - - appendStack(this, httpError); - } - inherit(ReplayableError, MessageError); - - function UnregisteredUserError(identifier, httpError) { - this.message = httpError.message; - this.name = 'UnregisteredUserError'; - - Error.call(this, this.message); - - // Maintains proper stack trace, where our error was thrown (only available on V8) - // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - if (Error.captureStackTrace) { - Error.captureStackTrace(this); - } - - this.identifier = identifier; - this.code = httpError.code; - - appendStack(this, httpError); - } - inherit(Error, UnregisteredUserError); - - window.textsecure.UnregisteredUserError = UnregisteredUserError; - window.textsecure.SendMessageNetworkError = SendMessageNetworkError; - window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; - window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError; - window.textsecure.ReplayableError = ReplayableError; - window.textsecure.OutgoingMessageError = OutgoingMessageError; - window.textsecure.MessageError = MessageError; - window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError; -})(); diff --git a/libtextsecure/event_target.js b/libtextsecure/event_target.js deleted file mode 100644 index 0bfaa31c97..0000000000 --- a/libtextsecure/event_target.js +++ /dev/null @@ -1,82 +0,0 @@ -/* global window, Event, textsecure */ - -/* - * Implements EventTarget - * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget - */ -// eslint-disable-next-line func-names -(function() { - window.textsecure = window.textsecure || {}; - - function EventTarget() {} - - EventTarget.prototype = { - constructor: EventTarget, - dispatchEvent(ev) { - if (!(ev instanceof Event)) { - throw new Error('Expects an event'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - const listeners = this.listeners[ev.type]; - const results = []; - if (typeof listeners === 'object') { - for (let i = 0, max = listeners.length; i < max; i += 1) { - const listener = listeners[i]; - if (typeof listener === 'function') { - results.push(listener.call(null, ev)); - } - } - } - return results; - }, - addEventListener(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - let listeners = this.listeners[eventName]; - if (typeof listeners !== 'object') { - listeners = []; - } - listeners.push(callback); - this.listeners[eventName] = listeners; - }, - removeEventListener(eventName, callback) { - if (typeof eventName !== 'string') { - throw new Error('First argument expects a string'); - } - if (typeof callback !== 'function') { - throw new Error('Second argument expects a function'); - } - if (this.listeners === null || typeof this.listeners !== 'object') { - this.listeners = {}; - } - const listeners = this.listeners[eventName]; - if (typeof listeners === 'object') { - for (let i = 0; i < listeners.length; i += 1) { - if (listeners[i] === callback) { - listeners.splice(i, 1); - return; - } - } - } - this.listeners[eventName] = listeners; - }, - extend(obj) { - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const prop in obj) { - this[prop] = obj[prop]; - } - return this; - }, - }; - - textsecure.EventTarget = EventTarget; -})(); diff --git a/libtextsecure/helpers.js b/libtextsecure/helpers.js deleted file mode 100644 index ffa4b59dcb..0000000000 --- a/libtextsecure/helpers.js +++ /dev/null @@ -1,70 +0,0 @@ -/* global window, dcodeIO */ - -/* eslint-disable no-proto, no-restricted-syntax, guard-for-in */ - -window.textsecure = window.textsecure || {}; - -/** ******************************* - *** Type conversion utilities *** - ******************************** */ -// Strings/arrays -// TODO: Throw all this shit in favor of consistent types -// TODO: Namespace -const StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__; -const StaticArrayBufferProto = new ArrayBuffer().__proto__; -const StaticUint8ArrayProto = new Uint8Array().__proto__; -function getString(thing) { - if (thing === Object(thing)) { - if (thing.__proto__ === StaticUint8ArrayProto) - return String.fromCharCode.apply(null, thing); - if (thing.__proto__ === StaticArrayBufferProto) - return getString(new Uint8Array(thing)); - if (thing.__proto__ === StaticByteBufferProto) - return thing.toString('binary'); - } - return thing; -} - -function getStringable(thing) { - return ( - typeof thing === 'string' || - typeof thing === 'number' || - typeof thing === 'boolean' || - (thing === Object(thing) && - (thing.__proto__ === StaticArrayBufferProto || - thing.__proto__ === StaticUint8ArrayProto || - thing.__proto__ === StaticByteBufferProto)) - ); -} - -// Number formatting utils -window.textsecure.utils = (() => { - const self = {}; - self.unencodeNumber = number => number.split('.'); - self.isNumberSane = number => - number[0] === '+' && /^[0-9]+$/.test(number.substring(1)); - - /** ************************ - *** JSON'ing Utilities *** - ************************* */ - function ensureStringed(thing) { - if (getStringable(thing)) return getString(thing); - else if (thing instanceof Array) { - const res = []; - for (let i = 0; i < thing.length; i += 1) - res[i] = ensureStringed(thing[i]); - return res; - } else if (thing === Object(thing)) { - const res = {}; - for (const key in thing) res[key] = ensureStringed(thing[key]); - return res; - } else if (thing === null) { - return null; - } - throw new Error(`unsure of how to jsonify object of type ${typeof thing}`); - } - - self.jsonThing = thing => JSON.stringify(ensureStringed(thing)); - - return self; -})(); diff --git a/libtextsecure/libsignal-protocol.js b/libtextsecure/libsignal-protocol.js index e41fa5a70c..cbb2b7080d 100644 --- a/libtextsecure/libsignal-protocol.js +++ b/libtextsecure/libsignal-protocol.js @@ -36105,7 +36105,6 @@ SessionCipher.prototype = { var ourIdentityKeyBuffer = util.toArrayBuffer(ourIdentityKey.pubKey); var theirIdentityKey = util.toArrayBuffer(session.indexInfo.remoteIdentityKey); var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1); - macInput.set(new Uint8Array(ourIdentityKeyBuffer)); macInput.set(new Uint8Array(theirIdentityKey), 33); macInput[33*2] = (3 << 4) | 3; @@ -36512,10 +36511,20 @@ Internal.SessionLock = {}; var jobQueue = {}; Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJob) { - jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 }); - var queue = jobQueue[number]; + if (window.PQueue) { + jobQueue[number] = jobQueue[number] || new window.PQueue({ concurrency: 1 }); + var queue = jobQueue[number]; + return queue.add(runJob); + } - return queue.add(runJob); + var runPrevious = jobQueue[number] || Promise.resolve(); + var runCurrent = jobQueue[number] = runPrevious.then(runJob, runJob); + runCurrent.then(function() { + if (jobQueue[number] === runCurrent) { + delete jobQueue[number]; + } + }); + return runCurrent; }; })(); @@ -36555,7 +36564,7 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ let i = 0; let buf = new Uint8Array(16); - uuid.replace(/[0-9A-F]{2}/ig, oct => { + uuid.replace(/[0-9A-F]{2}/ig, function(oct) { buf[i++] = parseInt(oct, 16); }); diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js deleted file mode 100644 index 1bc74cf2a0..0000000000 --- a/libtextsecure/sendmessage.js +++ /dev/null @@ -1,1364 +0,0 @@ -// eslint-disable-next-line max-len -/* global _, textsecure, WebAPI, libsignal, OutgoingMessage, window, dcodeIO, ConversationController */ - -/* eslint-disable more/no-then, no-bitwise */ - -function stringToArrayBuffer(str) { - 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; -} -function hexStringToArrayBuffer(string) { - return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer(); -} -function base64ToArrayBuffer(string) { - return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer(); -} - -function Message(options) { - this.attachments = options.attachments || []; - this.body = options.body; - this.expireTimer = options.expireTimer; - this.flags = options.flags; - this.group = options.group; - this.needsSync = options.needsSync; - this.preview = options.preview; - this.profileKey = options.profileKey; - this.quote = options.quote; - this.recipients = options.recipients; - this.sticker = options.sticker; - this.reaction = options.reaction; - this.timestamp = options.timestamp; - - if (!(this.recipients instanceof Array)) { - throw new Error('Invalid recipient list'); - } - - if (!this.group && this.recipients.length !== 1) { - throw new Error('Invalid recipient list for non-group'); - } - - if (typeof this.timestamp !== 'number') { - throw new Error('Invalid timestamp'); - } - - if (this.expireTimer !== undefined && this.expireTimer !== null) { - if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) { - throw new Error('Invalid expireTimer'); - } - } - - if (this.attachments) { - if (!(this.attachments instanceof Array)) { - throw new Error('Invalid message attachments'); - } - } - if (this.flags !== undefined) { - if (typeof this.flags !== 'number') { - throw new Error('Invalid message flags'); - } - } - if (this.isEndSession()) { - if ( - this.body !== null || - this.group !== null || - this.attachments.length !== 0 - ) { - throw new Error('Invalid end session message'); - } - } else { - if ( - typeof this.timestamp !== 'number' || - (this.body && typeof this.body !== 'string') - ) { - throw new Error('Invalid message body'); - } - if (this.group) { - if ( - typeof this.group.id !== 'string' || - typeof this.group.type !== 'number' - ) { - throw new Error('Invalid group context'); - } - } - } -} - -Message.prototype = { - constructor: Message, - isEndSession() { - return this.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION; - }, - toProto() { - if (this.dataMessage instanceof textsecure.protobuf.DataMessage) { - return this.dataMessage; - } - const proto = new textsecure.protobuf.DataMessage(); - - proto.timestamp = this.timestamp; - proto.attachments = this.attachmentPointers; - - if (this.body) { - proto.body = this.body; - } - if (this.flags) { - proto.flags = this.flags; - } - if (this.group) { - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(this.group.id); - proto.group.type = this.group.type; - } - if (this.sticker) { - proto.sticker = new textsecure.protobuf.DataMessage.Sticker(); - proto.sticker.packId = hexStringToArrayBuffer(this.sticker.packId); - proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey); - proto.sticker.stickerId = this.sticker.stickerId; - - if (this.sticker.attachmentPointer) { - proto.sticker.data = this.sticker.attachmentPointer; - } - } - if (this.reaction) { - proto.reaction = this.reaction; - } - if (Array.isArray(this.preview)) { - proto.preview = this.preview.map(preview => { - const item = new textsecure.protobuf.DataMessage.Preview(); - item.title = preview.title; - item.url = preview.url; - item.image = preview.image || null; - return item; - }); - } - if (this.quote) { - const { QuotedAttachment } = textsecure.protobuf.DataMessage.Quote; - const { Quote } = textsecure.protobuf.DataMessage; - - proto.quote = new Quote(); - const { quote } = proto; - - quote.id = this.quote.id; - quote.author = this.quote.author; - quote.text = this.quote.text; - quote.attachments = (this.quote.attachments || []).map(attachment => { - const quotedAttachment = new QuotedAttachment(); - - quotedAttachment.contentType = attachment.contentType; - quotedAttachment.fileName = attachment.fileName; - if (attachment.attachmentPointer) { - quotedAttachment.thumbnail = attachment.attachmentPointer; - } - - return quotedAttachment; - }); - } - if (this.expireTimer) { - proto.expireTimer = this.expireTimer; - } - if (this.profileKey) { - proto.profileKey = this.profileKey; - } - - this.dataMessage = proto; - return proto; - }, - toArrayBuffer() { - return this.toProto().toArrayBuffer(); - }, -}; - -function MessageSender(username, password) { - this.server = WebAPI.connect({ username, password }); - this.pendingMessages = {}; -} - -MessageSender.prototype = { - constructor: MessageSender, - - _getAttachmentSizeBucket(size) { - return Math.max( - 541, - Math.floor(1.05 ** Math.ceil(Math.log(size) / Math.log(1.05))) - ); - }, - - getPaddedAttachment(data) { - const size = data.byteLength; - const paddedSize = this._getAttachmentSizeBucket(size); - const padding = window.Signal.Crypto.getZeroes(paddedSize - size); - - return window.Signal.Crypto.concatenateBytes(data, padding); - }, - - async makeAttachmentPointer(attachment) { - if (typeof attachment !== 'object' || attachment == null) { - return Promise.resolve(undefined); - } - - const { data, size } = attachment; - if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) { - throw new Error( - `makeAttachmentPointer: data was a '${typeof data}' instead of ArrayBuffer/ArrayBufferView` - ); - } - if (data.byteLength !== size) { - throw new Error( - `makeAttachmentPointer: Size ${size} did not match data.byteLength ${data.byteLength}` - ); - } - - const padded = this.getPaddedAttachment(data); - const key = libsignal.crypto.getRandomBytes(64); - const iv = libsignal.crypto.getRandomBytes(16); - - const result = await textsecure.crypto.encryptAttachment(padded, key, iv); - const id = await this.server.putAttachment(result.ciphertext); - - const proto = new textsecure.protobuf.AttachmentPointer(); - proto.id = id; - proto.contentType = attachment.contentType; - proto.key = key; - proto.size = attachment.size; - proto.digest = result.digest; - - if (attachment.fileName) { - proto.fileName = attachment.fileName; - } - if (attachment.flags) { - proto.flags = attachment.flags; - } - if (attachment.width) { - proto.width = attachment.width; - } - if (attachment.height) { - proto.height = attachment.height; - } - if (attachment.caption) { - proto.caption = attachment.caption; - } - - return proto; - }, - - async queueJobForIdentifier(identifier, runJob) { - const { id } = await ConversationController.getOrCreateAndWait( - identifier, - 'private' - ); - this.pendingMessages[id] = - this.pendingMessages[id] || new window.PQueue({ concurrency: 1 }); - - const queue = this.pendingMessages[id]; - - const taskWithTimeout = textsecure.createTaskWithTimeout( - runJob, - `queueJobForIdentifier ${identifier} ${id}` - ); - - return queue.add(taskWithTimeout); - }, - - uploadAttachments(message) { - return Promise.all( - message.attachments.map(this.makeAttachmentPointer.bind(this)) - ) - .then(attachmentPointers => { - // eslint-disable-next-line no-param-reassign - message.attachmentPointers = attachmentPointers; - }) - .catch(error => { - if (error instanceof Error && error.name === 'HTTPError') { - throw new textsecure.MessageError(message, error); - } else { - throw error; - } - }); - }, - - async uploadLinkPreviews(message) { - try { - const preview = await Promise.all( - (message.preview || []).map(async item => ({ - ...item, - image: await this.makeAttachmentPointer(item.image), - })) - ); - // eslint-disable-next-line no-param-reassign - message.preview = preview; - } catch (error) { - if (error instanceof Error && error.name === 'HTTPError') { - throw new textsecure.MessageError(message, error); - } else { - throw error; - } - } - }, - - async uploadSticker(message) { - try { - const { sticker } = message; - - if (!sticker || !sticker.data) { - return; - } - - // eslint-disable-next-line no-param-reassign - message.sticker = { - ...sticker, - attachmentPointer: await this.makeAttachmentPointer(sticker.data), - }; - } catch (error) { - if (error instanceof Error && error.name === 'HTTPError') { - throw new textsecure.MessageError(message, error); - } else { - throw error; - } - } - }, - - uploadThumbnails(message) { - const makePointer = this.makeAttachmentPointer.bind(this); - const { quote } = message; - - if (!quote || !quote.attachments || quote.attachments.length === 0) { - return Promise.resolve(); - } - - return Promise.all( - quote.attachments.map(attachment => { - const { thumbnail } = attachment; - if (!thumbnail) { - return null; - } - - return makePointer(thumbnail).then(pointer => { - // eslint-disable-next-line no-param-reassign - attachment.attachmentPointer = pointer; - }); - }) - ).catch(error => { - if (error instanceof Error && error.name === 'HTTPError') { - throw new textsecure.MessageError(message, error); - } else { - throw error; - } - }); - }, - - sendMessage(attrs, options) { - const message = new Message(attrs); - const silent = false; - - return Promise.all([ - this.uploadAttachments(message), - this.uploadThumbnails(message), - this.uploadLinkPreviews(message), - this.uploadSticker(message), - ]).then( - () => - new Promise((resolve, reject) => { - this.sendMessageProto( - message.timestamp, - message.recipients || [], - message.toProto(), - res => { - res.dataMessage = message.toArrayBuffer(); - if (res.errors.length > 0) { - reject(res); - } else { - resolve(res); - } - }, - silent, - options - ); - }) - ); - }, - sendMessageProto( - timestamp, - recipients, - messageProto, - callback, - silent, - options = {} - ) { - const rejections = textsecure.storage.get('signedKeyRotationRejected', 0); - if (rejections > 5) { - throw new textsecure.SignedPreKeyRotationError( - recipients, - messageProto.toArrayBuffer(), - timestamp - ); - } - - const outgoing = new OutgoingMessage( - this.server, - timestamp, - recipients, - messageProto, - silent, - callback, - options - ); - - recipients.forEach(identifier => { - this.queueJobForIdentifier(identifier, () => - outgoing.sendToIdentifier(identifier) - ); - }); - }, - - sendMessageProtoAndWait( - timestamp, - identifiers, - messageProto, - silent, - options = {} - ) { - return new Promise((resolve, reject) => { - const callback = result => { - if (result && result.errors && result.errors.length > 0) { - return reject(result); - } - - return resolve(result); - }; - - this.sendMessageProto( - timestamp, - identifiers, - messageProto, - callback, - silent, - options - ); - }); - }, - - sendIndividualProto(identifier, proto, timestamp, silent, options = {}) { - return new Promise((resolve, reject) => { - const callback = res => { - if (res && res.errors && res.errors.length > 0) { - reject(res); - } else { - resolve(res); - } - }; - this.sendMessageProto( - timestamp, - [identifier], - proto, - callback, - silent, - options - ); - }); - }, - - createSyncMessage() { - const syncMessage = new textsecure.protobuf.SyncMessage(); - - // Generate a random int from 1 and 512 - const buffer = libsignal.crypto.getRandomBytes(1); - const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; - - // Generate a random padding buffer of the chosen size - syncMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); - - return syncMessage; - }, - - sendSyncMessage( - encodedDataMessage, - timestamp, - destination, - destinationUuid, - expirationStartTimestamp, - sentTo = [], - unidentifiedDeliveries = [], - isUpdate = false, - options - ) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - - if (myDevice === 1 || myDevice === '1') { - return Promise.resolve(); - } - - const dataMessage = textsecure.protobuf.DataMessage.decode( - encodedDataMessage - ); - const sentMessage = new textsecure.protobuf.SyncMessage.Sent(); - sentMessage.timestamp = timestamp; - sentMessage.message = dataMessage; - if (destination) { - sentMessage.destination = destination; - } - if (destinationUuid) { - sentMessage.destinationUuid = destinationUuid; - } - if (expirationStartTimestamp) { - sentMessage.expirationStartTimestamp = expirationStartTimestamp; - } - - const unidentifiedLookup = unidentifiedDeliveries.reduce( - (accumulator, item) => { - // eslint-disable-next-line no-param-reassign - accumulator[item] = true; - return accumulator; - }, - Object.create(null) - ); - - if (isUpdate) { - sentMessage.isRecipientUpdate = true; - } - - // Though this field has 'unidenified' in the name, it should have entries for each - // number we sent to. - if (sentTo && sentTo.length) { - sentMessage.unidentifiedStatus = sentTo.map(identifier => { - const status = new textsecure.protobuf.SyncMessage.Sent.UnidentifiedDeliveryStatus(); - const conv = ConversationController.get(identifier); - if (conv && conv.get('e164')) { - status.destination = conv.get('e164'); - } - if (conv && conv.get('uuid')) { - status.destinationUuid = conv.get('uuid'); - } - status.unidentified = Boolean(unidentifiedLookup[identifier]); - return status; - }); - } - - const syncMessage = this.createSyncMessage(); - syncMessage.sent = sentMessage; - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - timestamp, - silent, - options - ); - }, - - async getProfile(number, { accessKey } = {}) { - if (accessKey) { - return this.server.getProfileUnauth(number, { accessKey }); - } - - return this.server.getProfile(number); - }, - - getAvatar(path) { - return this.server.getAvatar(path); - }, - - getSticker(packId, stickerId) { - return this.server.getSticker(packId, stickerId); - }, - getStickerPackManifest(packId) { - return this.server.getStickerPackManifest(packId); - }, - - sendRequestBlockSyncMessage(options) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { - const request = new textsecure.protobuf.SyncMessage.Request(); - request.type = textsecure.protobuf.SyncMessage.Request.Type.BLOCKED; - const syncMessage = this.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - Date.now(), - silent, - options - ); - } - - return Promise.resolve(); - }, - - sendRequestConfigurationSyncMessage(options) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { - const request = new textsecure.protobuf.SyncMessage.Request(); - request.type = textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION; - const syncMessage = this.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - Date.now(), - silent, - options - ); - } - - return Promise.resolve(); - }, - - sendRequestGroupSyncMessage(options) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { - const request = new textsecure.protobuf.SyncMessage.Request(); - request.type = textsecure.protobuf.SyncMessage.Request.Type.GROUPS; - const syncMessage = this.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - Date.now(), - silent, - options - ); - } - - return Promise.resolve(); - }, - - sendRequestContactSyncMessage(options) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - - const myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { - const request = new textsecure.protobuf.SyncMessage.Request(); - request.type = textsecure.protobuf.SyncMessage.Request.Type.CONTACTS; - const syncMessage = this.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - Date.now(), - silent, - options - ); - } - - return Promise.resolve(); - }, - - async sendTypingMessage(options = {}, sendOptions = {}) { - const ACTION_ENUM = textsecure.protobuf.TypingMessage.Action; - const { recipientId, groupId, groupNumbers, isTyping, timestamp } = options; - - // We don't want to send typing messages to our other devices, but we will - // in the group case. - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - if (recipientId && (myNumber === recipientId || myUuid === recipientId)) { - return null; - } - - if (!recipientId && !groupId) { - throw new Error('Need to provide either recipientId or groupId!'); - } - - const recipients = groupId - ? _.without(groupNumbers, myNumber, myUuid) - : [recipientId]; - const groupIdBuffer = groupId - ? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId) - : null; - - const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED; - const finalTimestamp = timestamp || Date.now(); - - const typingMessage = new textsecure.protobuf.TypingMessage(); - typingMessage.groupId = groupIdBuffer; - typingMessage.action = action; - typingMessage.timestamp = finalTimestamp; - - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.typingMessage = typingMessage; - - const silent = true; - const online = true; - - return this.sendMessageProtoAndWait( - finalTimestamp, - recipients, - contentMessage, - silent, - { - ...sendOptions, - online, - } - ); - }, - - sendDeliveryReceipt(recipientE164, recipientUuid, timestamps, options) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - if ( - (myNumber === recipientE164 || myUuid === recipientUuid) && - (myDevice === 1 || myDevice === '1') - ) { - return Promise.resolve(); - } - - const receiptMessage = new textsecure.protobuf.ReceiptMessage(); - receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.DELIVERY; - receiptMessage.timestamp = timestamps; - - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.receiptMessage = receiptMessage; - - const silent = true; - return this.sendIndividualProto( - recipientUuid || recipientE164, - contentMessage, - Date.now(), - silent, - options - ); - }, - - sendReadReceipts(senderE164, senderUuid, timestamps, options) { - const receiptMessage = new textsecure.protobuf.ReceiptMessage(); - receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ; - receiptMessage.timestamp = timestamps; - - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.receiptMessage = receiptMessage; - - const silent = true; - return this.sendIndividualProto( - senderUuid || senderE164, - contentMessage, - Date.now(), - silent, - options - ); - }, - syncReadMessages(reads, options) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice !== 1 && myDevice !== '1') { - const syncMessage = this.createSyncMessage(); - syncMessage.read = []; - for (let i = 0; i < reads.length; i += 1) { - const read = new textsecure.protobuf.SyncMessage.Read(); - read.timestamp = reads[i].timestamp; - read.sender = reads[i].sender; - - syncMessage.read.push(read); - } - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - Date.now(), - silent, - options - ); - } - - return Promise.resolve(); - }, - - async syncViewOnceOpen(sender, senderUuid, timestamp, options) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { - return null; - } - - const syncMessage = this.createSyncMessage(); - - const viewOnceOpen = new textsecure.protobuf.SyncMessage.ViewOnceOpen(); - viewOnceOpen.sender = sender; - viewOnceOpen.senderUuid = senderUuid; - viewOnceOpen.timestamp = timestamp; - syncMessage.viewOnceOpen = viewOnceOpen; - - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - Date.now(), - silent, - options - ); - }, - - async sendStickerPackSync(operations, options) { - const myDevice = textsecure.storage.user.getDeviceId(); - if (myDevice === 1 || myDevice === '1') { - return null; - } - - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const ENUM = textsecure.protobuf.SyncMessage.StickerPackOperation.Type; - - const packOperations = operations.map(item => { - const { packId, packKey, installed } = item; - - const operation = new textsecure.protobuf.SyncMessage.StickerPackOperation(); - operation.packId = hexStringToArrayBuffer(packId); - operation.packKey = base64ToArrayBuffer(packKey); - operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE; - - return operation; - }); - - const syncMessage = this.createSyncMessage(); - syncMessage.stickerPackOperation = packOperations; - - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.syncMessage = syncMessage; - - const silent = true; - return this.sendIndividualProto( - myUuid || myNumber, - contentMessage, - Date.now(), - silent, - options - ); - }, - - syncVerification( - destinationE164, - destinationUuid, - state, - identityKey, - options - ) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const myDevice = textsecure.storage.user.getDeviceId(); - const now = Date.now(); - - if (myDevice === 1 || myDevice === '1') { - return Promise.resolve(); - } - - // First send a null message to mask the sync message. - const nullMessage = new textsecure.protobuf.NullMessage(); - - // Generate a random int from 1 and 512 - const buffer = libsignal.crypto.getRandomBytes(1); - const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; - - // Generate a random padding buffer of the chosen size - nullMessage.padding = libsignal.crypto.getRandomBytes(paddingLength); - - const contentMessage = new textsecure.protobuf.Content(); - contentMessage.nullMessage = nullMessage; - - // We want the NullMessage to look like a normal outgoing message; not silent - const silent = false; - const promise = this.sendIndividualProto( - destinationUuid || destinationE164, - contentMessage, - now, - silent, - options - ); - - return promise.then(() => { - const verified = new textsecure.protobuf.Verified(); - verified.state = state; - if (destinationE164) { - verified.destination = destinationE164; - } - if (destinationUuid) { - verified.destinationUuid = destinationUuid; - } - verified.identityKey = identityKey; - verified.nullMessage = nullMessage.padding; - - const syncMessage = this.createSyncMessage(); - syncMessage.verified = verified; - - const secondMessage = new textsecure.protobuf.Content(); - secondMessage.syncMessage = syncMessage; - - const innerSilent = true; - return this.sendIndividualProto( - myUuid || myNumber, - secondMessage, - now, - innerSilent, - options - ); - }); - }, - - sendGroupProto( - providedIdentifiers, - proto, - timestamp = Date.now(), - options = {} - ) { - const myE164 = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const identifiers = providedIdentifiers.filter( - id => id !== myE164 && id !== myUuid - ); - - if (identifiers.length === 0) { - return Promise.resolve({ - successfulIdentifiers: [], - failoverIdentifiers: [], - errors: [], - unidentifiedDeliveries: [], - dataMessage: proto.toArrayBuffer(), - }); - } - - return new Promise((resolve, reject) => { - const silent = true; - const callback = res => { - res.dataMessage = proto.toArrayBuffer(); - if (res.errors.length > 0) { - reject(res); - } else { - resolve(res); - } - }; - - this.sendMessageProto( - timestamp, - providedIdentifiers, - proto, - callback, - silent, - options - ); - }); - }, - - async getMessageProto( - destination, - body, - attachments, - quote, - preview, - sticker, - reaction, - timestamp, - expireTimer, - profileKey, - flags - ) { - const attributes = { - recipients: [destination], - destination, - body, - timestamp, - attachments, - quote, - preview, - sticker, - reaction, - expireTimer, - profileKey, - flags, - }; - - return this.getMessageProtoObj(attributes); - }, - - async getMessageProtoObj(attributes) { - const message = new Message(attributes); - await Promise.all([ - this.uploadAttachments(message), - this.uploadThumbnails(message), - this.uploadLinkPreviews(message), - this.uploadSticker(message), - ]); - - return message.toArrayBuffer(); - }, - - sendMessageToIdentifier( - identifier, - messageText, - attachments, - quote, - preview, - sticker, - reaction, - timestamp, - expireTimer, - profileKey, - options - ) { - return this.sendMessage( - { - recipients: [identifier], - body: messageText, - timestamp, - attachments, - quote, - preview, - sticker, - reaction, - expireTimer, - profileKey, - }, - options - ); - }, - - resetSession(identifier, timestamp, options) { - window.log.info('resetting secure session'); - const silent = false; - const proto = new textsecure.protobuf.DataMessage(); - proto.body = 'TERMINATE'; - proto.flags = textsecure.protobuf.DataMessage.Flags.END_SESSION; - - const logError = prefix => error => { - window.log.error(prefix, error && error.stack ? error.stack : error); - throw error; - }; - const deleteAllSessions = targetNumber => - textsecure.storage.protocol.getDeviceIds(targetNumber).then(deviceIds => - Promise.all( - deviceIds.map(deviceId => { - const address = new libsignal.SignalProtocolAddress( - targetNumber, - deviceId - ); - window.log.info('deleting sessions for', address.toString()); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - return sessionCipher.deleteAllSessionsForDevice(); - }) - ) - ); - - const sendToContactPromise = deleteAllSessions(identifier) - .catch(logError('resetSession/deleteAllSessions1 error:')) - .then(() => { - window.log.info( - 'finished closing local sessions, now sending to contact' - ); - return this.sendIndividualProto( - identifier, - proto, - timestamp, - silent, - options - ).catch(logError('resetSession/sendToContact error:')); - }) - .then(() => - deleteAllSessions(identifier).catch( - logError('resetSession/deleteAllSessions2 error:') - ) - ); - - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - // We already sent the reset session to our other devices in the code above! - if (identifier === myNumber || identifier === myUuid) { - return sendToContactPromise; - } - - const buffer = proto.toArrayBuffer(); - const sendSyncPromise = this.sendSyncMessage( - buffer, - timestamp, - identifier, - null, - [], - [], - options - ).catch(logError('resetSession/sendSync error:')); - - return Promise.all([sendToContactPromise, sendSyncPromise]); - }, - - async sendMessageToGroup( - groupId, - recipients, - messageText, - attachments, - quote, - preview, - sticker, - reaction, - timestamp, - expireTimer, - profileKey, - options - ) { - const myE164 = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getNumber(); - const attrs = { - recipients: recipients.filter(r => r !== myE164 && r !== myUuid), - body: messageText, - timestamp, - attachments, - quote, - preview, - sticker, - reaction, - expireTimer, - profileKey, - group: { - id: groupId, - type: textsecure.protobuf.GroupContext.Type.DELIVER, - }, - }; - - if (recipients.length === 0) { - return Promise.resolve({ - successfulIdentifiers: [], - failoverIdentifiers: [], - errors: [], - unidentifiedDeliveries: [], - dataMessage: await this.getMessageProtoObj(attrs), - }); - } - - return this.sendMessage(attrs, options); - }, - - createGroup(targetIdentifiers, id, name, avatar, options) { - const proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(id); - - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - // TODO - proto.group.members = targetIdentifiers; - proto.group.name = name; - - return this.makeAttachmentPointer(avatar).then(attachment => { - proto.group.avatar = attachment; - return this.sendGroupProto( - targetIdentifiers, - proto, - Date.now(), - options - ).then(() => proto.group.id); - }); - }, - - updateGroup(groupId, name, avatar, targetIdentifiers, options) { - const proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.name = name; - proto.group.members = targetIdentifiers; - - return this.makeAttachmentPointer(avatar).then(attachment => { - proto.group.avatar = attachment; - return this.sendGroupProto( - targetIdentifiers, - proto, - Date.now(), - options - ).then(() => proto.group.id); - }); - }, - - addIdentifierToGroup(groupId, newIdentifiers, options) { - const proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.members = newIdentifiers; - return this.sendGroupProto(newIdentifiers, proto, Date.now(), options); - }, - - setGroupName(groupId, name, groupIdentifiers, options) { - const proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.name = name; - proto.group.members = groupIdentifiers; - - return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options); - }, - - setGroupAvatar(groupId, avatar, groupIdentifiers, options) { - const proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.UPDATE; - proto.group.members = groupIdentifiers; - - return this.makeAttachmentPointer(avatar).then(attachment => { - proto.group.avatar = attachment; - return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options); - }); - }, - - leaveGroup(groupId, groupIdentifiers, options) { - const proto = new textsecure.protobuf.DataMessage(); - proto.group = new textsecure.protobuf.GroupContext(); - proto.group.id = stringToArrayBuffer(groupId); - proto.group.type = textsecure.protobuf.GroupContext.Type.QUIT; - return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options); - }, - async sendExpirationTimerUpdateToGroup( - groupId, - groupIdentifiers, - expireTimer, - timestamp, - profileKey, - options - ) { - const myNumber = textsecure.storage.user.getNumber(); - const myUuid = textsecure.storage.user.getUuid(); - const recipients = groupIdentifiers.filter( - identifier => identifier !== myNumber && identifier !== myUuid - ); - const attrs = { - recipients, - timestamp, - expireTimer, - profileKey, - flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, - group: { - id: groupId, - type: textsecure.protobuf.GroupContext.Type.DELIVER, - }, - }; - - if (recipients.length === 0) { - return Promise.resolve({ - successfulIdentifiers: [], - failoverIdentifiers: [], - errors: [], - unidentifiedDeliveries: [], - dataMessage: await this.getMessageProtoObj(attrs), - }); - } - - return this.sendMessage(attrs, options); - }, - sendExpirationTimerUpdateToIdentifier( - identifier, - expireTimer, - timestamp, - profileKey, - options - ) { - return this.sendMessage( - { - recipients: [identifier], - timestamp, - expireTimer, - profileKey, - flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, - }, - options - ); - }, - makeProxiedRequest(url, options) { - return this.server.makeProxiedRequest(url, options); - }, -}; - -window.textsecure = window.textsecure || {}; - -textsecure.MessageSender = function MessageSenderWrapper(username, password) { - const sender = new MessageSender(username, password); - - this.sendExpirationTimerUpdateToIdentifier = sender.sendExpirationTimerUpdateToIdentifier.bind( - sender - ); - this.sendExpirationTimerUpdateToGroup = sender.sendExpirationTimerUpdateToGroup.bind( - sender - ); - this.sendRequestGroupSyncMessage = sender.sendRequestGroupSyncMessage.bind( - sender - ); - this.sendRequestContactSyncMessage = sender.sendRequestContactSyncMessage.bind( - sender - ); - this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind( - sender - ); - this.sendRequestBlockSyncMessage = sender.sendRequestBlockSyncMessage.bind( - sender - ); - - this.sendMessageToIdentifier = sender.sendMessageToIdentifier.bind(sender); - this.sendMessage = sender.sendMessage.bind(sender); - this.resetSession = sender.resetSession.bind(sender); - this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender); - this.sendTypingMessage = sender.sendTypingMessage.bind(sender); - this.createGroup = sender.createGroup.bind(sender); - this.updateGroup = sender.updateGroup.bind(sender); - this.addIdentifierToGroup = sender.addIdentifierToGroup.bind(sender); - this.setGroupName = sender.setGroupName.bind(sender); - this.setGroupAvatar = sender.setGroupAvatar.bind(sender); - this.leaveGroup = sender.leaveGroup.bind(sender); - this.sendSyncMessage = sender.sendSyncMessage.bind(sender); - this.getProfile = sender.getProfile.bind(sender); - this.getAvatar = sender.getAvatar.bind(sender); - this.syncReadMessages = sender.syncReadMessages.bind(sender); - this.syncVerification = sender.syncVerification.bind(sender); - this.sendDeliveryReceipt = sender.sendDeliveryReceipt.bind(sender); - this.sendReadReceipts = sender.sendReadReceipts.bind(sender); - this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender); - this.getMessageProto = sender.getMessageProto.bind(sender); - this._getAttachmentSizeBucket = sender._getAttachmentSizeBucket.bind(sender); - this.getSticker = sender.getSticker.bind(sender); - this.getStickerPackManifest = sender.getStickerPackManifest.bind(sender); - this.sendStickerPackSync = sender.sendStickerPackSync.bind(sender); - this.syncViewOnceOpen = sender.syncViewOnceOpen.bind(sender); -}; - -textsecure.MessageSender.prototype = { - constructor: textsecure.MessageSender, -}; diff --git a/libtextsecure/storage.js b/libtextsecure/storage.js deleted file mode 100644 index dff324cbf3..0000000000 --- a/libtextsecure/storage.js +++ /dev/null @@ -1,37 +0,0 @@ -/* global window, textsecure, localStorage */ - -// eslint-disable-next-line func-names -(function() { - /** ********************************************** - *** Utilities to store data in local storage *** - *********************************************** */ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - - // Overrideable storage implementation - window.textsecure.storage.impl = window.textsecure.storage.impl || { - /** *************************** - *** Base Storage Routines *** - **************************** */ - put(key, value) { - if (value === undefined) throw new Error('Tried to store undefined'); - localStorage.setItem(`${key}`, textsecure.utils.jsonThing(value)); - }, - - get(key, defaultValue) { - const value = localStorage.getItem(`${key}`); - if (value === null) return defaultValue; - return JSON.parse(value); - }, - - remove(key) { - localStorage.removeItem(`${key}`); - }, - }; - - window.textsecure.storage.put = (key, value) => - textsecure.storage.impl.put(key, value); - window.textsecure.storage.get = (key, defaultValue) => - textsecure.storage.impl.get(key, defaultValue); - window.textsecure.storage.remove = key => textsecure.storage.impl.remove(key); -})(); diff --git a/libtextsecure/stringview.js b/libtextsecure/stringview.js deleted file mode 100644 index 4a83ceaf13..0000000000 --- a/libtextsecure/stringview.js +++ /dev/null @@ -1,104 +0,0 @@ -/* global window, StringView */ - -/* eslint-disable no-bitwise, no-nested-ternary */ - -// eslint-disable-next-line func-names -(function() { - window.StringView = { - /* - * These functions from the Mozilla Developer Network - * and have been placed in the public domain. - * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding - * https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses - */ - - // prettier-ignore - b64ToUint6(nChr) { - return nChr > 64 && nChr < 91 - ? nChr - 65 - : nChr > 96 && nChr < 123 - ? nChr - 71 - : nChr > 47 && nChr < 58 - ? nChr + 4 - : nChr === 43 - ? 62 - : nChr === 47 - ? 63 - : 0; - }, - - base64ToBytes(sBase64, nBlocksSize) { - const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ''); - const nInLen = sB64Enc.length; - const nOutLen = nBlocksSize - ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize - : (nInLen * 3 + 1) >> 2; - const aBBytes = new ArrayBuffer(nOutLen); - const taBytes = new Uint8Array(aBBytes); - - let nMod3; - let nMod4; - for ( - let nUint24 = 0, nOutIdx = 0, nInIdx = 0; - nInIdx < nInLen; - nInIdx += 1 - ) { - nMod4 = nInIdx & 3; - nUint24 |= - StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4); - if (nMod4 === 3 || nInLen - nInIdx === 1) { - for ( - nMod3 = 0; - nMod3 < 3 && nOutIdx < nOutLen; - nMod3 += 1, nOutIdx += 1 - ) { - taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; - } - nUint24 = 0; - } - } - return aBBytes; - }, - - // prettier-ignore - uint6ToB64(nUint6) { - return nUint6 < 26 - ? nUint6 + 65 - : nUint6 < 52 - ? nUint6 + 71 - : nUint6 < 62 - ? nUint6 - 4 - : nUint6 === 62 - ? 43 - : nUint6 === 63 - ? 47 - : 65; - }, - - bytesToBase64(aBytes) { - let nMod3; - let sB64Enc = ''; - for ( - let nLen = aBytes.length, nUint24 = 0, nIdx = 0; - nIdx < nLen; - nIdx += 1 - ) { - nMod3 = nIdx % 3; - if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) { - sB64Enc += '\r\n'; - } - nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24); - if (nMod3 === 2 || aBytes.length - nIdx === 1) { - sB64Enc += String.fromCharCode( - StringView.uint6ToB64((nUint24 >>> 18) & 63), - StringView.uint6ToB64((nUint24 >>> 12) & 63), - StringView.uint6ToB64((nUint24 >>> 6) & 63), - StringView.uint6ToB64(nUint24 & 63) - ); - nUint24 = 0; - } - } - return sB64Enc.replace(/A(?=A$|$)/g, '='); - }, - }; -})(); diff --git a/libtextsecure/sync_request.js b/libtextsecure/sync_request.js deleted file mode 100644 index 23bb2be444..0000000000 --- a/libtextsecure/sync_request.js +++ /dev/null @@ -1,97 +0,0 @@ -/* global Event, textsecure, window, ConversationController */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - window.textsecure = window.textsecure || {}; - - function SyncRequest(sender, receiver) { - if ( - !(sender instanceof textsecure.MessageSender) || - !(receiver instanceof textsecure.MessageReceiver) - ) { - throw new Error( - 'Tried to construct a SyncRequest without MessageSender and MessageReceiver' - ); - } - this.receiver = receiver; - - this.oncontact = this.onContactSyncComplete.bind(this); - receiver.addEventListener('contactsync', this.oncontact); - - this.ongroup = this.onGroupSyncComplete.bind(this); - receiver.addEventListener('groupsync', this.ongroup); - - const ourNumber = textsecure.storage.user.getNumber(); - const { - wrap, - sendOptions, - } = ConversationController.prepareForSend(ourNumber, { syncMessage: true }); - - window.log.info('SyncRequest created. Sending config sync request...'); - wrap(sender.sendRequestConfigurationSyncMessage(sendOptions)); - - window.log.info('SyncRequest now sending block sync request...'); - wrap(sender.sendRequestBlockSyncMessage(sendOptions)); - - window.log.info('SyncRequest now sending contact sync message...'); - wrap(sender.sendRequestContactSyncMessage(sendOptions)) - .then(() => { - window.log.info('SyncRequest now sending group sync messsage...'); - return wrap(sender.sendRequestGroupSyncMessage(sendOptions)); - }) - .catch(error => { - window.log.error( - 'SyncRequest error:', - error && error.stack ? error.stack : error - ); - }); - this.timeout = setTimeout(this.onTimeout.bind(this), 60000); - } - - SyncRequest.prototype = new textsecure.EventTarget(); - SyncRequest.prototype.extend({ - constructor: SyncRequest, - onContactSyncComplete() { - this.contactSync = true; - this.update(); - }, - onGroupSyncComplete() { - this.groupSync = true; - this.update(); - }, - update() { - if (this.contactSync && this.groupSync) { - this.dispatchEvent(new Event('success')); - this.cleanup(); - } - }, - onTimeout() { - if (this.contactSync || this.groupSync) { - this.dispatchEvent(new Event('success')); - } else { - this.dispatchEvent(new Event('timeout')); - } - this.cleanup(); - }, - cleanup() { - clearTimeout(this.timeout); - this.receiver.removeEventListener('contactsync', this.oncontact); - this.receiver.removeEventListener('groupSync', this.ongroup); - delete this.listeners; - }, - }); - - textsecure.SyncRequest = function SyncRequestWrapper(sender, receiver) { - const syncRequest = new SyncRequest(sender, receiver); - this.addEventListener = syncRequest.addEventListener.bind(syncRequest); - this.removeEventListener = syncRequest.removeEventListener.bind( - syncRequest - ); - }; - - textsecure.SyncRequest.prototype = { - constructor: textsecure.SyncRequest, - }; -})(); diff --git a/libtextsecure/task_with_timeout.js b/libtextsecure/task_with_timeout.js deleted file mode 100644 index 119d03f11c..0000000000 --- a/libtextsecure/task_with_timeout.js +++ /dev/null @@ -1,72 +0,0 @@ -/* global window */ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - window.textsecure = window.textsecure || {}; - - window.textsecure.createTaskWithTimeout = (task, id, options = {}) => { - const timeout = options.timeout || 1000 * 60 * 2; // two minutes - - const errorForStack = new Error('for stack'); - return () => - new Promise((resolve, reject) => { - let complete = false; - let timer = setTimeout(() => { - if (!complete) { - const message = `${id || - ''} task did not complete in time. Calling stack: ${ - errorForStack.stack - }`; - - window.log.error(message); - return reject(new Error(message)); - } - - return null; - }, timeout); - const clearTimer = () => { - try { - const localTimer = timer; - if (localTimer) { - timer = null; - clearTimeout(localTimer); - } - } catch (error) { - window.log.error( - id || '', - 'task ran into problem canceling timer. Calling stack:', - errorForStack.stack - ); - } - }; - - const success = result => { - clearTimer(); - complete = true; - return resolve(result); - }; - const failure = error => { - clearTimer(); - complete = true; - return reject(error); - }; - - let promise; - try { - promise = task(); - } catch (error) { - clearTimer(); - throw error; - } - if (!promise || !promise.then) { - clearTimer(); - complete = true; - return resolve(promise); - } - - return promise.then(success, failure); - }); - }; -})(); diff --git a/libtextsecure/test/contacts_parser_test.js b/libtextsecure/test/contacts_parser_test.js index 1338851c2d..acd0302acd 100644 --- a/libtextsecure/test/contacts_parser_test.js +++ b/libtextsecure/test/contacts_parser_test.js @@ -1,5 +1,3 @@ -/* global ContactBuffer, GroupBuffer, textsecure */ - describe('ContactBuffer', () => { function getTestBuffer() { const buffer = new dcodeIO.ByteBuffer(); @@ -10,7 +8,7 @@ describe('ContactBuffer', () => { } avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.offset = 0; - const contactInfo = new textsecure.protobuf.ContactDetails({ + const contactInfo = new window.textsecure.protobuf.ContactDetails({ name: 'Zero Cool', number: '+10000000000', uuid: '7198E1BD-1293-452A-A098-F982FF201902', @@ -31,7 +29,7 @@ describe('ContactBuffer', () => { it('parses an array buffer of contacts', () => { const arrayBuffer = getTestBuffer(); - const contactBuffer = new ContactBuffer(arrayBuffer); + const contactBuffer = new window.textsecure.ContactBuffer(arrayBuffer); let contact = contactBuffer.next(); let count = 0; while (contact !== undefined) { @@ -62,7 +60,7 @@ describe('GroupBuffer', () => { } avatarBuffer.limit = avatarBuffer.offset; avatarBuffer.offset = 0; - const groupInfo = new textsecure.protobuf.GroupDetails({ + const groupInfo = new window.textsecure.protobuf.GroupDetails({ id: new Uint8Array([1, 3, 3, 7]).buffer, name: 'Hackers', membersE164: ['cereal', 'burn', 'phreak', 'joey'], @@ -89,7 +87,7 @@ describe('GroupBuffer', () => { it('parses an array buffer of groups', () => { const arrayBuffer = getTestBuffer(); - const groupBuffer = new GroupBuffer(arrayBuffer); + const groupBuffer = new window.textsecure.GroupBuffer(arrayBuffer); let group = groupBuffer.next(); let count = 0; while (group !== undefined) { diff --git a/libtextsecure/test/helpers_test.js b/libtextsecure/test/helpers_test.js index 66ecb83e73..1dce091e35 100644 --- a/libtextsecure/test/helpers_test.js +++ b/libtextsecure/test/helpers_test.js @@ -6,7 +6,7 @@ describe('Helpers', () => { a[0] = 0; a[1] = 255; a[2] = 128; - assert.equal(getString(b), '\x00\xff\x80'); + assert.equal(window.textsecure.utils.getString(b), '\x00\xff\x80'); }); }); @@ -15,13 +15,16 @@ describe('Helpers', () => { const anArrayBuffer = new ArrayBuffer(1); const typedArray = new Uint8Array(anArrayBuffer); typedArray[0] = 'a'.charCodeAt(0); - assertEqualArrayBuffers(stringToArrayBuffer('a'), anArrayBuffer); + assertEqualArrayBuffers( + window.textsecure.utils.stringToArrayBuffer('a'), + anArrayBuffer + ); }); it('throws an error when passed a non string', () => { const notStringable = [{}, undefined, null, new ArrayBuffer()]; notStringable.forEach(notString => { assert.throw(() => { - stringToArrayBuffer(notString); + window.textsecure.utils.stringToArrayBuffer(notString); }, Error); }); }); diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index 2920723d3b..d8b418cb7c 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -19,23 +19,10 @@ - - - - - - - - - - - - - - + diff --git a/libtextsecure/test/websocket-resources_test.js b/libtextsecure/test/websocket-resources_test.js index 4ce99ddd46..54cebe5b20 100644 --- a/libtextsecure/test/websocket-resources_test.js +++ b/libtextsecure/test/websocket-resources_test.js @@ -1,5 +1,3 @@ -/* global textsecure, WebSocketResource */ - describe('WebSocket-Resource', () => { describe('requests and responses', () => { it('receives requests and sends responses', done => { @@ -7,10 +5,12 @@ describe('WebSocket-Resource', () => { const requestId = '1'; const socket = { send(data) { - const message = textsecure.protobuf.WebSocketMessage.decode(data); + const message = window.textsecure.protobuf.WebSocketMessage.decode( + data + ); assert.strictEqual( message.type, - textsecure.protobuf.WebSocketMessage.Type.RESPONSE + window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE ); assert.strictEqual(message.response.message, 'OK'); assert.strictEqual(message.response.status, 200); @@ -21,7 +21,7 @@ describe('WebSocket-Resource', () => { }; // actual test - this.resource = new WebSocketResource(socket, { + this.resource = new window.textsecure.WebSocketResource(socket, { handleRequest(request) { assert.strictEqual(request.verb, 'PUT'); assert.strictEqual(request.path, '/some/path'); @@ -36,8 +36,8 @@ describe('WebSocket-Resource', () => { // mock socket request socket.onmessage({ data: new Blob([ - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, + new window.textsecure.protobuf.WebSocketMessage({ + type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST, request: { id: requestId, verb: 'PUT', @@ -56,10 +56,12 @@ describe('WebSocket-Resource', () => { let requestId; const socket = { send(data) { - const message = textsecure.protobuf.WebSocketMessage.decode(data); + const message = window.textsecure.protobuf.WebSocketMessage.decode( + data + ); assert.strictEqual( message.type, - textsecure.protobuf.WebSocketMessage.Type.REQUEST + window.textsecure.protobuf.WebSocketMessage.Type.REQUEST ); assert.strictEqual(message.request.verb, 'PUT'); assert.strictEqual(message.request.path, '/some/path'); @@ -73,7 +75,7 @@ describe('WebSocket-Resource', () => { }; // actual test - const resource = new WebSocketResource(socket); + const resource = new window.textsecure.WebSocketResource(socket); resource.sendRequest({ verb: 'PUT', path: '/some/path', @@ -89,8 +91,8 @@ describe('WebSocket-Resource', () => { // mock socket response socket.onmessage({ data: new Blob([ - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, + new window.textsecure.protobuf.WebSocketMessage({ + type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE, response: { id: requestId, message: 'OK', status: 200 }, }) .encode() @@ -112,7 +114,7 @@ describe('WebSocket-Resource', () => { mockServer.on('connection', server => { server.on('close', done); }); - const resource = new WebSocketResource( + const resource = new window.textsecure.WebSocketResource( new WebSocket('ws://localhost:8081') ); resource.close(); @@ -131,10 +133,12 @@ describe('WebSocket-Resource', () => { const mockServer = new MockServer('ws://localhost:8081'); mockServer.on('connection', server => { server.on('message', data => { - const message = textsecure.protobuf.WebSocketMessage.decode(data); + const message = window.textsecure.protobuf.WebSocketMessage.decode( + data + ); assert.strictEqual( message.type, - textsecure.protobuf.WebSocketMessage.Type.REQUEST + window.textsecure.protobuf.WebSocketMessage.Type.REQUEST ); assert.strictEqual(message.request.verb, 'GET'); assert.strictEqual(message.request.path, '/v1/keepalive'); @@ -142,7 +146,7 @@ describe('WebSocket-Resource', () => { done(); }); }); - this.resource = new WebSocketResource( + this.resource = new window.textsecure.WebSocketResource( new WebSocket('ws://loc1alhost:8081'), { keepalive: { path: '/v1/keepalive' }, @@ -154,10 +158,12 @@ describe('WebSocket-Resource', () => { const mockServer = new MockServer('ws://localhost:8081'); mockServer.on('connection', server => { server.on('message', data => { - const message = textsecure.protobuf.WebSocketMessage.decode(data); + const message = window.textsecure.protobuf.WebSocketMessage.decode( + data + ); assert.strictEqual( message.type, - textsecure.protobuf.WebSocketMessage.Type.REQUEST + window.textsecure.protobuf.WebSocketMessage.Type.REQUEST ); assert.strictEqual(message.request.verb, 'GET'); assert.strictEqual(message.request.path, '/'); @@ -165,7 +171,7 @@ describe('WebSocket-Resource', () => { done(); }); }); - this.resource = new WebSocketResource( + this.resource = new window.textsecure.WebSocketResource( new WebSocket('ws://localhost:8081'), { keepalive: true, @@ -180,7 +186,9 @@ describe('WebSocket-Resource', () => { mockServer.on('connection', server => { server.on('close', done); }); - this.resource = new WebSocketResource(socket, { keepalive: true }); + this.resource = new window.textsecure.WebSocketResource(socket, { + keepalive: true, + }); }); it('allows resetting the keepalive timer', function thisNeeded2(done) { @@ -190,10 +198,12 @@ describe('WebSocket-Resource', () => { const startTime = Date.now(); mockServer.on('connection', server => { server.on('message', data => { - const message = textsecure.protobuf.WebSocketMessage.decode(data); + const message = window.textsecure.protobuf.WebSocketMessage.decode( + data + ); assert.strictEqual( message.type, - textsecure.protobuf.WebSocketMessage.Type.REQUEST + window.textsecure.protobuf.WebSocketMessage.Type.REQUEST ); assert.strictEqual(message.request.verb, 'GET'); assert.strictEqual(message.request.path, '/'); @@ -205,7 +215,9 @@ describe('WebSocket-Resource', () => { done(); }); }); - const resource = new WebSocketResource(socket, { keepalive: true }); + const resource = new window.textsecure.WebSocketResource(socket, { + keepalive: true, + }); setTimeout(() => { resource.resetKeepAliveTimer(); }, 5000); diff --git a/libtextsecure/websocket-resources.js b/libtextsecure/websocket-resources.js deleted file mode 100644 index 9d0398a31b..0000000000 --- a/libtextsecure/websocket-resources.js +++ /dev/null @@ -1,243 +0,0 @@ -/* global window, dcodeIO, Event, textsecure, FileReader, WebSocketResource */ - -// eslint-disable-next-line func-names -(function() { - /* - * WebSocket-Resources - * - * Create a request-response interface over websockets using the - * WebSocket-Resources sub-protocol[1]. - * - * var client = new WebSocketResource(socket, function(request) { - * request.respond(200, 'OK'); - * }); - * - * client.sendRequest({ - * verb: 'PUT', - * path: '/v1/messages', - * body: '{ some: "json" }', - * success: function(message, status, request) {...}, - * error: function(message, status, request) {...} - * }); - * - * 1. https://github.com/signalapp/WebSocket-Resources - * - */ - - const Request = function Request(options) { - this.verb = options.verb || options.type; - this.path = options.path || options.url; - this.headers = options.headers; - this.body = options.body || options.data; - this.success = options.success; - this.error = options.error; - this.id = options.id; - - if (this.id === undefined) { - const bits = new Uint32Array(2); - window.crypto.getRandomValues(bits); - this.id = dcodeIO.Long.fromBits(bits[0], bits[1], true); - } - - if (this.body === undefined) { - this.body = null; - } - }; - - const IncomingWebSocketRequest = function IncomingWebSocketRequest(options) { - const request = new Request(options); - const { socket } = options; - - this.verb = request.verb; - this.path = request.path; - this.body = request.body; - this.headers = request.headers; - - this.respond = (status, message) => { - socket.send( - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.RESPONSE, - response: { id: request.id, message, status }, - }) - .encode() - .toArrayBuffer() - ); - }; - }; - - const outgoing = {}; - const OutgoingWebSocketRequest = function OutgoingWebSocketRequest( - options, - socket - ) { - const request = new Request(options); - outgoing[request.id] = request; - socket.send( - new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: { - verb: request.verb, - path: request.path, - body: request.body, - headers: request.headers, - id: request.id, - }, - }) - .encode() - .toArrayBuffer() - ); - }; - - window.WebSocketResource = function WebSocketResource(socket, opts = {}) { - let { handleRequest } = opts; - if (typeof handleRequest !== 'function') { - handleRequest = request => request.respond(404, 'Not found'); - } - this.sendRequest = options => new OutgoingWebSocketRequest(options, socket); - - // eslint-disable-next-line no-param-reassign - socket.onmessage = socketMessage => { - const blob = socketMessage.data; - const handleArrayBuffer = buffer => { - const message = textsecure.protobuf.WebSocketMessage.decode(buffer); - if ( - message.type === textsecure.protobuf.WebSocketMessage.Type.REQUEST - ) { - handleRequest( - new IncomingWebSocketRequest({ - verb: message.request.verb, - path: message.request.path, - body: message.request.body, - headers: message.request.headers, - id: message.request.id, - socket, - }) - ); - } else if ( - message.type === textsecure.protobuf.WebSocketMessage.Type.RESPONSE - ) { - const { response } = message; - const request = outgoing[response.id]; - if (request) { - request.response = response; - let callback = request.error; - if (response.status >= 200 && response.status < 300) { - callback = request.success; - } - - if (typeof callback === 'function') { - callback(response.message, response.status, request); - } - } else { - throw new Error( - `Received response for unknown request ${message.response.id}` - ); - } - } - }; - - if (blob instanceof ArrayBuffer) { - handleArrayBuffer(blob); - } else { - const reader = new FileReader(); - reader.onload = () => handleArrayBuffer(reader.result); - reader.readAsArrayBuffer(blob); - } - }; - - if (opts.keepalive) { - this.keepalive = new KeepAlive(this, { - path: opts.keepalive.path, - disconnect: opts.keepalive.disconnect, - }); - const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); - socket.addEventListener('open', resetKeepAliveTimer); - socket.addEventListener('message', resetKeepAliveTimer); - socket.addEventListener( - 'close', - this.keepalive.stop.bind(this.keepalive) - ); - } - - socket.addEventListener('close', () => { - this.closed = true; - }); - - this.close = (code = 3000, reason) => { - if (this.closed) { - return; - } - - window.log.info('WebSocketResource.close()'); - if (this.keepalive) { - this.keepalive.stop(); - } - - socket.close(code, reason); - // eslint-disable-next-line no-param-reassign - socket.onmessage = null; - - // On linux the socket can wait a long time to emit its close event if we've - // lost the internet connection. On the order of minutes. This speeds that - // process up. - setTimeout(() => { - if (this.closed) { - return; - } - this.closed = true; - - window.log.warn('Dispatching our own socket close event'); - const ev = new Event('close'); - ev.code = code; - ev.reason = reason; - this.dispatchEvent(ev); - }, 5000); - }; - }; - window.WebSocketResource.prototype = new textsecure.EventTarget(); - - function KeepAlive(websocketResource, opts = {}) { - if (websocketResource instanceof WebSocketResource) { - this.path = opts.path; - if (this.path === undefined) { - this.path = '/'; - } - this.disconnect = opts.disconnect; - if (this.disconnect === undefined) { - this.disconnect = true; - } - this.wsr = websocketResource; - } else { - throw new TypeError('KeepAlive expected a WebSocketResource'); - } - } - - KeepAlive.prototype = { - constructor: KeepAlive, - stop() { - clearTimeout(this.keepAliveTimer); - clearTimeout(this.disconnectTimer); - }, - reset() { - clearTimeout(this.keepAliveTimer); - clearTimeout(this.disconnectTimer); - this.keepAliveTimer = setTimeout(() => { - if (this.disconnect) { - // automatically disconnect if server doesn't ack - this.disconnectTimer = setTimeout(() => { - clearTimeout(this.keepAliveTimer); - this.wsr.close(3001, 'No response to keepalive request'); - }, 10000); - } else { - this.reset(); - } - window.log.info('Sending a keepalive message'); - this.wsr.sendRequest({ - verb: 'GET', - path: this.path, - success: this.reset.bind(this), - }); - }, 55000); - }, - }; -})(); diff --git a/preload.js b/preload.js index 6094a60281..a0bf65298a 100644 --- a/preload.js +++ b/preload.js @@ -222,9 +222,9 @@ try { window.nodeSetImmediate = setImmediate; - const { initialize: initializeWebAPI } = require('./ts/WebAPI'); + window.textsecure = require('./ts/textsecure').default; - window.WebAPI = initializeWebAPI({ + window.WebAPI = window.textsecure.WebAPI.initialize({ url: config.serverUrl, cdnUrl: config.cdnUrl, certificateAuthority: config.certificateAuthority, diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js index 0da9d5f193..8b6a782535 100644 --- a/sticker-creator/preload.js +++ b/sticker-creator/preload.js @@ -29,7 +29,7 @@ const Signal = require('../js/modules/signal'); window.Signal = Signal.setup({}); -const { initialize: initializeWebAPI } = require('../ts/WebAPI'); +const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI'); const WebAPI = initializeWebAPI({ url: config.serverUrl, diff --git a/ts/libsignal.d.ts b/ts/libsignal.d.ts new file mode 100644 index 0000000000..8a00321913 --- /dev/null +++ b/ts/libsignal.d.ts @@ -0,0 +1,215 @@ +export type LibSignalType = { + externalCurve?: CurveType; + crypto: { + encrypt: ( + key: ArrayBuffer, + data: ArrayBuffer, + iv: ArrayBuffer + ) => Promise; + decrypt: ( + key: ArrayBuffer, + data: ArrayBuffer, + iv: ArrayBuffer + ) => Promise; + calculateMAC: (key: ArrayBuffer, data: ArrayBuffer) => Promise; + verifyMAC: ( + data: ArrayBuffer, + key: ArrayBuffer, + mac: ArrayBuffer, + length: number + ) => Promise; + getRandomBytes: (size: number) => ArrayBuffer; + }; + KeyHelper: { + generateIdentityKeyPair: () => Promise<{ + privKey: ArrayBuffer; + pubKey: ArrayBuffer; + }>; + generateRegistrationId: () => number; + generateSignedPreKey: ( + identityKeyPair: KeyPairType, + signedKeyId: number + ) => Promise; + generatePreKey: (keyId: number) => Promise; + }; + Curve: { + generateKeyPair: () => KeyPairType; + createKeyPair: (privKey: ArrayBuffer) => KeyPairType; + calculateAgreement: ( + pubKey: ArrayBuffer, + privKey: ArrayBuffer + ) => ArrayBuffer; + verifySignature: ( + pubKey: ArrayBuffer, + msg: ArrayBuffer, + sig: ArrayBuffer + ) => void; + calculateSignature: ( + privKey: ArrayBuffer, + message: ArrayBuffer + ) => ArrayBuffer | Promise; + validatePubKeyFormat: (buffer: ArrayBuffer) => ArrayBuffer; + async: CurveType; + }; + HKDF: { + deriveSecrets: ( + packKey: ArrayBuffer, + salt: ArrayBuffer, + // The string is a bit crazy, but ProvisioningCipher currently passes in a string + info: ArrayBuffer | string + ) => Promise>; + }; + worker: { + startWorker: () => void; + stopWorker: () => void; + }; + FingerprintGenerator: typeof FingerprintGeneratorClass; + SessionBuilder: typeof SessionBuilderClass; + SessionCipher: typeof SessionCipherClass; + SignalProtocolAddress: typeof SignalProtocolAddressClass; +}; + +export type KeyPairType = { + pubKey: ArrayBuffer; + privKey: ArrayBuffer; +}; + +export type SignedPreKeyType = { + keyId: number; + keyPair: KeyPairType; + signature: ArrayBuffer; +}; + +export type PreKeyType = { + keyId: number; + keyPair: KeyPairType; +}; + +type RecordType = { + archiveCurrentState: () => void; + deleteAllSessions: () => void; + getOpenSession: () => void; + getSessionByBaseKey: () => void; + getSessions: () => void; + haveOpenSession: () => void; + promoteState: () => void; + serialize: () => void; + updateSessionState: () => void; +}; + +type CurveType = { + generateKeyPair: () => Promise; + createKeyPair: (privKey: ArrayBuffer) => Promise; + calculateAgreement: ( + pubKey: ArrayBuffer, + privKey: ArrayBuffer + ) => Promise; + verifySignature: ( + pubKey: ArrayBuffer, + msg: ArrayBuffer, + sig: ArrayBuffer + ) => Promise; + calculateSignature: ( + privKey: ArrayBuffer, + message: ArrayBuffer + ) => ArrayBuffer | Promise; + validatePubKeyFormat: (buffer: ArrayBuffer) => ArrayBuffer; +}; + +type SessionRecordType = any; + +export type StorageType = { + Direction: { + SENDING: number; + RECEIVING: number; + }; + getIdentityKeyPair: () => Promise; + getLocalRegistrationId: () => Promise; + isTrustedIdentity: () => Promise; + loadPreKey: ( + encodedAddress: string, + publicKey: ArrayBuffer | undefined, + direction: number + ) => Promise; + loadSession: (encodedAddress: string) => Promise; + loadSignedPreKey: (keyId: number) => Promise; + removePreKey: (keyId: number) => Promise; + saveIdentity: ( + encodedAddress: string, + publicKey: ArrayBuffer, + nonblockingApproval?: boolean + ) => Promise; + storeSession: ( + encodedAddress: string, + record: SessionRecordType + ) => Promise; +}; + +declare class FingerprintGeneratorClass { + constructor(iterations: number); + createFor: ( + localIdentifier: string, + localIdentityKey: ArrayBuffer, + remoteIdentifier: string, + remoteIdentityKey: ArrayBuffer + ) => string; +} + +export declare class SignalProtocolAddressClass { + static fromString(encodedAddress: string): SignalProtocolAddressClass; + constructor(name: string, deviceId: number); + getName: () => string; + getDeviceId: () => number; + toString: () => string; + equals: (other: SignalProtocolAddressClass) => boolean; +} + +type DeviceType = { + deviceId: number; + identityKey: ArrayBuffer; + registrationId: number; + signedPreKey: { + keyId: number; + publicKey: ArrayBuffer; + signature: ArrayBuffer; + }; + preKey?: { + keyId: number; + publicKey: ArrayBuffer; + }; +}; + +declare class SessionBuilderClass { + constructor(storage: StorageType, remoteAddress: SignalProtocolAddressClass); + processPreKey: (device: DeviceType) => Promise; + processV3: (record: RecordType, message: any) => Promise; +} + +export declare class SessionCipherClass { + constructor( + storage: StorageType, + remoteAddress: SignalProtocolAddressClass, + options?: any + ); + closeOpenSessionForDevice: () => Promise; + decryptPreKeyWhisperMessage: ( + buffer: ArrayBuffer, + encoding?: string + ) => Promise; + decryptWhisperMessage: ( + buffer: ArrayBuffer, + encoding?: string + ) => Promise; + deleteAllSessionsForDevice: () => Promise; + encrypt: ( + buffer: ArrayBuffer | Uint8Array, + encoding?: string + ) => Promise<{ + type: number; + registrationId: number; + body: string; + }>; + getRecord: () => Promise; + getRemoteRegistrationId: () => Promise; + hasOpenSession: () => Promise; +} diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index a6a163ac5d..6999bbdcfa 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -1535,6 +1535,7 @@ async function updateToSchemaVersion20( await instance.run('PRAGMA user_version = 20;'); await instance.run('COMMIT TRANSACTION;'); + console.log('updateToSchemaVersion20: success!'); } catch (error) { await instance.run('ROLLBACK;'); throw error; diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts new file mode 100644 index 0000000000..a994698595 --- /dev/null +++ b/ts/textsecure.d.ts @@ -0,0 +1,647 @@ +import { + KeyPairType, + SessionRecordType, + SignedPreKeyType, + StorageType, +} from './libsignal.d'; +import MessageReceiver from './textsecure/MessageReceiver'; +import EventTarget from './textsecure/EventTarget'; +import { ByteBufferClass } from './window.d'; + +type AttachmentType = any; + +export type UnprocessedType = { + attempts: number; + decrypted?: string; + envelope?: string; + id: string; + serverTimestamp?: number; + source?: string; + sourceDevice?: number; + sourceUuid?: string; + version: number; +}; + +export type TextSecureType = { + createTaskWithTimeout: ( + task: () => Promise, + id?: string, + options?: { timeout?: number } + ) => () => Promise; + storage: { + user: { + getNumber: () => string; + getUuid: () => string | undefined; + getDeviceId: () => number | string; + getDeviceName: () => string; + getDeviceNameEncrypted: () => boolean; + setDeviceNameEncrypted: () => Promise; + getSignalingKey: () => ArrayBuffer; + setNumberAndDeviceId: ( + number: string, + deviceId: number, + deviceName?: string | null + ) => Promise; + setUuidAndDeviceId: (uuid: string, deviceId: number) => Promise; + }; + unprocessed: { + batchAdd: (dataArray: Array) => Promise; + remove: (id: string | Array) => Promise; + getCount: () => Promise; + removeAll: () => Promise; + getAll: () => Promise>; + updateAttempts: (id: string, attempts: number) => Promise; + addDecryptedDataToList: ( + array: Array> + ) => Promise; + }; + get: (key: string, defaultValue?: any) => any; + put: (key: string, value: any) => Promise; + remove: (key: string | Array) => Promise; + protocol: StorageProtocolType; + }; + messaging: { + sendStickerPackSync: ( + operations: Array<{ + packId: string; + packKey: string; + installed: boolean; + }>, + options: Object + ) => Promise; + }; + protobuf: ProtobufCollectionType; + + EventTarget: typeof EventTarget; + MessageReceiver: typeof MessageReceiver; +}; + +type StoredSignedPreKeyType = SignedPreKeyType & { + confirmed?: boolean; + created_at: number; +}; + +export type StorageProtocolType = StorageType & { + VerifiedStatus: { + DEFAULT: number; + VERIFIED: number; + UNVERIFIED: number; + }; + archiveSiblingSessions: (identifier: string) => Promise; + removeSession: (identifier: string) => Promise; + getDeviceIds: (identifier: string) => Promise>; + hydrateCaches: () => Promise; + clearPreKeyStore: () => Promise; + clearSignedPreKeysStore: () => Promise; + clearSessionStore: () => Promise; + isTrustedIdentity: () => void; + storePreKey: (keyId: number, keyPair: KeyPairType) => Promise; + storeSignedPreKey: ( + keyId: number, + keyPair: KeyPairType, + confirmed?: boolean + ) => Promise; + loadSignedPreKeys: () => Promise>; + saveIdentityWithAttributes: ( + number: string, + options: { + publicKey: ArrayBuffer; + firstUse: boolean; + timestamp: number; + verified: number; + nonblockingApproval: boolean; + } + ) => Promise; + removeSignedPreKey: (keyId: number) => Promise; + removeAllData: () => Promise; +}; + +// Protobufs + +type ProtobufCollectionType = { + AttachmentPointer: typeof AttachmentPointerClass; + ContactDetails: typeof ContactDetailsClass; + Content: typeof ContentClass; + DataMessage: typeof DataMessageClass; + DeviceName: typeof DeviceNameClass; + Envelope: typeof EnvelopeClass; + GroupContext: typeof GroupContextClass; + GroupDetails: typeof GroupDetailsClass; + NullMessage: typeof NullMessageClass; + ProvisioningUuid: typeof ProvisioningUuidClass; + ProvisionEnvelope: typeof ProvisionEnvelopeClass; + ProvisionMessage: typeof ProvisionMessageClass; + ReceiptMessage: typeof ReceiptMessageClass; + SyncMessage: typeof SyncMessageClass; + TypingMessage: typeof TypingMessageClass; + Verified: typeof VerifiedClass; + WebSocketMessage: typeof WebSocketMessageClass; + WebSocketRequestMessage: typeof WebSocketRequestMessageClass; + WebSocketResponseMessage: typeof WebSocketResponseMessageClass; +}; + +// Note: there are a lot of places in the code that overwrite a field like this +// with a type that the app can use. Being more rigorous with these +// types would require code changes, out of scope for now. +type ProtoBinaryType = any; +type ProtoBigNumberType = any; + +export declare class AttachmentPointerClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => AttachmentPointerClass; + + id?: ProtoBigNumberType; + contentType?: string; + key?: ProtoBinaryType; + size?: number; + thumbnail?: ProtoBinaryType; + digest?: ProtoBinaryType; + fileName?: string; + flags?: number; + width?: number; + height?: number; + caption?: string; +} + +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; + callMessage?: any; + nullMessage?: NullMessageClass; + receiptMessage?: ReceiptMessageClass; + typingMessage?: TypingMessageClass; +} + +export declare class DataMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => DataMessageClass; + toArrayBuffer(): ArrayBuffer; + + body?: string | null; + attachments?: Array; + group?: GroupContextClass | 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; +} + +// 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; + } + + class ProtocolVersion { + static INITIAL: number; + static MESSAGE_TIMERS: number; + static VIEW_ONCE: number; + static VIEW_ONCE_VIDEO: number; + static REACTIONS: number; + static CURRENT: number; + } + + // Note: deep nesting + class Quote { + id?: ProtoBigNumberType; + author?: string; + authorUuid?: string; + text?: string; + attachments?: Array; + } + + class Reaction { + emoji?: string; + remove?: boolean; + targetAuthorE164?: string; + targetAuthorUuid?: string; + targetTimestamp?: ProtoBigNumberType; + } + + class Sticker { + packId?: ProtoBinaryType; + packKey?: ProtoBinaryType; + stickerId?: number; + data?: AttachmentPointerClass; + } +} + +// 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, + encoding?: string + ) => DeviceNameClass; + encode: () => DeviceNameClass; + toArrayBuffer: () => ArrayBuffer; + + ephemeralPublic: ProtoBinaryType; + syntheticIv: ProtoBinaryType; + ciphertext: ProtoBinaryType; +} + +export declare class EnvelopeClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => EnvelopeClass; + + type?: number; + source?: string; + sourceUuid?: string; + sourceDevice?: number; + relay?: string; + timestamp?: ProtoBigNumberType; + legacyMessage?: ProtoBinaryType; + content?: ProtoBinaryType; + serverGuid?: string; + serverTimestamp?: ProtoBigNumberType; + + // Note: these additional properties are added in the course of processing + id: string; + unidentifiedDeliveryReceived?: boolean; +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace EnvelopeClass { + class Type { + static CIPHERTEXT: number; + static PREKEY_BUNDLE: number; + static RECEIPT: number; + static UNIDENTIFIED_SENDER: number; + } +} + +export declare class GroupContextClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => GroupContextClass; + + id?: ProtoBinaryType; + type?: number; + name?: string | null; + membersE164?: Array; + members?: Array; + avatar?: AttachmentPointerClass | null; +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace GroupContextClass { + class Member { + uuid?: string; + e164?: string; + } + class Type { + static UNKNOWN: number; + static UPDATE: number; + static DELIVER: number; + static QUIT: number; + static REQUEST_INFO: number; + } +} + +export declare class GroupDetailsClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => GroupDetailsClass; + + id?: ProtoBinaryType; + name?: string; + membersE164?: Array; + members?: Array; + avatar?: GroupDetailsClass.Avatar; + active?: boolean; + expireTimer?: number; + color?: string; + blocked?: boolean; + inboxPosition?: number; +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace GroupDetailsClass { + class Avatar { + contentType?: string; + length?: string; + } + + class Member { + uuid?: string; + e164?: string; + } +} + +export declare class NullMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => NullMessageClass; + + padding?: ProtoBinaryType; +} + +declare class ProvisioningUuidClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => ProvisioningUuidClass; + encode: () => ProvisioningUuidClass; + toArrayBuffer: () => ArrayBuffer; + + uuid?: string; +} + +declare class ProvisionEnvelopeClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => ProvisionEnvelopeClass; + encode: () => ProvisionEnvelopeClass; + toArrayBuffer: () => ArrayBuffer; + + publicKey?: ProtoBinaryType; + body?: ProtoBinaryType; +} + +declare class ProvisionMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => ProvisionMessageClass; + encode: () => ProvisionMessageClass; + toArrayBuffer: () => ArrayBuffer; + + identityKeyPrivate?: ProtoBinaryType; + number?: string; + uuid?: string; + provisioningCode?: string; + userAgent?: string; + profileKey?: ProtoBinaryType; + readReceipts?: boolean; + ProvisioningVersion?: number; +} + +export declare class ReceiptMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => ReceiptMessageClass; + + type?: number; + timestamp?: ProtoBigNumberType; +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace ReceiptMessageClass { + class Type { + static DELIVERY: number; + static READ: number; + } +} + +export declare class SyncMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => SyncMessageClass; + + sent?: SyncMessageClass.Sent; + contacts?: SyncMessageClass.Contacts; + groups?: SyncMessageClass.Groups; + request?: SyncMessageClass.Request; + read?: Array; + blocked?: SyncMessageClass.Blocked; + verified?: VerifiedClass; + configuration?: SyncMessageClass.Configuration; + padding?: ProtoBinaryType; + stickerPackOperation?: Array; + viewOnceOpen?: SyncMessageClass.ViewOnceOpen; +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace SyncMessageClass { + class Configuration { + readReceipts?: boolean; + unidentifiedDeliveryIndicators?: boolean; + typingIndicators?: boolean; + linkPreviews?: boolean; + } + class Contacts { + blob?: AttachmentPointerClass; + complete?: boolean; + } + class Groups { + blob?: AttachmentPointerClass; + } + class Blocked { + numbers?: Array; + uuids?: Array; + groupIds?: Array; + } + class Read { + sender?: string; + senderUuid?: string; + timestamp?: ProtoBigNumberType; + } + class Request { + type?: number; + } + class Sent { + destination?: string; + destinationUuid?: string; + timestamp?: ProtoBigNumberType; + message?: DataMessageClass; + expirationStartTimestamp?: ProtoBigNumberType; + unidentifiedStatus?: Array< + SyncMessageClass.Sent.UnidentifiedDeliveryStatus + >; + isRecipientUpdate?: boolean; + } + class StickerPackOperation { + packId?: ProtoBinaryType; + packKey?: ProtoBinaryType; + type?: number; + } + class ViewOnceOpen { + sender?: string; + senderUuid?: string; + timestamp?: ProtoBinaryType; + } +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace SyncMessageClass.Request { + class Type { + static UNKNOWN: number; + static BLOCKED: number; + static CONFIGURATION: number; + static CONTACTS: number; + static GROUPS: number; + } +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace SyncMessageClass.Sent { + class UnidentifiedDeliveryStatus { + destination?: string; + destinationUuid?: string; + unidentified?: boolean; + } +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace SyncMessageClass.StickerPackOperation { + class Type { + static INSTALL: number; + static REMOVE: number; + } +} + +export declare class TypingMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => TypingMessageClass; + + timestamp?: ProtoBigNumberType; + action?: number; + groupId?: ProtoBinaryType; +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace TypingMessageClass { + class Action { + static STARTED: number; + static STOPPED: number; + } +} + +export declare class VerifiedClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => VerifiedClass; + + destination?: string; + destinationUuid?: string; + identityKey?: ProtoBinaryType; + state?: number; + nullMessage?: ProtoBinaryType; +} + +export declare class WebSocketMessageClass { + constructor(data: any); + encode: () => WebSocketMessageClass; + toArrayBuffer: () => ArrayBuffer; + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => WebSocketMessageClass; + + type?: number; + request?: WebSocketRequestMessageClass; + response?: WebSocketResponseMessageClass; +} + +// Note: we need to use namespaces to express nested classes in Typescript +export declare namespace WebSocketMessageClass { + class Type { + static UNKNOWN: number; + static REQUEST: number; + static RESPONSE: number; + } +} + +export declare class WebSocketRequestMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => WebSocketRequestMessageClass; + verb?: string; + path?: string; + body?: ProtoBinaryType; + headers?: Array; + id?: ProtoBigNumberType; +} + +export declare class WebSocketResponseMessageClass { + static decode: ( + data: ArrayBuffer | ByteBufferClass, + encoding?: string + ) => WebSocketResponseMessageClass; + id?: ProtoBigNumberType; + status?: number; + message?: string; + headers?: Array; + body?: ProtoBinaryType; +} diff --git a/ts/textsecure/AccountManager.ts b/ts/textsecure/AccountManager.ts new file mode 100644 index 0000000000..6d354d7ee4 --- /dev/null +++ b/ts/textsecure/AccountManager.ts @@ -0,0 +1,699 @@ +// tslint:disable no-backbone-get-set-outside-model no-default-export no-unnecessary-local-variable + +import EventTarget from './EventTarget'; +import { WebAPIType } from './WebAPI'; +import MessageReceiver from './MessageReceiver'; +import { KeyPairType, SignedPreKeyType } from '../libsignal.d'; +import utils from './Helpers'; +import PQueue from 'p-queue'; +import ProvisioningCipher from './ProvisioningCipher'; +import WebSocketResource, { + IncomingWebSocketRequest, +} from './WebsocketResources'; + +const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000; + +function getIdentifier(id: string) { + if (!id || !id.length) { + return id; + } + + const parts = id.split('.'); + if (!parts.length) { + return id; + } + + return parts[0]; +} + +type GeneratedKeysType = { + preKeys: Array<{ + keyId: number; + publicKey: ArrayBuffer; + }>; + signedPreKey: { + keyId: number; + publicKey: ArrayBuffer; + signature: ArrayBuffer; + keyPair: KeyPairType; + }; + identityKey: ArrayBuffer; +}; + +export default class AccountManager extends EventTarget { + server: WebAPIType; + pending: Promise; + pendingQueue?: PQueue; + + constructor(username: string, password: string) { + super(); + + this.server = window.WebAPI.connect({ username, password }); + this.pending = Promise.resolve(); + } + + async requestVoiceVerification(number: string) { + return this.server.requestVerificationVoice(number); + } + async requestSMSVerification(number: string) { + return this.server.requestVerificationSMS(number); + } + async encryptDeviceName(name: string, providedIdentityKey?: KeyPairType) { + if (!name) { + return null; + } + const identityKey = + providedIdentityKey || + (await window.textsecure.storage.protocol.getIdentityKeyPair()); + if (!identityKey) { + throw new Error('Identity key was not provided and is not in database!'); + } + const encrypted = await window.Signal.Crypto.encryptDeviceName( + name, + identityKey.pubKey + ); + + const proto = new window.textsecure.protobuf.DeviceName(); + proto.ephemeralPublic = encrypted.ephemeralPublic; + proto.syntheticIv = encrypted.syntheticIv; + proto.ciphertext = encrypted.ciphertext; + + const arrayBuffer = proto.encode().toArrayBuffer(); + return MessageReceiver.arrayBufferToStringBase64(arrayBuffer); + } + async decryptDeviceName(base64: string) { + const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair(); + + const arrayBuffer = MessageReceiver.stringToArrayBufferBase64(base64); + const proto = window.textsecure.protobuf.DeviceName.decode(arrayBuffer); + const encrypted = { + ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(), + syntheticIv: proto.syntheticIv.toArrayBuffer(), + ciphertext: proto.ciphertext.toArrayBuffer(), + }; + + const name = await window.Signal.Crypto.decryptDeviceName( + encrypted, + identityKey.privKey + ); + + return name; + } + async maybeUpdateDeviceName() { + const isNameEncrypted = window.textsecure.storage.user.getDeviceNameEncrypted(); + if (isNameEncrypted) { + return; + } + const deviceName = window.textsecure.storage.user.getDeviceName(); + const base64 = await this.encryptDeviceName(deviceName); + + if (base64) { + await this.server.updateDeviceName(base64); + } + } + async deviceNameIsEncrypted() { + await window.textsecure.storage.user.setDeviceNameEncrypted(); + } + async maybeDeleteSignalingKey() { + const key = window.textsecure.storage.user.getSignalingKey(); + if (key) { + await this.server.removeSignalingKey(); + } + } + async registerSingleDevice(number: string, verificationCode: string) { + const registerKeys = this.server.registerKeys.bind(this.server); + const createAccount = this.createAccount.bind(this); + const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); + const generateKeys = this.generateKeys.bind(this, 100); + const confirmKeys = this.confirmKeys.bind(this); + const registrationDone = this.registrationDone.bind(this); + return this.queueTask(async () => + window.libsignal.KeyHelper.generateIdentityKeyPair().then( + async identityKeyPair => { + const profileKey = window.libsignal.crypto.getRandomBytes(32); + const accessKey = await window.Signal.Crypto.deriveAccessKey( + profileKey + ); + + return createAccount( + number, + verificationCode, + identityKeyPair, + profileKey, + null, + null, + null, + { accessKey } + ) + .then(clearSessionsAndPreKeys) + .then(async () => generateKeys()) + .then(async (keys: GeneratedKeysType) => + registerKeys(keys).then(async () => confirmKeys(keys)) + ) + .then(async () => registrationDone({ number })); + } + ) + ); + } + + // tslint:disable-next-line max-func-body-length + async registerSecondDevice( + setProvisioningUrl: Function, + confirmNumber: (number?: string) => Promise, + progressCallback: Function + ) { + const createAccount = this.createAccount.bind(this); + const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this); + const generateKeys = this.generateKeys.bind(this, 100, progressCallback); + const confirmKeys = this.confirmKeys.bind(this); + const registrationDone = this.registrationDone.bind(this); + const registerKeys = this.server.registerKeys.bind(this.server); + const getSocket = this.server.getProvisioningSocket.bind(this.server); + const queueTask = this.queueTask.bind(this); + const provisioningCipher = new ProvisioningCipher(); + let gotProvisionEnvelope = false; + return provisioningCipher.getPublicKey().then( + async (pubKey: ArrayBuffer) => + new Promise((resolve, reject) => { + const socket = getSocket(); + socket.onclose = event => { + window.log.info('provisioning socket closed. Code:', event.code); + if (!gotProvisionEnvelope) { + reject(new Error('websocket closed')); + } + }; + socket.onopen = () => { + window.log.info('provisioning socket open'); + }; + const wsr = new WebSocketResource(socket, { + keepalive: { path: '/v1/keepalive/provisioning' }, + handleRequest(request: IncomingWebSocketRequest) { + if ( + request.path === '/v1/address' && + request.verb === 'PUT' && + request.body + ) { + const proto = window.textsecure.protobuf.ProvisioningUuid.decode( + request.body + ); + setProvisioningUrl( + [ + 'tsdevice:/?uuid=', + proto.uuid, + '&pub_key=', + encodeURIComponent(btoa(utils.getString(pubKey))), + ].join('') + ); + request.respond(200, 'OK'); + } else if ( + request.path === '/v1/message' && + request.verb === 'PUT' && + request.body + ) { + const envelope = window.textsecure.protobuf.ProvisionEnvelope.decode( + request.body, + 'binary' + ); + request.respond(200, 'OK'); + gotProvisionEnvelope = true; + wsr.close(); + resolve( + provisioningCipher + .decrypt(envelope) + .then(async provisionMessage => + queueTask(async () => + confirmNumber(provisionMessage.number).then( + async deviceName => { + if ( + typeof deviceName !== 'string' || + deviceName.length === 0 + ) { + throw new Error( + 'AccountManager.registerSecondDevice: Invalid device name' + ); + } + if ( + !provisionMessage.number || + !provisionMessage.provisioningCode || + !provisionMessage.identityKeyPair + ) { + throw new Error( + 'AccountManager.registerSecondDevice: Provision message was missing key data' + ); + } + + return createAccount( + provisionMessage.number, + provisionMessage.provisioningCode, + provisionMessage.identityKeyPair, + provisionMessage.profileKey, + deviceName, + provisionMessage.userAgent, + provisionMessage.readReceipts, + { uuid: provisionMessage.uuid } + ) + .then(clearSessionsAndPreKeys) + .then(generateKeys) + .then(async (keys: GeneratedKeysType) => + registerKeys(keys).then(async () => + confirmKeys(keys) + ) + ) + .then(async () => + registrationDone(provisionMessage) + ); + } + ) + ) + ) + ); + } else { + window.log.error('Unknown websocket message', request.path); + } + }, + }); + }) + ); + } + async refreshPreKeys() { + const generateKeys = this.generateKeys.bind(this, 100); + const registerKeys = this.server.registerKeys.bind(this.server); + + return this.queueTask(async () => + this.server.getMyKeys().then(async preKeyCount => { + window.log.info(`prekey count ${preKeyCount}`); + if (preKeyCount < 10) { + return generateKeys().then(registerKeys); + } + return null; + }) + ); + } + async rotateSignedPreKey() { + return this.queueTask(async () => { + const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); + if (typeof signedKeyId !== 'number') { + throw new Error('Invalid signedKeyId'); + } + + const store = window.textsecure.storage.protocol; + const { server, cleanSignedPreKeys } = this; + + return store + .getIdentityKeyPair() + .then( + async (identityKey: KeyPairType) => + window.libsignal.KeyHelper.generateSignedPreKey( + identityKey, + signedKeyId + ), + () => { + // We swallow any error here, because we don't want to get into + // a loop of repeated retries. + window.log.error( + 'Failed to get identity key. Canceling key rotation.' + ); + return null; + } + ) + .then(async (res: SignedPreKeyType | null) => { + if (!res) { + return null; + } + window.log.info('Saving new signed prekey', res.keyId); + return Promise.all([ + window.textsecure.storage.put('signedKeyId', signedKeyId + 1), + store.storeSignedPreKey(res.keyId, res.keyPair), + server.setSignedPreKey({ + keyId: res.keyId, + publicKey: res.keyPair.pubKey, + signature: res.signature, + }), + ]) + .then(async () => { + const confirmed = true; + window.log.info('Confirming new signed prekey', res.keyId); + return Promise.all([ + window.textsecure.storage.remove('signedKeyRotationRejected'), + store.storeSignedPreKey(res.keyId, res.keyPair, confirmed), + ]); + }) + .then(cleanSignedPreKeys); + }) + .catch(async (e: Error) => { + window.log.error( + 'rotateSignedPrekey error:', + e && e.stack ? e.stack : e + ); + + if ( + e instanceof Error && + e.name === 'HTTPError' && + e.code && + e.code >= 400 && + e.code <= 599 + ) { + const rejections = + // tslint:disable-next-line restrict-plus-operands + 1 + window.textsecure.storage.get('signedKeyRotationRejected', 0); + await window.textsecure.storage.put( + 'signedKeyRotationRejected', + rejections + ); + window.log.error('Signed key rotation rejected count:', rejections); + } else { + throw e; + } + }); + }); + } + async queueTask(task: () => Promise) { + this.pendingQueue = this.pendingQueue || new PQueue({ concurrency: 1 }); + const taskWithTimeout = window.textsecure.createTaskWithTimeout(task); + + return this.pendingQueue.add(taskWithTimeout); + } + async cleanSignedPreKeys() { + const MINIMUM_KEYS = 3; + const store = window.textsecure.storage.protocol; + return store.loadSignedPreKeys().then(async allKeys => { + allKeys.sort((a, b) => (a.created_at || 0) - (b.created_at || 0)); + allKeys.reverse(); // we want the most recent first + const confirmed = allKeys.filter(key => key.confirmed); + const unconfirmed = allKeys.filter(key => !key.confirmed); + + const recent = allKeys[0] ? allKeys[0].keyId : 'none'; + const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none'; + window.log.info(`Most recent signed key: ${recent}`); + window.log.info(`Most recent confirmed signed key: ${recentConfirmed}`); + window.log.info( + 'Total signed key count:', + allKeys.length, + '-', + confirmed.length, + 'confirmed' + ); + + let confirmedCount = confirmed.length; + + // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week + await Promise.all( + confirmed.map(async (key, index) => { + if (index < MINIMUM_KEYS) { + return; + } + const createdAt = key.created_at || 0; + const age = Date.now() - createdAt; + + if (age > ARCHIVE_AGE) { + window.log.info( + 'Removing confirmed signed prekey:', + key.keyId, + 'with timestamp:', + new Date(createdAt).toJSON() + ); + await store.removeSignedPreKey(key.keyId); + confirmedCount -= 1; + } + }) + ); + + const stillNeeded = MINIMUM_KEYS - confirmedCount; + + // If we still don't have enough total keys, we keep as many unconfirmed + // keys as necessary. If not necessary, and over a week old, we drop. + await Promise.all( + unconfirmed.map(async (key, index) => { + if (index < stillNeeded) { + return; + } + + const createdAt = key.created_at || 0; + const age = Date.now() - createdAt; + if (age > ARCHIVE_AGE) { + window.log.info( + 'Removing unconfirmed signed prekey:', + key.keyId, + 'with timestamp:', + new Date(createdAt).toJSON() + ); + await store.removeSignedPreKey(key.keyId); + } + }) + ); + }); + } + + // tslint:disable max-func-body-length + async createAccount( + number: string, + verificationCode: string, + identityKeyPair: KeyPairType, + profileKey: ArrayBuffer | undefined, + deviceName: string | null, + userAgent?: string | null, + readReceipts?: boolean | null, + options: { accessKey?: ArrayBuffer; uuid?: string } = {} + ): Promise { + const { accessKey } = options; + let password = btoa( + utils.getString(window.libsignal.crypto.getRandomBytes(16)) + ); + password = password.substring(0, password.length - 2); + const registrationId = window.libsignal.KeyHelper.generateRegistrationId(); + + const previousNumber = getIdentifier( + window.textsecure.storage.get('number_id') + ); + const previousUuid = getIdentifier( + window.textsecure.storage.get('uuid_id') + ); + + let encryptedDeviceName; + if (deviceName) { + encryptedDeviceName = await this.encryptDeviceName( + deviceName, + identityKeyPair + ); + await this.deviceNameIsEncrypted(); + } + + window.log.info( + `createAccount: Number is ${number}, password has length: ${ + password ? password.length : 'none' + }` + ); + + const response = await this.server.confirmCode( + number, + verificationCode, + password, + registrationId, + encryptedDeviceName, + { accessKey } + ); + + const numberChanged = previousNumber && previousNumber !== number; + const uuidChanged = + previousUuid && response.uuid && previousUuid !== response.uuid; + + if (numberChanged || uuidChanged) { + if (numberChanged) { + window.log.warn( + 'New number is different from old number; deleting all previous data' + ); + } + if (uuidChanged) { + window.log.warn( + 'New uuid is different from old uuid; deleting all previous data' + ); + } + + try { + await window.textsecure.storage.protocol.removeAllData(); + window.log.info('Successfully deleted previous data'); + } catch (error) { + window.log.error( + 'Something went wrong deleting data from previous number', + error && error.stack ? error.stack : error + ); + } + } + + await Promise.all([ + window.textsecure.storage.remove('identityKey'), + window.textsecure.storage.remove('password'), + window.textsecure.storage.remove('registrationId'), + window.textsecure.storage.remove('number_id'), + window.textsecure.storage.remove('device_name'), + window.textsecure.storage.remove('regionCode'), + window.textsecure.storage.remove('userAgent'), + window.textsecure.storage.remove('profileKey'), + window.textsecure.storage.remove('read-receipts-setting'), + ]); + + // `setNumberAndDeviceId` and `setUuidAndDeviceId` need to be called + // before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes` + // indirectly calls `ConversationController.getConverationId()` which + // initializes the conversation for the given number (our number) which + // calls out to the user storage API to get the stored UUID and number + // information. + await window.textsecure.storage.user.setNumberAndDeviceId( + number, + response.deviceId || 1, + deviceName + ); + + const setUuid = response.uuid; + if (setUuid) { + await window.textsecure.storage.user.setUuidAndDeviceId( + setUuid, + response.deviceId || 1 + ); + } + + // update our own identity key, which may have changed + // if we're relinking after a reinstall on the master device + await window.textsecure.storage.protocol.saveIdentityWithAttributes( + number, + { + publicKey: identityKeyPair.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: window.textsecure.storage.protocol.VerifiedStatus.VERIFIED, + nonblockingApproval: true, + } + ); + + await window.textsecure.storage.put('identityKey', identityKeyPair); + await window.textsecure.storage.put('password', password); + await window.textsecure.storage.put('registrationId', registrationId); + if (profileKey) { + await window.textsecure.storage.put('profileKey', profileKey); + } + if (userAgent) { + await window.textsecure.storage.put('userAgent', userAgent); + } + + await window.textsecure.storage.put( + 'read-receipt-setting', + Boolean(readReceipts) + ); + + const regionCode = window.libphonenumber.util.getRegionCodeForNumber( + number + ); + await window.textsecure.storage.put('regionCode', regionCode); + await window.textsecure.storage.protocol.hydrateCaches(); + } + async clearSessionsAndPreKeys() { + const store = window.textsecure.storage.protocol; + + window.log.info('clearing all sessions, prekeys, and signed prekeys'); + await Promise.all([ + store.clearPreKeyStore(), + store.clearSignedPreKeysStore(), + store.clearSessionStore(), + ]); + } + // Takes the same object returned by generateKeys + async confirmKeys(keys: GeneratedKeysType) { + const store = window.textsecure.storage.protocol; + const key = keys.signedPreKey; + const confirmed = true; + + if (!key) { + throw new Error('confirmKeys: signedPreKey is null'); + } + + window.log.info('confirmKeys: confirming key', key.keyId); + await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed); + } + async generateKeys(count: number, providedProgressCallback?: Function) { + const progressCallback = + typeof providedProgressCallback === 'function' + ? providedProgressCallback + : null; + const startId = window.textsecure.storage.get('maxPreKeyId', 1); + const signedKeyId = window.textsecure.storage.get('signedKeyId', 1); + + if (typeof startId !== 'number') { + throw new Error('Invalid maxPreKeyId'); + } + if (typeof signedKeyId !== 'number') { + throw new Error('Invalid signedKeyId'); + } + + const store = window.textsecure.storage.protocol; + return store.getIdentityKeyPair().then(async identityKey => { + const result: any = { + preKeys: [], + identityKey: identityKey.pubKey, + }; + const promises = []; + + for (let keyId = startId; keyId < startId + count; keyId += 1) { + promises.push( + window.libsignal.KeyHelper.generatePreKey(keyId).then(async res => { + await store.storePreKey(res.keyId, res.keyPair); + result.preKeys.push({ + keyId: res.keyId, + publicKey: res.keyPair.pubKey, + }); + if (progressCallback) { + progressCallback(); + } + }) + ); + } + + promises.push( + window.libsignal.KeyHelper.generateSignedPreKey( + identityKey, + signedKeyId + ).then(async res => { + await store.storeSignedPreKey(res.keyId, res.keyPair); + result.signedPreKey = { + keyId: res.keyId, + publicKey: res.keyPair.pubKey, + signature: res.signature, + // server.registerKeys doesn't use keyPair, confirmKeys does + keyPair: res.keyPair, + }; + }) + ); + + promises.push( + window.textsecure.storage.put('maxPreKeyId', startId + count) + ); + promises.push( + window.textsecure.storage.put('signedKeyId', signedKeyId + 1) + ); + + return Promise.all(promises).then(async () => + // This is primarily for the signed prekey summary it logs out + this.cleanSignedPreKeys().then(() => result as GeneratedKeysType) + ); + }); + } + async registrationDone({ uuid, number }: { uuid?: string; number?: string }) { + window.log.info('registration done'); + + const identifier = number || uuid; + if (!identifier) { + throw new Error('registrationDone: no identifier!'); + } + + // Ensure that we always have a conversation for ourself + const conversation = await window.ConversationController.getOrCreateAndWait( + identifier, + 'private' + ); + conversation.updateE164(number); + conversation.updateUuid(uuid); + + window.log.info('dispatching registration event'); + + this.dispatchEvent(new Event('registration')); + } +} diff --git a/libtextsecure/contacts_parser.js b/ts/textsecure/ContactsParser.ts similarity index 54% rename from libtextsecure/contacts_parser.js rename to ts/textsecure/ContactsParser.ts index 7cbf85f6ab..b1e103d5af 100644 --- a/libtextsecure/contacts_parser.js +++ b/ts/textsecure/ContactsParser.ts @@ -1,14 +1,33 @@ -/* global dcodeIO, window, textsecure */ +import { ByteBufferClass } from '../window.d'; +import { AttachmentType } from './SendMessage'; + +type ProtobufConstructorType = { + decode: (data: ArrayBuffer) => ProtobufType; +}; + +type ProtobufType = { + avatar?: PackedAttachmentType; + profileKey?: any; + uuid?: string; + members: Array; +}; + +export type PackedAttachmentType = AttachmentType & { + length: number; +}; + +export class ProtoParser { + buffer: ByteBufferClass; + protobuf: ProtobufConstructorType; + + 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; + } -function ProtoParser(arrayBuffer, protobuf) { - this.protobuf = protobuf; - this.buffer = new dcodeIO.ByteBuffer(); - this.buffer.append(arrayBuffer); - this.buffer.offset = 0; - this.buffer.limit = arrayBuffer.byteLength; -} -ProtoParser.prototype = { - constructor: ProtoParser, next() { try { if (this.buffer.limit === this.buffer.offset) { @@ -18,8 +37,6 @@ ProtoParser.prototype = { const nextBuffer = this.buffer .slice(this.buffer.offset, this.buffer.offset + len) .toArrayBuffer(); - // TODO: de-dupe ByteBuffer.js includes in libaxo/libts - // then remove this toArrayBuffer call. const proto = this.protobuf.decode(nextBuffer); this.buffer.skip(len); @@ -61,15 +78,17 @@ ProtoParser.prototype = { } return null; - }, -}; -const GroupBuffer = function Constructor(arrayBuffer) { - ProtoParser.call(this, arrayBuffer, textsecure.protobuf.GroupDetails); -}; -GroupBuffer.prototype = Object.create(ProtoParser.prototype); -GroupBuffer.prototype.constructor = GroupBuffer; -const ContactBuffer = function Constructor(arrayBuffer) { - ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails); -}; -ContactBuffer.prototype = Object.create(ProtoParser.prototype); -ContactBuffer.prototype.constructor = ContactBuffer; + } +} + +export class GroupBuffer extends ProtoParser { + constructor(arrayBuffer: ArrayBuffer) { + super(arrayBuffer, window.textsecure.protobuf.GroupDetails as any); + } +} + +export class ContactBuffer extends ProtoParser { + constructor(arrayBuffer: ArrayBuffer) { + super(arrayBuffer, window.textsecure.protobuf.ContactDetails as any); + } +} diff --git a/ts/textsecure/Crypto.ts b/ts/textsecure/Crypto.ts new file mode 100644 index 0000000000..84866d4f81 --- /dev/null +++ b/ts/textsecure/Crypto.ts @@ -0,0 +1,269 @@ +// tslint:disable no-bitwise no-default-export + +import { ByteBufferClass } from '../window.d'; + +const PROFILE_IV_LENGTH = 12; // bytes +const PROFILE_KEY_LENGTH = 32; // bytes +const PROFILE_TAG_LENGTH = 128; // bits +const PROFILE_NAME_PADDED_LENGTH = 53; // bytes + +function verifyDigest(data: ArrayBuffer, theirDigest: ArrayBuffer) { + return window.crypto.subtle + .digest({ name: 'SHA-256' }, data) + .then(ourDigest => { + const a = new Uint8Array(ourDigest); + const b = new Uint8Array(theirDigest); + let result = 0; + for (let i = 0; i < theirDigest.byteLength; i += 1) { + result |= a[i] ^ b[i]; + } + if (result !== 0) { + throw new Error('Bad digest'); + } + }); +} + +function calculateDigest(data: ArrayBuffer) { + return window.crypto.subtle.digest({ name: 'SHA-256' }, data); +} + +const Crypto = { + // Decrypts message into a raw string + async decryptWebsocketMessage( + message: ByteBufferClass, + signalingKey: ArrayBuffer + ) { + const decodedMessage = message.toArrayBuffer(); + + if (signalingKey.byteLength !== 52) { + throw new Error('Got invalid length signalingKey'); + } + if (decodedMessage.byteLength < 1 + 16 + 10) { + throw new Error('Got invalid length message'); + } + if (new Uint8Array(decodedMessage)[0] !== 1) { + throw new Error( + `Got bad version number: ${new Uint8Array(decodedMessage)[0]}` + ); + } + + const aesKey = signalingKey.slice(0, 32); + const macKey = signalingKey.slice(32, 32 + 20); + + const iv = decodedMessage.slice(1, 1 + 16); + const ciphertext = decodedMessage.slice( + 1 + 16, + decodedMessage.byteLength - 10 + ); + const ivAndCiphertext = decodedMessage.slice( + 0, + decodedMessage.byteLength - 10 + ); + const mac = decodedMessage.slice( + decodedMessage.byteLength - 10, + decodedMessage.byteLength + ); + + return window.libsignal.crypto + .verifyMAC(ivAndCiphertext, macKey, mac, 10) + .then(async () => + window.libsignal.crypto.decrypt(aesKey, ciphertext, iv) + ); + }, + + async decryptAttachment( + encryptedBin: ArrayBuffer, + keys: ArrayBuffer, + theirDigest: ArrayBuffer + ) { + if (keys.byteLength !== 64) { + throw new Error('Got invalid length attachment keys'); + } + if (encryptedBin.byteLength < 16 + 32) { + throw new Error('Got invalid length attachment'); + } + + const aesKey = keys.slice(0, 32); + const macKey = keys.slice(32, 64); + + const iv = encryptedBin.slice(0, 16); + const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32); + const ivAndCiphertext = encryptedBin.slice(0, encryptedBin.byteLength - 32); + const mac = encryptedBin.slice( + encryptedBin.byteLength - 32, + encryptedBin.byteLength + ); + + return window.libsignal.crypto + .verifyMAC(ivAndCiphertext, macKey, mac, 32) + .then(async () => { + if (theirDigest) { + return verifyDigest(encryptedBin, theirDigest); + } + + return null; + }) + .then(async () => + window.libsignal.crypto.decrypt(aesKey, ciphertext, iv) + ); + }, + + async encryptAttachment( + plaintext: ArrayBuffer, + keys: ArrayBuffer, + iv: ArrayBuffer + ) { + if (!(plaintext instanceof ArrayBuffer) && !ArrayBuffer.isView(plaintext)) { + throw new TypeError( + `\`plaintext\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof plaintext}` + ); + } + + if (keys.byteLength !== 64) { + throw new Error('Got invalid length attachment keys'); + } + if (iv.byteLength !== 16) { + throw new Error('Got invalid length attachment iv'); + } + const aesKey = keys.slice(0, 32); + const macKey = keys.slice(32, 64); + + return window.libsignal.crypto + .encrypt(aesKey, plaintext, iv) + .then(async ciphertext => { + const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength); + ivAndCiphertext.set(new Uint8Array(iv)); + ivAndCiphertext.set(new Uint8Array(ciphertext), 16); + + return window.libsignal.crypto + .calculateMAC(macKey, ivAndCiphertext.buffer as ArrayBuffer) + .then(async mac => { + const encryptedBin = new Uint8Array( + 16 + ciphertext.byteLength + 32 + ); + encryptedBin.set(ivAndCiphertext); + encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength); + return calculateDigest(encryptedBin.buffer as ArrayBuffer).then( + digest => ({ + ciphertext: encryptedBin.buffer, + digest, + }) + ); + }); + }); + }, + async encryptProfile(data: ArrayBuffer, key: ArrayBuffer) { + const iv = window.libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH); + if (key.byteLength !== PROFILE_KEY_LENGTH) { + throw new Error('Got invalid length profile key'); + } + if (iv.byteLength !== PROFILE_IV_LENGTH) { + throw new Error('Got invalid length profile iv'); + } + return window.crypto.subtle + .importKey('raw', key, { name: 'AES-GCM' } as any, false, ['encrypt']) + .then(async keyForEncryption => + window.crypto.subtle + .encrypt( + { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH }, + keyForEncryption, + data + ) + .then(ciphertext => { + const ivAndCiphertext = new Uint8Array( + PROFILE_IV_LENGTH + ciphertext.byteLength + ); + ivAndCiphertext.set(new Uint8Array(iv)); + ivAndCiphertext.set(new Uint8Array(ciphertext), PROFILE_IV_LENGTH); + return ivAndCiphertext.buffer; + }) + ); + }, + async decryptProfile(data: ArrayBuffer, key: ArrayBuffer) { + if (data.byteLength < 12 + 16 + 1) { + throw new Error(`Got too short input: ${data.byteLength}`); + } + const iv = data.slice(0, PROFILE_IV_LENGTH); + const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength); + if (key.byteLength !== PROFILE_KEY_LENGTH) { + throw new Error('Got invalid length profile key'); + } + if (iv.byteLength !== PROFILE_IV_LENGTH) { + throw new Error('Got invalid length profile iv'); + } + const error = new Error(); // save stack + return window.crypto.subtle + .importKey('raw', key, { name: 'AES-GCM' } as any, false, ['decrypt']) + .then(async keyForEncryption => + window.crypto.subtle + .decrypt( + { name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH }, + keyForEncryption, + ciphertext + ) + // Typescript says that there's no .catch() available here + // @ts-ignore + .catch((e: Error) => { + if (e.name === 'OperationError') { + // bad mac, basically. + error.message = + 'Failed to decrypt profile data. Most likely the profile key has changed.'; + error.name = 'ProfileDecryptError'; + throw error; + } + }) + ); + }, + async encryptProfileName(name: ArrayBuffer, key: ArrayBuffer) { + const padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH); + padded.set(new Uint8Array(name)); + return Crypto.encryptProfile(padded.buffer as ArrayBuffer, key); + }, + async decryptProfileName(encryptedProfileName: string, key: ArrayBuffer) { + const data = window.dcodeIO.ByteBuffer.wrap( + encryptedProfileName, + 'base64' + ).toArrayBuffer(); + return Crypto.decryptProfile(data, key).then(decrypted => { + const padded = new Uint8Array(decrypted); + + // Given name is the start of the string to the first null character + let givenEnd; + for (givenEnd = 0; givenEnd < padded.length; givenEnd += 1) { + if (padded[givenEnd] === 0x00) { + break; + } + } + + // Family name is the next chunk of non-null characters after that first null + let familyEnd; + for ( + familyEnd = givenEnd + 1; + familyEnd < padded.length; + familyEnd += 1 + ) { + if (padded[familyEnd] === 0x00) { + break; + } + } + const foundFamilyName = familyEnd > givenEnd + 1; + + return { + given: window.dcodeIO.ByteBuffer.wrap(padded) + .slice(0, givenEnd) + .toArrayBuffer(), + family: foundFamilyName + ? window.dcodeIO.ByteBuffer.wrap(padded) + .slice(givenEnd + 1, familyEnd) + .toArrayBuffer() + : null, + }; + }); + }, + + getRandomBytes(size: number) { + return window.libsignal.crypto.getRandomBytes(size); + }, +}; + +export default Crypto; diff --git a/ts/textsecure/Errors.ts b/ts/textsecure/Errors.ts new file mode 100644 index 0000000000..806992fd98 --- /dev/null +++ b/ts/textsecure/Errors.ts @@ -0,0 +1,164 @@ +// tslint:disable max-classes-per-file + +function appendStack(newError: Error, originalError: Error) { + // eslint-disable-next-line no-param-reassign + newError.stack += `\nOriginal stack:\n${originalError.stack}`; +} + +export class ReplayableError extends Error { + name: string; + message: string; + functionCode?: number; + + constructor(options: { + name?: string; + message: string; + functionCode?: number; + }) { + super(options.message); + + this.name = options.name || 'ReplayableError'; + this.message = options.message; + + // Maintains proper stack trace, where our error was thrown (only available on V8) + // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } + + this.functionCode = options.functionCode; + } +} + +export class IncomingIdentityKeyError extends ReplayableError { + identifier: string; + identityKey: ArrayBuffer; + + // Note: Data to resend message is no longer captured + constructor(incomingIdentifier: string, _m: ArrayBuffer, key: ArrayBuffer) { + const identifer = incomingIdentifier.split('.')[0]; + + super({ + name: 'IncomingIdentityKeyError', + message: `The identity of ${identifer} has changed.`, + }); + + this.identifier = identifer; + this.identityKey = key; + } +} + +export class OutgoingIdentityKeyError extends ReplayableError { + identifier: string; + identityKey: ArrayBuffer; + + // Note: Data to resend message is no longer captured + constructor( + incomingIdentifier: string, + _m: ArrayBuffer, + _t: number, + identityKey: ArrayBuffer + ) { + const identifier = incomingIdentifier.split('.')[0]; + + super({ + name: 'OutgoingIdentityKeyError', + message: `The identity of ${identifier} has changed.`, + }); + + this.identifier = identifier; + this.identityKey = identityKey; + } +} + +export class OutgoingMessageError extends ReplayableError { + identifier: string; + code?: any; + + // Note: Data to resend message is no longer captured + constructor( + incomingIdentifier: string, + _m: ArrayBuffer, + _t: number, + httpError?: Error + ) { + const identifier = incomingIdentifier.split('.')[0]; + + super({ + name: 'OutgoingMessageError', + message: httpError ? httpError.message : 'no http error', + }); + + this.identifier = identifier; + + if (httpError) { + this.code = httpError.code; + appendStack(this, httpError); + } + } +} + +export class SendMessageNetworkError extends ReplayableError { + identifier: string; + + constructor(identifier: string, _m: any, httpError: Error) { + super({ + name: 'SendMessageNetworkError', + message: httpError.message, + }); + + this.identifier = identifier.split('.')[0]; + this.code = httpError.code; + + appendStack(this, httpError); + } +} + +export class SignedPreKeyRotationError extends ReplayableError { + constructor() { + super({ + name: 'SignedPreKeyRotationError', + message: 'Too many signed prekey rotation failures', + }); + } +} + +export class MessageError extends ReplayableError { + code?: any; + + constructor(_m: any, httpError: Error) { + super({ + name: 'MessageError', + message: httpError.message, + }); + + this.code = httpError.code; + + appendStack(this, httpError); + } +} + +export class UnregisteredUserError extends Error { + identifier: string; + code?: any; + + constructor(identifier: string, httpError: Error) { + const message = httpError.message; + + super(message); + + this.message = message; + this.name = 'UnregisteredUserError'; + + // Maintains proper stack trace, where our error was thrown (only available on V8) + // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } + + this.identifier = identifier; + this.code = httpError.code; + + appendStack(this, httpError); + } +} diff --git a/ts/textsecure/EventTarget.ts b/ts/textsecure/EventTarget.ts new file mode 100644 index 0000000000..5467108bea --- /dev/null +++ b/ts/textsecure/EventTarget.ts @@ -0,0 +1,81 @@ +// tslint:disable no-default-export + +/* + * Implements EventTarget + * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget + */ + +export default class EventTarget { + listeners?: { [type: string]: Array }; + + dispatchEvent(ev: Event) { + if (!(ev instanceof Event)) { + throw new Error('Expects an event'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + const listeners = this.listeners[ev.type]; + const results = []; + if (typeof listeners === 'object') { + const max = listeners.length; + for (let i = 0; i < max; i += 1) { + const listener = listeners[i]; + if (typeof listener === 'function') { + results.push(listener.call(null, ev)); + } + } + } + return results; + } + + addEventListener(eventName: string, callback: Function) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + let listeners = this.listeners[eventName]; + if (typeof listeners !== 'object') { + listeners = []; + } + listeners.push(callback); + this.listeners[eventName] = listeners; + } + + removeEventListener(eventName: string, callback: Function) { + if (typeof eventName !== 'string') { + throw new Error('First argument expects a string'); + } + if (typeof callback !== 'function') { + throw new Error('Second argument expects a function'); + } + if (this.listeners === null || typeof this.listeners !== 'object') { + this.listeners = {}; + } + const listeners = this.listeners[eventName]; + if (typeof listeners === 'object') { + for (let i = 0; i < listeners.length; i += 1) { + if (listeners[i] === callback) { + listeners.splice(i, 1); + return; + } + } + } + this.listeners[eventName] = listeners; + } + + extend(source: any) { + const target = this as any; + + // tslint:disable-next-line forin no-for-in no-default-export + for (const prop in source) { + target[prop] = source[prop]; + } + return target; + } +} diff --git a/ts/textsecure/Helpers.ts b/ts/textsecure/Helpers.ts new file mode 100644 index 0000000000..6312ac20e2 --- /dev/null +++ b/ts/textsecure/Helpers.ts @@ -0,0 +1,98 @@ +// tslint:disable no-default-export + +import { ByteBufferClass } from '../window.d'; + +let ByteBuffer: ByteBufferClass | undefined; +const arrayBuffer = new ArrayBuffer(0); +const uint8Array = new Uint8Array(); + +let StaticByteBufferProto: any; +// @ts-ignore +const StaticArrayBufferProto = arrayBuffer.__proto__; +// @ts-ignore +const StaticUint8ArrayProto = uint8Array.__proto__; + +function getString(thing: any): string { + // Note: we must make this at runtime because it's loaded in the browser context + if (!ByteBuffer) { + ByteBuffer = new window.dcodeIO.ByteBuffer(); + } + + if (!StaticByteBufferProto) { + // @ts-ignore + StaticByteBufferProto = ByteBuffer.__proto__; + } + + if (thing === Object(thing)) { + if (thing.__proto__ === StaticUint8ArrayProto) { + return String.fromCharCode.apply(null, thing); + } + if (thing.__proto__ === StaticArrayBufferProto) { + return getString(new Uint8Array(thing)); + } + if (thing.__proto__ === StaticByteBufferProto) { + return thing.toString('binary'); + } + } + return thing; +} + +function getStringable(thing: any): boolean { + return ( + typeof thing === 'string' || + typeof thing === 'number' || + typeof thing === 'boolean' || + (thing === Object(thing) && + (thing.__proto__ === StaticArrayBufferProto || + thing.__proto__ === StaticUint8ArrayProto || + thing.__proto__ === StaticByteBufferProto)) + ); +} + +function ensureStringed(thing: any): any { + if (getStringable(thing)) { + return getString(thing); + } else if (thing instanceof Array) { + const res = []; + for (let i = 0; i < thing.length; i += 1) { + res[i] = ensureStringed(thing[i]); + } + + return res; + } else if (thing === Object(thing)) { + const res: any = {}; + // tslint:disable-next-line forin no-for-in no-default-export + for (const key in thing) { + res[key] = ensureStringed(thing[key]); + } + + return res; + } else if (thing === null) { + return null; + } + throw new Error(`unsure of how to jsonify object of type ${typeof thing}`); +} + +function stringToArrayBuffer(string: string) { + if (typeof string !== 'string') { + throw new TypeError("'string' must be a string"); + } + + const array = new Uint8Array(string.length); + for (let i = 0; i < string.length; i += 1) { + array[i] = string.charCodeAt(i); + } + return array.buffer; +} + +// Number formatting utils +const utils = { + getString, + isNumberSane: (number: string) => + number[0] === '+' && /^[0-9]+$/.test(number.substring(1)), + jsonThing: (thing: any) => JSON.stringify(ensureStringed(thing)), + stringToArrayBuffer, + unencodeNumber: (number: string) => number.split('.'), +}; + +export default utils; diff --git a/libtextsecure/message_receiver.js b/ts/textsecure/MessageReceiver.ts similarity index 64% rename from libtextsecure/message_receiver.js rename to ts/textsecure/MessageReceiver.ts index 21c71ca0ef..9cf2a0f190 100644 --- a/libtextsecure/message_receiver.js +++ b/ts/textsecure/MessageReceiver.ts @@ -1,85 +1,189 @@ -/* global window: false */ -/* global textsecure: false */ -/* global WebAPI: false */ -/* global libsignal: false */ -/* global WebSocketResource: false */ -/* global WebSocket: false */ -/* global Event: false */ -/* global dcodeIO: false */ -/* global _: false */ -/* global ContactBuffer: false */ -/* global GroupBuffer: false */ +// tslint:disable no-bitwise no-default-export -/* eslint-disable more/no-then */ +import { isNumber, map, omit } from 'lodash'; +import { w3cwebsocket as WebSocket } from 'websocket'; +import PQueue from 'p-queue'; +import { v4 as getGuid } from 'uuid'; + +import EventTarget from './EventTarget'; +import { WebAPIType } from './WebAPI'; +import { BatcherType, createBatcher } from '../util/batcher'; +import utils from './Helpers'; +import WebSocketResource, { + IncomingWebSocketRequest, +} from './WebsocketResources'; +import Crypto from './Crypto'; +import { SessionCipherClass, SignalProtocolAddressClass } from '../libsignal.d'; +import { ContactBuffer, GroupBuffer } from './ContactsParser'; +import { IncomingIdentityKeyError } from './Errors'; + +import { + AttachmentPointerClass, + DataMessageClass, + EnvelopeClass, + ReceiptMessageClass, + SyncMessageClass, + TypingMessageClass, + UnprocessedType, + VerifiedClass, +} from '../textsecure.d'; const RETRY_TIMEOUT = 2 * 60 * 1000; -function MessageReceiver( - oldUsername, - username, - password, - signalingKey, - options = {} -) { - this.count = 0; - - this.signalingKey = signalingKey; - this.username = oldUsername; - this.uuid = username; - this.password = password; - this.server = WebAPI.connect({ username: username || oldUsername, password }); - - if (!options.serverTrustRoot) { - throw new Error('Server trust root is required!'); +declare global { + interface Event { + code?: string | number; + configuration?: any; + confirm?: () => void; + contactDetails?: any; + count?: number; + data?: any; + deliveryReceipt?: any; + error?: any; + groupDetails?: any; + proto?: any; + read?: any; + reason?: any; + sender?: any; + senderDevice?: any; + senderUuid?: any; + source?: any; + sourceUuid?: any; + stickerPacks?: any; + timestamp?: any; + typing?: any; + verified?: any; } - this.serverTrustRoot = window.Signal.Crypto.base64ToArrayBuffer( - options.serverTrustRoot - ); - - this.number_id = oldUsername - ? textsecure.utils.unencodeNumber(oldUsername)[0] - : null; - this.uuid_id = username ? textsecure.utils.unencodeNumber(username)[0] : null; - // eslint-disable-next-line prefer-destructuring - this.deviceId = textsecure.utils.unencodeNumber(username || oldUsername)[1]; - - this.incomingQueue = new window.PQueue({ concurrency: 1 }); - this.pendingQueue = new window.PQueue({ concurrency: 1 }); - this.appQueue = new window.PQueue({ concurrency: 1 }); - - this.cacheAddBatcher = window.Signal.Util.createBatcher({ - wait: 200, - maxSize: 30, - processBatch: this.cacheAndQueueBatch.bind(this), - }); - this.cacheUpdateBatcher = window.Signal.Util.createBatcher({ - wait: 500, - maxSize: 30, - processBatch: this.cacheUpdateBatch.bind(this), - }); - this.cacheRemoveBatcher = window.Signal.Util.createBatcher({ - wait: 500, - maxSize: 30, - processBatch: this.cacheRemoveBatch.bind(this), - }); - - if (options.retryCached) { - this.pendingQueue.add(() => this.queueAllCached()); + interface Error { + reason?: any; + sender?: SignalProtocolAddressClass; + senderUuid?: SignalProtocolAddressClass; } } -MessageReceiver.stringToArrayBuffer = string => - dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer(); -MessageReceiver.arrayBufferToString = arrayBuffer => - dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'); -MessageReceiver.stringToArrayBufferBase64 = string => - dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer(); -MessageReceiver.arrayBufferToStringBase64 = arrayBuffer => - dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64'); +type AttachmentType = { + id?: string; + data: ArrayBuffer; + contentType?: string; + size?: number; + fileName?: string; + flags?: number; + width?: number; + height?: number; + caption?: string; +}; + +type CacheAddItemType = { + envelope: EnvelopeClass; + data: UnprocessedType; + request: IncomingWebSocketRequest; +}; + +type CacheUpdateItemType = { + id: string; + data: Partial; +}; + +class MessageReceiverInner extends EventTarget { + _onClose?: (ev: any) => Promise; + appQueue: PQueue; + cacheAddBatcher: BatcherType; + cacheRemoveBatcher: BatcherType; + cacheUpdateBatcher: BatcherType; + calledClose?: boolean; + count: number; + deviceId: number; + hasConnected?: boolean; + incomingQueue: PQueue; + isEmptied?: boolean; + // tslint:disable-next-line variable-name + number_id: string | null; + password: string; + pendingQueue: PQueue; + retryCachedTimeout: any; + server: WebAPIType; + serverTrustRoot: ArrayBuffer; + signalingKey: ArrayBuffer; + socket?: WebSocket; + stoppingProcessing?: boolean; + username: string; + uuid: string; + // tslint:disable-next-line variable-name + uuid_id: string | null; + wsr?: WebSocketResource; + + constructor( + oldUsername: string, + username: string, + password: string, + signalingKey: ArrayBuffer, + options: { + serverTrustRoot: string; + retryCached?: string; + } + ) { + super(); + + this.count = 0; + + this.signalingKey = signalingKey; + this.username = oldUsername; + this.uuid = username; + this.password = password; + this.server = window.WebAPI.connect({ + username: username || oldUsername, + password, + }); + + if (!options.serverTrustRoot) { + throw new Error('Server trust root is required!'); + } + this.serverTrustRoot = MessageReceiverInner.stringToArrayBufferBase64( + options.serverTrustRoot + ); + + this.number_id = oldUsername ? utils.unencodeNumber(oldUsername)[0] : null; + this.uuid_id = username ? utils.unencodeNumber(username)[0] : null; + this.deviceId = parseInt( + utils.unencodeNumber(username || oldUsername)[1], + 10 + ); + + this.incomingQueue = new PQueue({ concurrency: 1 }); + this.pendingQueue = new PQueue({ concurrency: 1 }); + this.appQueue = new PQueue({ concurrency: 1 }); + + this.cacheAddBatcher = createBatcher({ + wait: 200, + maxSize: 30, + processBatch: this.cacheAndQueueBatch.bind(this), + }); + this.cacheUpdateBatcher = createBatcher({ + wait: 500, + maxSize: 30, + processBatch: this.cacheUpdateBatch.bind(this), + }); + this.cacheRemoveBatcher = createBatcher({ + wait: 500, + maxSize: 30, + processBatch: this.cacheRemoveBatch.bind(this), + }); + + if (options.retryCached) { + // tslint:disable-next-line no-floating-promises + this.pendingQueue.add(async () => this.queueAllCached()); + } + } + + static stringToArrayBuffer = (string: string): ArrayBuffer => + window.dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer(); + static arrayBufferToString = (arrayBuffer: ArrayBuffer): string => + window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'); + static stringToArrayBufferBase64 = (string: string): ArrayBuffer => + window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer(); + static arrayBufferToStringBase64 = (arrayBuffer: ArrayBuffer): string => + window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64'); -MessageReceiver.prototype = new textsecure.EventTarget(); -MessageReceiver.prototype.extend({ - constructor: MessageReceiver, connect() { if (this.calledClose) { return; @@ -96,7 +200,9 @@ MessageReceiver.prototype.extend({ if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { this.socket.close(); - this.wsr.close(); + if (this.wsr) { + this.wsr.close(); + } } // initialize the socket and start listening for messages this.socket = this.server.getMessageSocket(); @@ -113,33 +219,43 @@ MessageReceiver.prototype.extend({ // Because sometimes the socket doesn't properly emit its close event this._onClose = this.onclose.bind(this); - this.wsr.addEventListener('close', this._onClose); - }, - stopProcessing() { + if (this._onClose) { + this.wsr.addEventListener('close', this._onClose); + } + } + + async stopProcessing() { window.log.info('MessageReceiver: stopProcessing requested'); this.stoppingProcessing = true; return this.close(); - }, + } + unregisterBatchers() { window.log.info('MessageReceiver: unregister batchers'); this.cacheAddBatcher.unregister(); this.cacheUpdateBatcher.unregister(); this.cacheRemoveBatcher.unregister(); - }, + } + shutdown() { if (this.socket) { + // @ts-ignore this.socket.onclose = null; + // @ts-ignore this.socket.onerror = null; + // @ts-ignore this.socket.onopen = null; - this.socket = null; + this.socket = undefined; } if (this.wsr) { - this.wsr.removeEventListener('close', this._onClose); - this.wsr = null; + if (this._onClose) { + this.wsr.removeEventListener('close', this._onClose); + } + this.wsr = undefined; } - }, - close() { + } + async close() { window.log.info('MessageReceiver.close()'); this.calledClose = true; @@ -152,19 +268,20 @@ MessageReceiver.prototype.extend({ this.clearRetryTimeout(); return this.drain(); - }, + } onopen() { window.log.info('websocket open'); - }, + } onerror() { window.log.error('websocket error'); - }, - dispatchAndWait(event) { - this.appQueue.add(() => Promise.all(this.dispatchEvent(event))); + } + async dispatchAndWait(event: Event) { + // tslint:disable-next-line no-floating-promises + this.appQueue.add(async () => Promise.all(this.dispatchEvent(event))); return Promise.resolve(); - }, - onclose(ev) { + } + async onclose(ev: any) { window.log.info( 'websocket closed', ev.code, @@ -186,15 +303,15 @@ MessageReceiver.prototype.extend({ } // possible 403 or network issue. Make an request to confirm return this.server - .getDevices(this.number_id || this.uuid_id) + .getDevices() .then(this.connect.bind(this)) // No HTTP error? Reconnect - .catch(e => { + .catch(async e => { const event = new Event('error'); event.error = e; return this.dispatchAndWait(event); }); - }, - handleRequest(request) { + } + handleRequest(request: IncomingWebSocketRequest) { // We do the message decryption here, instead of in the ordered pending queue, // to avoid exposing the time it took us to process messages through the time-to-ack. @@ -203,7 +320,10 @@ MessageReceiver.prototype.extend({ request.respond(200, 'OK'); if (request.verb === 'PUT' && request.path === '/api/v1/queue/empty') { - this.incomingQueue.add(() => this.onEmpty()); + // tslint:disable-next-line no-floating-promises + this.incomingQueue.add(() => { + this.onEmpty(); + }); } return; } @@ -212,8 +332,14 @@ MessageReceiver.prototype.extend({ let plaintext; const headers = request.headers || []; + if (!request.body) { + throw new Error( + 'MessageReceiver.handleRequest: request.body was falsey!' + ); + } + if (headers.includes('X-Signal-Key: true')) { - plaintext = await textsecure.crypto.decryptWebsocketMessage( + plaintext = await Crypto.decryptWebsocketMessage( request.body, this.signalingKey ); @@ -222,7 +348,7 @@ MessageReceiver.prototype.extend({ } try { - const envelope = textsecure.protobuf.Envelope.decode(plaintext); + const envelope = window.textsecure.protobuf.Envelope.decode(plaintext); window.normalizeUuids( envelope, ['sourceUuid'], @@ -232,22 +358,19 @@ MessageReceiver.prototype.extend({ // fault, and we should handle them gracefully and tell the // user they received an invalid message - if (this.isBlocked(envelope.source)) { + if (envelope.source && this.isBlocked(envelope.source)) { request.respond(200, 'OK'); return; } - if (this.isUuidBlocked(envelope.sourceUuid)) { + if (envelope.sourceUuid && this.isUuidBlocked(envelope.sourceUuid)) { request.respond(200, 'OK'); return; } // Make non-private envelope IDs dashless so they don't get redacted // from logs - envelope.id = (envelope.serverGuid || window.getGuid()).replace( - /-/g, - '' - ); + envelope.id = (envelope.serverGuid || getGuid()).replace(/-/g, ''); envelope.serverTimestamp = envelope.serverTimestamp ? envelope.serverTimestamp.toNumber() : null; @@ -265,9 +388,10 @@ MessageReceiver.prototype.extend({ } }; + // tslint:disable-next-line no-floating-promises this.incomingQueue.add(job); - }, - addToQueue(task) { + } + async addToQueue(task: () => Promise) { this.count += 1; const promise = this.pendingQueue.add(task); @@ -281,27 +405,29 @@ MessageReceiver.prototype.extend({ promise.then(update, update); return promise; - }, + } onEmpty() { const emitEmpty = () => { window.log.info("MessageReceiver: emitting 'empty' event"); const ev = new Event('empty'); - this.dispatchAndWait(ev); + this.dispatchEvent(ev); this.isEmptied = true; this.maybeScheduleRetryTimeout(); }; - const waitForPendingQueue = () => { + const waitForPendingQueue = async () => { window.log.info( "MessageReceiver: finished processing messages after 'empty', now waiting for application" ); // We don't await here because we don't want this to gate future message processing + // tslint:disable-next-line no-floating-promises this.appQueue.add(emitEmpty); }; const waitForIncomingQueue = () => { + // tslint:disable-next-line no-floating-promises this.addToQueue(waitForPendingQueue); // Note: this.count is used in addToQueue @@ -311,20 +437,22 @@ MessageReceiver.prototype.extend({ const waitForCacheAddBatcher = async () => { await this.cacheAddBatcher.onIdle(); + // tslint:disable-next-line no-floating-promises this.incomingQueue.add(waitForIncomingQueue); }; + // tslint:disable-next-line no-floating-promises waitForCacheAddBatcher(); - }, - drain() { - const waitForIncomingQueue = () => - this.addToQueue(() => { + } + async drain() { + const waitForIncomingQueue = async () => + this.addToQueue(async () => { window.log.info('drained'); }); return this.incomingQueue.add(waitForIncomingQueue); - }, - updateProgress(count) { + } + updateProgress(count: number) { // count by 10s if (count % 10 !== 0) { return; @@ -332,30 +460,36 @@ MessageReceiver.prototype.extend({ const ev = new Event('progress'); ev.count = count; this.dispatchEvent(ev); - }, + } async queueAllCached() { const items = await this.getAllFromCache(); - for (let i = 0, max = items.length; i < max; i += 1) { + const max = items.length; + for (let i = 0; i < max; i += 1) { // eslint-disable-next-line no-await-in-loop await this.queueCached(items[i]); } - }, - async queueCached(item) { + } + async queueCached(item: UnprocessedType) { try { - let envelopePlaintext = item.envelope; + let envelopePlaintext: ArrayBuffer; - if (item.version === 2) { - envelopePlaintext = MessageReceiver.stringToArrayBufferBase64( - envelopePlaintext + if (item.envelope && item.version === 2) { + envelopePlaintext = MessageReceiverInner.stringToArrayBufferBase64( + item.envelope + ); + } else if (item.envelope && typeof item.envelope === 'string') { + envelopePlaintext = MessageReceiverInner.stringToArrayBuffer( + item.envelope + ); + } else { + throw new Error( + 'MessageReceiver.queueCached: item.envelope was malformed' ); } - if (typeof envelopePlaintext === 'string') { - envelopePlaintext = MessageReceiver.stringToArrayBuffer( - envelopePlaintext - ); - } - const envelope = textsecure.protobuf.Envelope.decode(envelopePlaintext); + const envelope = window.textsecure.protobuf.Envelope.decode( + envelopePlaintext + ); envelope.id = envelope.serverGuid || item.id; envelope.source = envelope.source || item.source; envelope.sourceUuid = envelope.sourceUuid || item.sourceUuid; @@ -365,21 +499,23 @@ MessageReceiver.prototype.extend({ const { decrypted } = item; if (decrypted) { - let payloadPlaintext = decrypted; + let payloadPlaintext: ArrayBuffer; if (item.version === 2) { - payloadPlaintext = MessageReceiver.stringToArrayBufferBase64( - payloadPlaintext + payloadPlaintext = MessageReceiverInner.stringToArrayBufferBase64( + decrypted ); - } - - if (typeof payloadPlaintext === 'string') { - payloadPlaintext = MessageReceiver.stringToArrayBuffer( - payloadPlaintext + } else if (typeof decrypted === 'string') { + payloadPlaintext = MessageReceiverInner.stringToArrayBuffer( + decrypted ); + } else { + throw new Error('Cached decrypted value was not a string!'); } + // tslint:disable-next-line no-floating-promises this.queueDecryptedEnvelope(envelope, payloadPlaintext); } else { + // tslint:disable-next-line no-floating-promises this.queueEnvelope(envelope); } } catch (error) { @@ -392,7 +528,7 @@ MessageReceiver.prototype.extend({ try { const { id } = item; - await textsecure.storage.unprocessed.remove(id); + await window.textsecure.storage.unprocessed.remove(id); } catch (deleteError) { window.log.error( 'queueCached error deleting item', @@ -402,8 +538,8 @@ MessageReceiver.prototype.extend({ ); } } - }, - getEnvelopeId(envelope) { + } + getEnvelopeId(envelope: EnvelopeClass) { if (envelope.sourceUuid || envelope.source) { return `${envelope.sourceUuid || envelope.source}.${ envelope.sourceDevice @@ -411,38 +547,39 @@ MessageReceiver.prototype.extend({ } return envelope.id; - }, + } clearRetryTimeout() { if (this.retryCachedTimeout) { clearInterval(this.retryCachedTimeout); this.retryCachedTimeout = null; } - }, + } maybeScheduleRetryTimeout() { if (this.isEmptied) { this.clearRetryTimeout(); this.retryCachedTimeout = setTimeout(() => { - this.pendingQueue.add(() => this.queueAllCached()); + // tslint:disable-next-line no-floating-promises + this.pendingQueue.add(async () => this.queueAllCached()); }, RETRY_TIMEOUT); } - }, + } async getAllFromCache() { window.log.info('getAllFromCache'); - const count = await textsecure.storage.unprocessed.getCount(); + const count = await window.textsecure.storage.unprocessed.getCount(); if (count > 1500) { - await textsecure.storage.unprocessed.removeAll(); + await window.textsecure.storage.unprocessed.removeAll(); window.log.warn( `There were ${count} messages in cache. Deleted all instead of reprocessing` ); return []; } - const items = await textsecure.storage.unprocessed.getAll(); + const items = await window.textsecure.storage.unprocessed.getAll(); window.log.info('getAllFromCache loaded', items.length, 'saved envelopes'); return Promise.all( - _.map(items, async item => { + map(items, async item => { const attempts = 1 + (item.attempts || 0); try { @@ -451,9 +588,9 @@ MessageReceiver.prototype.extend({ 'getAllFromCache final attempt for envelope', item.id ); - await textsecure.storage.unprocessed.remove(item.id); + await window.textsecure.storage.unprocessed.remove(item.id); } else { - await textsecure.storage.unprocessed.updateAttempts( + await window.textsecure.storage.unprocessed.updateAttempts( item.id, attempts ); @@ -468,13 +605,14 @@ MessageReceiver.prototype.extend({ return item; }) ); - }, - async cacheAndQueueBatch(items) { + } + async cacheAndQueueBatch(items: Array) { const dataArray = items.map(item => item.data); try { - await textsecure.storage.unprocessed.batchAdd(dataArray); + await window.textsecure.storage.unprocessed.batchAdd(dataArray); items.forEach(item => { item.request.respond(200, 'OK'); + // tslint:disable-next-line no-floating-promises this.queueEnvelope(item.envelope); }); @@ -488,13 +626,17 @@ MessageReceiver.prototype.extend({ error && error.stack ? error.stack : error ); } - }, - cacheAndQueue(envelope, plaintext, request) { + } + cacheAndQueue( + envelope: EnvelopeClass, + plaintext: ArrayBuffer, + request: IncomingWebSocketRequest + ) { const { id } = envelope; const data = { id, version: 2, - envelope: MessageReceiver.arrayBufferToStringBase64(plaintext), + envelope: MessageReceiverInner.arrayBufferToStringBase64(plaintext), timestamp: Date.now(), attempts: 1, }; @@ -503,34 +645,37 @@ MessageReceiver.prototype.extend({ envelope, data, }); - }, - async cacheUpdateBatch(items) { - await textsecure.storage.unprocessed.addDecryptedDataToList(items); - }, - updateCache(envelope, plaintext) { + } + async cacheUpdateBatch(items: Array>) { + await window.textsecure.storage.unprocessed.addDecryptedDataToList(items); + } + updateCache(envelope: EnvelopeClass, plaintext: ArrayBuffer) { const { id } = envelope; const data = { source: envelope.source, sourceUuid: envelope.sourceUuid, sourceDevice: envelope.sourceDevice, serverTimestamp: envelope.serverTimestamp, - decrypted: MessageReceiver.arrayBufferToStringBase64(plaintext), + decrypted: MessageReceiverInner.arrayBufferToStringBase64(plaintext), }; this.cacheUpdateBatcher.add({ id, data }); - }, - async cacheRemoveBatch(items) { - await textsecure.storage.unprocessed.remove(items); - }, - removeFromCache(envelope) { + } + async cacheRemoveBatch(items: Array) { + await window.textsecure.storage.unprocessed.remove(items); + } + removeFromCache(envelope: EnvelopeClass) { const { id } = envelope; this.cacheRemoveBatcher.add(id); - }, - queueDecryptedEnvelope(envelope, plaintext) { + } + async queueDecryptedEnvelope( + envelope: EnvelopeClass, + plaintext: ArrayBuffer + ) { const id = this.getEnvelopeId(envelope); window.log.info('queueing decrypted envelope', id); const task = this.handleDecryptedEnvelope.bind(this, envelope, plaintext); - const taskWithTimeout = textsecure.createTaskWithTimeout( + const taskWithTimeout = window.textsecure.createTaskWithTimeout( task, `queueEncryptedEnvelope ${id}` ); @@ -542,13 +687,13 @@ MessageReceiver.prototype.extend({ error && error.stack ? error.stack : error ); }); - }, - queueEnvelope(envelope) { + } + async queueEnvelope(envelope: EnvelopeClass) { const id = this.getEnvelopeId(envelope); window.log.info('queueing envelope', id); const task = this.handleEnvelope.bind(this, envelope); - const taskWithTimeout = textsecure.createTaskWithTimeout( + const taskWithTimeout = window.textsecure.createTaskWithTimeout( task, `queueEnvelope ${id}` ); @@ -567,31 +712,39 @@ MessageReceiver.prototype.extend({ window.log.error(...args); } }); - }, + } // Same as handleEnvelope, just without the decryption step. Necessary for handling // messages which were successfully decrypted, but application logic didn't finish // processing. - handleDecryptedEnvelope(envelope, plaintext) { + async handleDecryptedEnvelope( + envelope: EnvelopeClass, + plaintext: ArrayBuffer + ): Promise { if (this.stoppingProcessing) { - return Promise.resolve(); + return; } // No decryption is required for delivery receipts, so the decrypted field of // the Unprocessed model will never be set if (envelope.content) { - return this.innerHandleContentMessage(envelope, plaintext); + await this.innerHandleContentMessage(envelope, plaintext); + + return; } else if (envelope.legacyMessage) { - return this.innerHandleLegacyMessage(envelope, plaintext); + await this.innerHandleLegacyMessage(envelope, plaintext); + + return; } + this.removeFromCache(envelope); throw new Error('Received message with no content and no legacyMessage'); - }, - handleEnvelope(envelope) { + } + async handleEnvelope(envelope: EnvelopeClass) { if (this.stoppingProcessing) { return Promise.resolve(); } - if (envelope.type === textsecure.protobuf.Envelope.Type.RECEIPT) { + if (envelope.type === window.textsecure.protobuf.Envelope.Type.RECEIPT) { return this.onDeliveryReceipt(envelope); } @@ -602,7 +755,7 @@ MessageReceiver.prototype.extend({ } this.removeFromCache(envelope); throw new Error('Received message with no content and no legacyMessage'); - }, + } getStatus() { if (this.socket) { return this.socket.readyState; @@ -610,8 +763,9 @@ MessageReceiver.prototype.extend({ return WebSocket.CLOSED; } return -1; - }, - onDeliveryReceipt(envelope) { + } + async onDeliveryReceipt(envelope: EnvelopeClass) { + // tslint:disable-next-line promise-must-complete return new Promise((resolve, reject) => { const ev = new Event('delivery'); ev.confirm = this.removeFromCache.bind(this, envelope); @@ -621,10 +775,10 @@ MessageReceiver.prototype.extend({ sourceUuid: envelope.sourceUuid, sourceDevice: envelope.sourceDevice, }; - this.dispatchAndWait(ev).then(resolve, reject); + this.dispatchAndWait(ev).then(resolve as any, reject as any); }); - }, - unpad(paddedData) { + } + unpad(paddedData: ArrayBuffer) { const paddedPlaintext = new Uint8Array(paddedData); let plaintext; @@ -640,21 +794,29 @@ MessageReceiver.prototype.extend({ } return plaintext; - }, - decrypt(envelope, ciphertext) { + } + + // tslint:disable-next-line max-func-body-length + async decrypt( + envelope: EnvelopeClass, + ciphertext: any + ): Promise { const { serverTrustRoot } = this; + let address: SignalProtocolAddressClass; let promise; - const address = new libsignal.SignalProtocolAddress( + const identifier = envelope.source || envelope.sourceUuid; + + address = new window.libsignal.SignalProtocolAddress( // Using source as opposed to sourceUuid allows us to get the existing // session if we haven't yet harvested the incoming uuid - envelope.source || envelope.sourceUuid, - envelope.sourceDevice + identifier as any, + envelope.sourceDevice as any ); - const ourNumber = textsecure.storage.user.getNumber(); - const ourUuid = textsecure.storage.user.getUuid(); - const options = {}; + const ourNumber = window.textsecure.storage.user.getNumber(); + const ourUuid = window.textsecure.storage.user.getUuid(); + const options: any = {}; // No limit on message keys if we're communicating with our other devices if ( @@ -664,29 +826,32 @@ MessageReceiver.prototype.extend({ options.messageKeysLimit = false; } - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, address, options ); const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( - textsecure.storage.protocol + window.textsecure.storage.protocol ); const me = { number: ourNumber, uuid: ourUuid, - deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10), + deviceId: parseInt( + window.textsecure.storage.user.getDeviceId() as string, + 10 + ), }; switch (envelope.type) { - case textsecure.protobuf.Envelope.Type.CIPHERTEXT: + case window.textsecure.protobuf.Envelope.Type.CIPHERTEXT: window.log.info('message from', this.getEnvelopeId(envelope)); promise = sessionCipher .decryptWhisperMessage(ciphertext) .then(this.unpad); break; - case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: + case window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: window.log.info('prekey message from', this.getEnvelopeId(envelope)); promise = this.decryptPreKeyWhisperMessage( ciphertext, @@ -694,7 +859,7 @@ MessageReceiver.prototype.extend({ address ); break; - case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: + case window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: window.log.info('received unidentified sender message'); promise = secretSessionCipher .decrypt( @@ -751,7 +916,7 @@ MessageReceiver.prototype.extend({ // decrypt methods used above. return this.unpad(content); }, - error => { + (error: Error) => { const { sender, senderUuid } = error || {}; if (sender || senderUuid) { @@ -800,7 +965,7 @@ MessageReceiver.prototype.extend({ } return promise - .then(plaintext => { + .then((plaintext: any) => { const { isMe, isBlocked } = plaintext || {}; if (isMe || isBlocked) { this.removeFromCache(envelope); @@ -813,14 +978,14 @@ MessageReceiver.prototype.extend({ return plaintext; }) - .catch(error => { + .catch(async error => { let errorToThrow = error; if (error && error.message === 'Unknown identity key') { // create an error that the UI will pick up and ask the // user if they want to re-negotiate - const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); - errorToThrow = new textsecure.IncomingIdentityKeyError( + const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext); + errorToThrow = new IncomingIdentityKeyError( address.toString(), buffer.toArrayBuffer(), error.identityKey @@ -831,11 +996,15 @@ MessageReceiver.prototype.extend({ ev.proto = envelope; ev.confirm = this.removeFromCache.bind(this, envelope); - const returnError = () => Promise.reject(errorToThrow); + const returnError = async () => Promise.reject(errorToThrow); return this.dispatchAndWait(ev).then(returnError, returnError); }); - }, - async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) { + } + async decryptPreKeyWhisperMessage( + ciphertext: ArrayBuffer, + sessionCipher: SessionCipherClass, + address: SignalProtocolAddressClass + ) { const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext); try { @@ -844,8 +1013,8 @@ MessageReceiver.prototype.extend({ if (e.message === 'Unknown identity key') { // create an error that the UI will pick up and ask the // user if they want to re-negotiate - const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); - throw new textsecure.IncomingIdentityKeyError( + const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext); + throw new IncomingIdentityKeyError( address.toString(), buffer.toArrayBuffer(), e.identityKey @@ -853,8 +1022,11 @@ MessageReceiver.prototype.extend({ } throw e; } - }, - handleSentMessage(envelope, sentContainer) { + } + async handleSentMessage( + envelope: EnvelopeClass, + sentContainer: SyncMessageClass.Sent + ) { const { destination, timestamp, @@ -864,24 +1036,37 @@ MessageReceiver.prototype.extend({ isRecipientUpdate, } = sentContainer; - let p = Promise.resolve(); + if (!msg) { + throw new Error('MessageReceiver.handleSentMessage: message was falsey!'); + } + + let p: Promise = Promise.resolve(); // eslint-disable-next-line no-bitwise - if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { + if ( + msg.flags && + msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION + ) { + if (!destination) { + throw new Error( + 'MessageReceiver.handleSentMessage: Cannot end session with falsey destination' + ); + } p = this.handleEndSession(destination); } - return p.then(() => + return p.then(async () => this.processDecrypted(envelope, msg).then(message => { const groupId = message.group && message.group.id; const isBlocked = this.isGroupBlocked(groupId); const { source, sourceUuid } = envelope; - const ourE164 = textsecure.storage.user.getNumber(); - const ourUuid = textsecure.storage.user.getUuid(); + 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.group && - message.group.type === textsecure.protobuf.GroupContext.Type.QUIT + message.group.type === + window.textsecure.protobuf.GroupContext.Type.QUIT ); if (groupId && isBlocked && !(isMe && isLeavingGroup)) { @@ -890,7 +1075,8 @@ MessageReceiver.prototype.extend({ envelope )} ignored; destined for blocked group` ); - return this.removeFromCache(envelope); + this.removeFromCache(envelope); + return; } const ev = new Event('sent'); @@ -909,27 +1095,38 @@ MessageReceiver.prototype.extend({ return this.dispatchAndWait(ev); }) ); - }, - handleDataMessage(envelope, msg) { + } + async handleDataMessage(envelope: EnvelopeClass, msg: DataMessageClass) { window.log.info('data message from', this.getEnvelopeId(envelope)); - let p = Promise.resolve(); + let p: Promise = Promise.resolve(); // eslint-disable-next-line no-bitwise - if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { - p = this.handleEndSession(envelope.source || envelope.sourceUuid); + const destination = envelope.source || envelope.sourceUuid; + if (!destination) { + throw new Error( + 'MessageReceiver.handleDataMessage: source and sourceUuid were falsey' + ); } - return p.then(() => + + if ( + msg.flags && + msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION + ) { + p = this.handleEndSession(destination); + } + return p.then(async () => this.processDecrypted(envelope, msg).then(message => { const groupId = message.group && message.group.id; const isBlocked = this.isGroupBlocked(groupId); const { source, sourceUuid } = envelope; - const ourE164 = textsecure.storage.user.getNumber(); - const ourUuid = textsecure.storage.user.getUuid(); + 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.group && - message.group.type === textsecure.protobuf.GroupContext.Type.QUIT + message.group.type === + window.textsecure.protobuf.GroupContext.Type.QUIT ); if (groupId && isBlocked && !(isMe && isLeavingGroup)) { @@ -938,7 +1135,8 @@ MessageReceiver.prototype.extend({ envelope )} ignored; destined for blocked group` ); - return this.removeFromCache(envelope); + this.removeFromCache(envelope); + return; } const ev = new Event('message'); @@ -948,15 +1146,14 @@ MessageReceiver.prototype.extend({ sourceUuid: envelope.sourceUuid, sourceDevice: envelope.sourceDevice, timestamp: envelope.timestamp.toNumber(), - receivedAt: envelope.receivedAt, unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived, message, }; return this.dispatchAndWait(ev); }) ); - }, - handleLegacyMessage(envelope) { + } + async handleLegacyMessage(envelope: EnvelopeClass) { return this.decrypt(envelope, envelope.legacyMessage).then(plaintext => { if (!plaintext) { window.log.warn('handleLegacyMessage: plaintext was falsey'); @@ -964,12 +1161,15 @@ MessageReceiver.prototype.extend({ } return this.innerHandleLegacyMessage(envelope, plaintext); }); - }, - innerHandleLegacyMessage(envelope, plaintext) { - const message = textsecure.protobuf.DataMessage.decode(plaintext); + } + async innerHandleLegacyMessage( + envelope: EnvelopeClass, + plaintext: ArrayBuffer + ) { + const message = window.textsecure.protobuf.DataMessage.decode(plaintext); return this.handleDataMessage(envelope, message); - }, - handleContentMessage(envelope) { + } + async handleContentMessage(envelope: EnvelopeClass) { return this.decrypt(envelope, envelope.content).then(plaintext => { if (!plaintext) { window.log.warn('handleContentMessage: plaintext was falsey'); @@ -977,17 +1177,22 @@ MessageReceiver.prototype.extend({ } return this.innerHandleContentMessage(envelope, plaintext); }); - }, - innerHandleContentMessage(envelope, plaintext) { - const content = textsecure.protobuf.Content.decode(plaintext); + } + async innerHandleContentMessage( + envelope: EnvelopeClass, + plaintext: ArrayBuffer + ) { + const content = window.textsecure.protobuf.Content.decode(plaintext); if (content.syncMessage) { return this.handleSyncMessage(envelope, content.syncMessage); } else if (content.dataMessage) { return this.handleDataMessage(envelope, content.dataMessage); } else if (content.nullMessage) { - return this.handleNullMessage(envelope, content.nullMessage); + this.handleNullMessage(envelope); + return; } else if (content.callMessage) { - return this.handleCallMessage(envelope, content.callMessage); + this.handleCallMessage(envelope); + return; } else if (content.receiptMessage) { return this.handleReceiptMessage(envelope, content.receiptMessage); } else if (content.typingMessage) { @@ -995,15 +1200,19 @@ MessageReceiver.prototype.extend({ } this.removeFromCache(envelope); throw new Error('Unsupported content message'); - }, - handleCallMessage(envelope) { + } + handleCallMessage(envelope: EnvelopeClass) { window.log.info('call message from', this.getEnvelopeId(envelope)); this.removeFromCache(envelope); - }, - handleReceiptMessage(envelope, receiptMessage) { + } + async handleReceiptMessage( + envelope: EnvelopeClass, + receiptMessage: ReceiptMessageClass + ) { const results = []; if ( - receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.DELIVERY + receiptMessage.type === + window.textsecure.protobuf.ReceiptMessage.Type.DELIVERY ) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { const ev = new Event('delivery'); @@ -1017,7 +1226,8 @@ MessageReceiver.prototype.extend({ results.push(this.dispatchAndWait(ev)); } } else if ( - receiptMessage.type === textsecure.protobuf.ReceiptMessage.Type.READ + receiptMessage.type === + window.textsecure.protobuf.ReceiptMessage.Type.READ ) { for (let i = 0; i < receiptMessage.timestamp.length; i += 1) { const ev = new Event('read'); @@ -1031,8 +1241,11 @@ MessageReceiver.prototype.extend({ } } return Promise.all(results); - }, - handleTypingMessage(envelope, typingMessage) { + } + handleTypingMessage( + envelope: EnvelopeClass, + typingMessage: TypingMessageClass + ) { const ev = new Event('typing'); this.removeFromCache(envelope); @@ -1062,19 +1275,24 @@ MessageReceiver.prototype.extend({ : null, started: typingMessage.action === - textsecure.protobuf.TypingMessage.Action.STARTED, + window.textsecure.protobuf.TypingMessage.Action.STARTED, stopped: typingMessage.action === - textsecure.protobuf.TypingMessage.Action.STOPPED, + window.textsecure.protobuf.TypingMessage.Action.STOPPED, }; return this.dispatchEvent(ev); - }, - handleNullMessage(envelope) { + } + handleNullMessage(envelope: EnvelopeClass) { window.log.info('null message from', this.getEnvelopeId(envelope)); this.removeFromCache(envelope); - }, - handleSyncMessage(envelope, syncMessage) { + } + + // tslint:disable-next-line cyclomatic-complexity + async handleSyncMessage( + envelope: EnvelopeClass, + syncMessage: SyncMessageClass + ) { const unidentified = syncMessage.sent ? syncMessage.sent.unidentifiedStatus || [] : []; @@ -1095,12 +1313,18 @@ MessageReceiver.prototype.extend({ if (!fromSelfSource && !fromSelfSourceUuid) { throw new Error('Received sync message from another number'); } - // eslint-disable-next-line eqeqeq + // tslint:disable-next-line triple-equals if (envelope.sourceDevice == this.deviceId) { throw new Error('Received sync message from our own device'); } if (syncMessage.sent) { const sentMessage = syncMessage.sent; + + if (!sentMessage || !sentMessage.message) { + throw new Error( + 'MessageReceiver.handleSyncMessage: sync sent message was missing message' + ); + } const to = sentMessage.message.group ? `group(${sentMessage.message.group.id.toBinary()})` : sentMessage.destination; @@ -1114,14 +1338,17 @@ MessageReceiver.prototype.extend({ ); return this.handleSentMessage(envelope, sentMessage); } else if (syncMessage.contacts) { - return this.handleContacts(envelope, syncMessage.contacts); + this.handleContacts(envelope, syncMessage.contacts); + return; } else if (syncMessage.groups) { - return this.handleGroups(envelope, syncMessage.groups); + this.handleGroups(envelope, syncMessage.groups); + return; } else if (syncMessage.blocked) { return this.handleBlocked(envelope, syncMessage.blocked); } else if (syncMessage.request) { window.log.info('Got SyncMessage Request'); - return this.removeFromCache(envelope); + this.removeFromCache(envelope); + return; } else if (syncMessage.read && syncMessage.read.length) { window.log.info('read messages from', this.getEnvelopeId(envelope)); return this.handleRead(envelope, syncMessage.read); @@ -1143,15 +1370,21 @@ MessageReceiver.prototype.extend({ this.removeFromCache(envelope); throw new Error('Got empty SyncMessage'); - }, - handleConfiguration(envelope, configuration) { + } + async handleConfiguration( + envelope: EnvelopeClass, + configuration: SyncMessageClass.Configuration + ) { window.log.info('got configuration sync message'); const ev = new Event('configuration'); ev.confirm = this.removeFromCache.bind(this, envelope); ev.configuration = configuration; return this.dispatchAndWait(ev); - }, - handleViewOnceOpen(envelope, sync) { + } + async handleViewOnceOpen( + envelope: EnvelopeClass, + sync: SyncMessageClass.ViewOnceOpen + ) { window.log.info('got view once open sync message'); const ev = new Event('viewSync'); @@ -1167,9 +1400,13 @@ MessageReceiver.prototype.extend({ ); return this.dispatchAndWait(ev); - }, - handleStickerPackOperation(envelope, operations) { - const ENUM = textsecure.protobuf.SyncMessage.StickerPackOperation.Type; + } + async handleStickerPackOperation( + envelope: EnvelopeClass, + operations: Array + ) { + const ENUM = + window.textsecure.protobuf.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); @@ -1180,8 +1417,8 @@ MessageReceiver.prototype.extend({ isRemove: operation.type === ENUM.REMOVE, })); return this.dispatchAndWait(ev); - }, - handleVerified(envelope, verified) { + } + async handleVerified(envelope: EnvelopeClass, verified: VerifiedClass) { const ev = new Event('verified'); ev.confirm = this.removeFromCache.bind(this, envelope); ev.verified = { @@ -1196,8 +1433,11 @@ MessageReceiver.prototype.extend({ 'message_receiver::handleVerified' ); return this.dispatchAndWait(ev); - }, - handleRead(envelope, read) { + } + async handleRead( + envelope: EnvelopeClass, + read: Array + ) { const results = []; for (let i = 0; i < read.length; i += 1) { const ev = new Event('readSync'); @@ -1216,48 +1456,57 @@ MessageReceiver.prototype.extend({ results.push(this.dispatchAndWait(ev)); } return Promise.all(results); - }, - handleContacts(envelope, contacts) { + } + handleContacts(envelope: EnvelopeClass, contacts: SyncMessageClass.Contacts) { window.log.info('contact sync'); const { blob } = contacts; + if (!blob) { + throw new Error('MessageReceiver.handleContacts: blob field was missing'); + } this.removeFromCache(envelope); // 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(attachmentPointer => { + // tslint:disable-next-line no-floating-promises + this.handleAttachment(blob).then(async attachmentPointer => { const results = []; const contactBuffer = new ContactBuffer(attachmentPointer.data); let contactDetails = contactBuffer.next(); while (contactDetails !== undefined) { - const ev = new Event('contact'); - ev.contactDetails = contactDetails; - results.push(this.dispatchAndWait(ev)); + const contactEvent = new Event('contact'); + contactEvent.contactDetails = contactDetails; + results.push(this.dispatchAndWait(contactEvent)); contactDetails = contactBuffer.next(); } - const ev = new Event('contactsync'); - results.push(this.dispatchAndWait(ev)); + const finalEvent = new Event('contactsync'); + results.push(this.dispatchAndWait(finalEvent)); return Promise.all(results).then(() => { window.log.info('handleContacts: finished'); }); }); - }, - handleGroups(envelope, groups) { + } + handleGroups(envelope: EnvelopeClass, groups: SyncMessageClass.Groups) { window.log.info('group sync'); const { blob } = groups; this.removeFromCache(envelope); + if (!blob) { + throw new Error('MessageReceiver.handleGroups: blob field was missing'); + } + // 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(attachmentPointer => { + // tslint:disable-next-line no-floating-promises + this.handleAttachment(blob).then(async attachmentPointer => { const groupBuffer = new GroupBuffer(attachmentPointer.data); - let groupDetails = groupBuffer.next(); + let groupDetails = groupBuffer.next() as any; const promises = []; - while (groupDetails !== undefined) { + while (groupDetails) { groupDetails.id = groupDetails.id.toBinary(); const ev = new Event('group'); ev.groupDetails = groupDetails; @@ -1268,52 +1517,58 @@ MessageReceiver.prototype.extend({ promises.push(promise); } - Promise.all(promises).then(() => { + return Promise.all(promises).then(async () => { const ev = new Event('groupsync'); return this.dispatchAndWait(ev); }); }); - }, - handleBlocked(envelope, blocked) { + } + async handleBlocked( + envelope: EnvelopeClass, + blocked: SyncMessageClass.Blocked + ) { window.log.info('Setting these numbers as blocked:', blocked.numbers); - textsecure.storage.put('blocked', blocked.numbers); + await window.textsecure.storage.put('blocked', blocked.numbers); if (blocked.uuids) { window.normalizeUuids( blocked, - blocked.uuids.map((_uuid, i) => `uuids.${i}`), + blocked.uuids.map((_uuid: string, i: number) => `uuids.${i}`), 'message_receiver::handleBlocked' ); window.log.info('Setting these uuids as blocked:', blocked.uuids); - textsecure.storage.put('blocked-uuids', blocked.uuids); + await window.textsecure.storage.put('blocked-uuids', blocked.uuids); } - const groupIds = _.map(blocked.groupIds, groupId => groupId.toBinary()); + const groupIds = map(blocked.groupIds, groupId => groupId.toBinary()); window.log.info( 'Setting these groups as blocked:', groupIds.map(groupId => `group(${groupId})`) ); - textsecure.storage.put('blocked-groups', groupIds); + await window.textsecure.storage.put('blocked-groups', groupIds); - return this.removeFromCache(envelope); - }, - isBlocked(number) { - return textsecure.storage.get('blocked', []).includes(number); - }, - isUuidBlocked(uuid) { - return textsecure.storage.get('blocked-uuids', []).includes(uuid); - }, - isGroupBlocked(groupId) { - return textsecure.storage.get('blocked-groups', []).includes(groupId); - }, - cleanAttachment(attachment) { + this.removeFromCache(envelope); + return; + } + isBlocked(number: string) { + return window.textsecure.storage.get('blocked', []).includes(number); + } + isUuidBlocked(uuid: string) { + return window.textsecure.storage.get('blocked-uuids', []).includes(uuid); + } + isGroupBlocked(groupId: string) { + return window.textsecure.storage + .get('blocked-groups', []) + .includes(groupId); + } + cleanAttachment(attachment: AttachmentPointerClass) { return { - ..._.omit(attachment, 'thumbnail'), + ...omit(attachment, 'thumbnail'), id: attachment.id.toString(), key: attachment.key ? attachment.key.toString('base64') : null, digest: attachment.digest ? attachment.digest.toString('base64') : null, }; - }, - async downloadAttachment(attachment) { + } + async downloadAttachment(attachment: AttachmentPointerClass) { const encrypted = await this.server.getAttachment(attachment.id); const { key, digest, size } = attachment; @@ -1321,13 +1576,13 @@ MessageReceiver.prototype.extend({ throw new Error('Failure: Ask sender to update Signal and resend.'); } - const paddedData = await textsecure.crypto.decryptAttachment( + const paddedData = await Crypto.decryptAttachment( encrypted, - window.Signal.Crypto.base64ToArrayBuffer(key), - window.Signal.Crypto.base64ToArrayBuffer(digest) + MessageReceiverInner.stringToArrayBufferBase64(key), + MessageReceiverInner.stringToArrayBufferBase64(digest) ); - if (!_.isNumber(size)) { + if (!isNumber(size)) { throw new Error( `downloadAttachment: Size was not provided, actual size was ${paddedData.byteLength}` ); @@ -1336,28 +1591,30 @@ MessageReceiver.prototype.extend({ const data = window.Signal.Crypto.getFirstBytes(paddedData, size); return { - ..._.omit(attachment, 'digest', 'key'), + ...omit(attachment, 'digest', 'key'), data, }; - }, - handleAttachment(attachment) { + } + async handleAttachment( + attachment: AttachmentPointerClass + ): Promise { const cleaned = this.cleanAttachment(attachment); return this.downloadAttachment(cleaned); - }, - async handleEndSession(identifier) { + } + async handleEndSession(identifier: string) { window.log.info('got end session'); - const deviceIds = await textsecure.storage.protocol.getDeviceIds( + const deviceIds = await window.textsecure.storage.protocol.getDeviceIds( identifier ); return Promise.all( - deviceIds.map(deviceId => { - const address = new libsignal.SignalProtocolAddress( + deviceIds.map(async deviceId => { + const address = new window.libsignal.SignalProtocolAddress( identifier, deviceId ); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, address ); @@ -1365,10 +1622,12 @@ MessageReceiver.prototype.extend({ return sessionCipher.deleteAllSessionsForDevice(); }) ); - }, - processDecrypted(envelope, decrypted) { + } + + // tslint:disable-next-line max-func-body-length cyclomatic-complexity + async processDecrypted(envelope: EnvelopeClass, decrypted: DataMessageClass) { /* eslint-disable no-bitwise, no-param-reassign */ - const FLAGS = textsecure.protobuf.DataMessage.Flags; + const FLAGS = window.textsecure.protobuf.DataMessage.Flags; // Now that its decrypted, validate the message and clean it up for consumer // processing @@ -1410,19 +1669,19 @@ MessageReceiver.prototype.extend({ throw new Error('Unknown flags in message'); } - if (decrypted.group !== null) { + if (decrypted.group) { decrypted.group.id = decrypted.group.id.toBinary(); switch (decrypted.group.type) { - case textsecure.protobuf.GroupContext.Type.UPDATE: + case window.textsecure.protobuf.GroupContext.Type.UPDATE: decrypted.body = null; decrypted.attachments = []; break; - case textsecure.protobuf.GroupContext.Type.QUIT: + case window.textsecure.protobuf.GroupContext.Type.QUIT: decrypted.body = null; decrypted.attachments = []; break; - case textsecure.protobuf.GroupContext.Type.DELIVER: + case window.textsecure.protobuf.GroupContext.Type.DELIVER: decrypted.group.name = null; decrypted.group.membersE164 = []; decrypted.group.members = []; @@ -1437,7 +1696,7 @@ MessageReceiver.prototype.extend({ } } - const attachmentCount = decrypted.attachments.length; + const attachmentCount = (decrypted.attachments || []).length; const ATTACHMENT_MAX = 32; if (attachmentCount > ATTACHMENT_MAX) { throw new Error( @@ -1449,9 +1708,10 @@ MessageReceiver.prototype.extend({ if ( decrypted.group && - decrypted.group.type === textsecure.protobuf.GroupContext.Type.UPDATE + decrypted.group.type === + window.textsecure.protobuf.GroupContext.Type.UPDATE ) { - if (decrypted.group.avatar !== null) { + if (decrypted.group.avatar) { decrypted.group.avatar = this.cleanAttachment(decrypted.group.avatar); } } @@ -1494,9 +1754,7 @@ MessageReceiver.prototype.extend({ if (decrypted.quote) { decrypted.quote.attachments = (decrypted.quote.attachments || []).map( item => { - const { thumbnail } = item; - - if (!thumbnail) { + if (!item.thumbnail) { return item; } @@ -1535,54 +1793,54 @@ MessageReceiver.prototype.extend({ return Promise.resolve(decrypted); /* eslint-enable no-bitwise, no-param-reassign */ - }, -}); + } +} -window.textsecure = window.textsecure || {}; +export default class MessageReceiver { + constructor( + oldUsername: string, + username: string, + password: string, + signalingKey: ArrayBuffer, + options: { + serverTrustRoot: string; + retryCached?: string; + } + ) { + const inner = new MessageReceiverInner( + oldUsername, + username, + password, + signalingKey, + options + ); -textsecure.MessageReceiver = function MessageReceiverWrapper( - username, - uuid, - password, - signalingKey, - options -) { - const messageReceiver = new MessageReceiver( - username, - uuid, - password, - signalingKey, - options - ); - this.addEventListener = messageReceiver.addEventListener.bind( - messageReceiver - ); - this.removeEventListener = messageReceiver.removeEventListener.bind( - messageReceiver - ); - this.getStatus = messageReceiver.getStatus.bind(messageReceiver); - this.close = messageReceiver.close.bind(messageReceiver); + this.addEventListener = inner.addEventListener.bind(inner); + this.removeEventListener = inner.removeEventListener.bind(inner); + this.getStatus = inner.getStatus.bind(inner); + this.close = inner.close.bind(inner); - this.downloadAttachment = messageReceiver.downloadAttachment.bind( - messageReceiver - ); - this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver); - this.unregisterBatchers = messageReceiver.unregisterBatchers.bind( - messageReceiver - ); + this.downloadAttachment = inner.downloadAttachment.bind(inner); + this.stopProcessing = inner.stopProcessing.bind(inner); + this.unregisterBatchers = inner.unregisterBatchers.bind(inner); - messageReceiver.connect(); -}; + inner.connect(); + } -textsecure.MessageReceiver.prototype = { - constructor: textsecure.MessageReceiver, -}; + addEventListener: (name: string, handler: Function) => void; + removeEventListener: (name: string, handler: Function) => void; + getStatus: () => number; + close: () => Promise; + downloadAttachment: ( + attachment: AttachmentPointerClass + ) => Promise; + stopProcessing: () => Promise; + unregisterBatchers: () => void; -textsecure.MessageReceiver.stringToArrayBuffer = - MessageReceiver.stringToArrayBuffer; -textsecure.MessageReceiver.arrayBufferToString = - MessageReceiver.arrayBufferToString; -textsecure.MessageReceiver.stringToArrayBufferBase64 = - MessageReceiver.stringToArrayBufferBase64; -textsecure.MessageReceiver.arrayBufferToStringBase64 = - MessageReceiver.arrayBufferToStringBase64; + static stringToArrayBuffer = MessageReceiverInner.stringToArrayBuffer; + static arrayBufferToString = MessageReceiverInner.arrayBufferToString; + static stringToArrayBufferBase64 = + MessageReceiverInner.stringToArrayBufferBase64; + static arrayBufferToStringBase64 = + MessageReceiverInner.arrayBufferToStringBase64; +} diff --git a/libtextsecure/outgoing_message.js b/ts/textsecure/OutgoingMessage.ts similarity index 57% rename from libtextsecure/outgoing_message.js rename to ts/textsecure/OutgoingMessage.ts index b0b5f5a985..9961d4e01c 100644 --- a/libtextsecure/outgoing_message.js +++ b/ts/textsecure/OutgoingMessage.ts @@ -1,45 +1,86 @@ -/* global textsecure, libsignal, window, btoa, _ */ +// tslint:disable no-default-export -/* eslint-disable more/no-then */ +import { reject } from 'lodash'; +import { ServerKeysType, WebAPIType } from './WebAPI'; +import { SignalProtocolAddressClass } from '../libsignal.d'; +import { ContentClass, DataMessageClass } from '../textsecure.d'; +import { + CallbackResultType, + SendMetadataType, + SendOptionsType, +} from './SendMessage'; +import { + OutgoingIdentityKeyError, + OutgoingMessageError, + SendMessageNetworkError, + UnregisteredUserError, +} from './Errors'; -function OutgoingMessage( - server, - timestamp, - identifiers, - message, - silent, - callback, - options = {} -) { - if (message instanceof textsecure.protobuf.DataMessage) { - const content = new textsecure.protobuf.Content(); - content.dataMessage = message; - // eslint-disable-next-line no-param-reassign - message = content; +type OutgoingMessageOptionsType = SendOptionsType & { + online?: boolean; +}; + +export default class OutgoingMessage { + server: WebAPIType; + timestamp: number; + identifiers: Array; + message: ContentClass; + callback: (result: CallbackResultType) => void; + silent?: boolean; + plaintext?: Uint8Array; + + identifiersCompleted: number; + errors: Array; + successfulIdentifiers: Array; + failoverIdentifiers: Array; + unidentifiedDeliveries: Array; + + sendMetadata?: SendMetadataType; + senderCertificate?: ArrayBuffer; + senderCertificateWithUuid?: ArrayBuffer; + online?: boolean; + + constructor( + server: WebAPIType, + timestamp: number, + identifiers: Array, + message: ContentClass | DataMessageClass, + silent: boolean | undefined, + callback: (result: CallbackResultType) => void, + options: OutgoingMessageOptionsType = {} + ) { + if (message instanceof window.textsecure.protobuf.DataMessage) { + const content = new window.textsecure.protobuf.Content(); + content.dataMessage = message; + // eslint-disable-next-line no-param-reassign + this.message = content; + } else { + this.message = message; + } + + this.server = server; + this.timestamp = timestamp; + this.identifiers = identifiers; + this.callback = callback; + this.silent = silent; + + this.identifiersCompleted = 0; + this.errors = []; + this.successfulIdentifiers = []; + this.failoverIdentifiers = []; + this.unidentifiedDeliveries = []; + + const { + sendMetadata, + senderCertificate, + senderCertificateWithUuid, + online, + } = options || ({} as any); + this.sendMetadata = sendMetadata; + this.senderCertificate = senderCertificate; + this.senderCertificateWithUuid = senderCertificateWithUuid; + this.online = online; } - this.server = server; - this.timestamp = timestamp; - this.identifiers = identifiers; - this.message = message; // ContentMessage proto - this.callback = callback; - this.silent = silent; - - this.identifiersCompleted = 0; - this.errors = []; - this.successfulIdentifiers = []; - this.failoverIdentifiers = []; - this.unidentifiedDeliveries = []; - - const { sendMetadata, senderCertificate, senderCertificateWithUuid, online } = - options || {}; - this.sendMetadata = sendMetadata; - this.senderCertificate = senderCertificate; - this.senderCertificateWithUuid = senderCertificateWithUuid; - this.online = online; -} - -OutgoingMessage.prototype = { - constructor: OutgoingMessage, numberCompleted() { this.identifiersCompleted += 1; if (this.identifiersCompleted >= this.identifiers.length) { @@ -50,11 +91,11 @@ OutgoingMessage.prototype = { unidentifiedDeliveries: this.unidentifiedDeliveries, }); } - }, - registerError(identifier, reason, error) { + } + registerError(identifier: string, reason: string, error?: Error) { if (!error || (error.name === 'HTTPError' && error.code !== 404)) { - // eslint-disable-next-line no-param-reassign - error = new textsecure.OutgoingMessageError( + // tslint:disable-next-line no-parameter-reassignment + error = new OutgoingMessageError( identifier, this.message.toArrayBuffer(), this.timestamp, @@ -66,50 +107,57 @@ OutgoingMessage.prototype = { error.reason = reason; this.errors[this.errors.length] = error; this.numberCompleted(); - }, - reloadDevicesAndSend(identifier, recurse) { - return () => - textsecure.storage.protocol.getDeviceIds(identifier).then(deviceIds => { - if (deviceIds.length === 0) { - return this.registerError( - identifier, - 'Got empty device list when loading device keys', - null - ); - } - return this.doSendMessage(identifier, deviceIds, recurse); - }); - }, + } + reloadDevicesAndSend( + identifier: string, + recurse?: boolean + ): () => Promise { + return async () => + window.textsecure.storage.protocol + .getDeviceIds(identifier) + .then(async deviceIds => { + if (deviceIds.length === 0) { + this.registerError( + identifier, + 'Got empty device list when loading device keys', + undefined + ); + return; + } + return this.doSendMessage(identifier, deviceIds, recurse); + }); + } - getKeysForIdentifier(identifier, updateDevices) { - const handleResult = response => + // tslint:disable-next-line max-func-body-length + async getKeysForIdentifier(identifier: string, updateDevices: Array) { + const handleResult = async (response: ServerKeysType) => Promise.all( - response.devices.map(device => { - // eslint-disable-next-line no-param-reassign - device.identityKey = response.identityKey; + response.devices.map(async device => { if ( updateDevices === undefined || updateDevices.indexOf(device.deviceId) > -1 ) { - const address = new libsignal.SignalProtocolAddress( + const address = new window.libsignal.SignalProtocolAddress( identifier, device.deviceId ); - const builder = new libsignal.SessionBuilder( - textsecure.storage.protocol, + const builder = new window.libsignal.SessionBuilder( + window.textsecure.storage.protocol, address ); if (device.registrationId === 0) { window.log.info('device registrationId 0!'); } - return builder.processPreKey(device).catch(error => { + + const deviceForProcess = { + ...device, + identityKey: response.identityKey, + }; + return builder.processPreKey(deviceForProcess).catch(error => { if (error.message === 'Identity key changed') { - // eslint-disable-next-line no-param-reassign error.timestamp = this.timestamp; - // eslint-disable-next-line no-param-reassign error.originalMessage = this.message.toArrayBuffer(); - // eslint-disable-next-line no-param-reassign - error.identityKey = device.identityKey; + error.identityKey = response.identityKey; } throw error; }); @@ -121,40 +169,40 @@ OutgoingMessage.prototype = { const { sendMetadata } = this; const info = - sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {}; - const { accessKey } = info || {}; + sendMetadata && sendMetadata[identifier] + ? sendMetadata[identifier] + : { accessKey: undefined }; + const { accessKey } = info; if (updateDevices === undefined) { if (accessKey) { return this.server - .getKeysForIdentifierUnauth(identifier, '*', { accessKey }) - .catch(error => { + .getKeysForIdentifierUnauth(identifier, undefined, { accessKey }) + .catch(async (error: Error) => { if (error.code === 401 || error.code === 403) { if (this.failoverIdentifiers.indexOf(identifier) === -1) { this.failoverIdentifiers.push(identifier); } - return this.server.getKeysForIdentifier(identifier, '*'); + return this.server.getKeysForIdentifier(identifier); } throw error; }) .then(handleResult); } - return this.server - .getKeysForIdentifier(identifier, '*') - .then(handleResult); + return this.server.getKeysForIdentifier(identifier).then(handleResult); } - let promise = Promise.resolve(); + let promise: Promise = Promise.resolve(); updateDevices.forEach(deviceId => { - promise = promise.then(() => { + promise = promise.then(async () => { let innerPromise; if (accessKey) { innerPromise = this.server .getKeysForIdentifierUnauth(identifier, deviceId, { accessKey }) .then(handleResult) - .catch(error => { + .catch(async error => { if (error.code === 401 || error.code === 403) { if (this.failoverIdentifiers.indexOf(identifier) === -1) { this.failoverIdentifiers.push(identifier); @@ -171,12 +219,12 @@ OutgoingMessage.prototype = { .then(handleResult); } - return innerPromise.catch(e => { + return innerPromise.catch(async e => { if (e.name === 'HTTPError' && e.code === 404) { if (deviceId !== 1) { return this.removeDeviceIdsForIdentifier(identifier, [deviceId]); } - throw new textsecure.UnregisteredUserError(identifier, e); + throw new UnregisteredUserError(identifier, e); } else { throw e; } @@ -185,9 +233,14 @@ OutgoingMessage.prototype = { }); return promise; - }, + } - transmitMessage(identifier, jsonData, timestamp, { accessKey } = {}) { + async transmitMessage( + identifier: string, + jsonData: Array, + timestamp: number, + { accessKey }: { accessKey?: string } = {} + ) { let promise; if (accessKey) { @@ -215,20 +268,15 @@ OutgoingMessage.prototype = { // 404 should throw UnregisteredUserError // all other network errors can be retried later. if (e.code === 404) { - throw new textsecure.UnregisteredUserError(identifier, e); + throw new UnregisteredUserError(identifier, e); } - throw new textsecure.SendMessageNetworkError( - identifier, - jsonData, - e, - timestamp - ); + throw new SendMessageNetworkError(identifier, jsonData, e); } throw e; }); - }, + } - getPaddedMessageLength(messageLength) { + getPaddedMessageLength(messageLength: number) { const messageLengthWithTerminator = messageLength + 1; let messagePartCount = Math.floor(messageLengthWithTerminator / 160); @@ -237,7 +285,7 @@ OutgoingMessage.prototype = { } return messagePartCount * 160; - }, + } getPlaintext() { if (!this.plaintext) { @@ -249,16 +297,29 @@ OutgoingMessage.prototype = { this.plaintext[messageBuffer.byteLength] = 0x80; } return this.plaintext; - }, + } - doSendMessage(identifier, deviceIds, recurse) { - const ciphers = {}; + // tslint:disable-next-line max-func-body-length + async doSendMessage( + identifier: string, + deviceIds: Array, + recurse?: boolean + ): Promise { + const ciphers: { + [key: number]: { + closeOpenSessionForDevice: ( + address: SignalProtocolAddressClass + ) => Promise; + }; + } = {}; const plaintext = this.getPlaintext(); const { sendMetadata } = this; const info = - sendMetadata && sendMetadata[identifier] ? sendMetadata[identifier] : {}; - const { accessKey, useUuidSenderCert } = info || {}; + sendMetadata && sendMetadata[identifier] + ? sendMetadata[identifier] + : { accessKey: undefined, useUuidSenderCert: undefined }; + const { accessKey, useUuidSenderCert } = info; const senderCertificate = useUuidSenderCert ? this.senderCertificateWithUuid : this.senderCertificate; @@ -272,27 +333,29 @@ OutgoingMessage.prototype = { const sealedSender = Boolean(accessKey && senderCertificate); // We don't send to ourselves if unless sealedSender is enabled - const ourNumber = textsecure.storage.user.getNumber(); - const ourUuid = textsecure.storage.user.getUuid(); - const ourDeviceId = textsecure.storage.user.getDeviceId(); + const ourNumber = window.textsecure.storage.user.getNumber(); + const ourUuid = window.textsecure.storage.user.getUuid(); + const ourDeviceId = window.textsecure.storage.user.getDeviceId(); if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) { - // eslint-disable-next-line no-param-reassign - deviceIds = _.reject( + // tslint:disable-next-line no-parameter-reassignment + deviceIds = reject( deviceIds, deviceId => // because we store our own device ID as a string at least sometimes - deviceId === ourDeviceId || deviceId === parseInt(ourDeviceId, 10) + deviceId === ourDeviceId || + (typeof ourDeviceId === 'string' && + deviceId === parseInt(ourDeviceId, 10)) ); } return Promise.all( deviceIds.map(async deviceId => { - const address = new libsignal.SignalProtocolAddress( + const address = new window.libsignal.SignalProtocolAddress( identifier, deviceId ); - const options = {}; + const options: any = {}; // No limit on message keys if we're communicating with our other devices if (ourNumber === identifier || ourUuid === identifier) { @@ -301,7 +364,7 @@ OutgoingMessage.prototype = { if (sealedSender) { const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( - textsecure.storage.protocol + window.textsecure.storage.protocol ); ciphers[address.getDeviceId()] = secretSessionCipher; @@ -312,32 +375,32 @@ OutgoingMessage.prototype = { ); return { - type: textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER, + type: window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER, destinationDeviceId: address.getDeviceId(), destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId( address ), content: window.Signal.Crypto.arrayBufferToBase64(ciphertext), }; + } else { + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, + address, + options + ); + ciphers[address.getDeviceId()] = sessionCipher; + + const ciphertext = await sessionCipher.encrypt(plaintext); + return { + type: ciphertext.type, + destinationDeviceId: address.getDeviceId(), + destinationRegistrationId: ciphertext.registrationId, + content: btoa(ciphertext.body), + }; } - - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address, - options - ); - ciphers[address.getDeviceId()] = sessionCipher; - - const ciphertext = await sessionCipher.encrypt(plaintext); - return { - type: ciphertext.type, - destinationDeviceId: address.getDeviceId(), - destinationRegistrationId: ciphertext.registrationId, - content: btoa(ciphertext.body), - }; }) ) - .then(jsonData => { + .then(async jsonData => { if (sealedSender) { return this.transmitMessage(identifier, jsonData, this.timestamp, { accessKey, @@ -347,18 +410,18 @@ OutgoingMessage.prototype = { this.successfulIdentifiers.push(identifier); this.numberCompleted(); }, - error => { + async (error: Error) => { if (error.code === 401 || error.code === 403) { if (this.failoverIdentifiers.indexOf(identifier) === -1) { this.failoverIdentifiers.push(identifier); } + + // This ensures that we don't hit this codepath the next time through if (info) { - info.accessKey = null; + info.accessKey = undefined; } - // Set final parameter to true to ensure we don't hit this codepath a - // second time. - return this.doSendMessage(identifier, deviceIds, recurse, true); + return this.doSendMessage(identifier, deviceIds, recurse); } throw error; @@ -373,20 +436,22 @@ OutgoingMessage.prototype = { } ); }) - .catch(error => { + .catch(async error => { if ( error instanceof Error && error.name === 'HTTPError' && (error.code === 410 || error.code === 409) ) { - if (!recurse) - return this.registerError( + if (!recurse) { + this.registerError( identifier, 'Hit retry limit attempting to reload device list', error ); + return; + } - let p; + let p: Promise = Promise.resolve(); if (error.code === 409) { p = this.removeDeviceIdsForIdentifier( identifier, @@ -394,15 +459,18 @@ OutgoingMessage.prototype = { ); } else { p = Promise.all( - error.response.staleDevices.map(deviceId => + error.response.staleDevices.map(async (deviceId: number) => ciphers[deviceId].closeOpenSessionForDevice( - new libsignal.SignalProtocolAddress(identifier, deviceId) + new window.libsignal.SignalProtocolAddress( + identifier, + deviceId + ) ) ) ); } - return p.then(() => { + return p.then(async () => { const resetDevices = error.code === 410 ? error.response.staleDevices @@ -425,10 +493,13 @@ OutgoingMessage.prototype = { ); window.log.info('closing all sessions for', identifier); - const address = new libsignal.SignalProtocolAddress(identifier, 1); + const address = new window.libsignal.SignalProtocolAddress( + identifier, + 1 + ); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, address ); window.log.info('closing session for', address.toString()); @@ -436,7 +507,7 @@ OutgoingMessage.prototype = { // Primary device sessionCipher.closeOpenSessionForDevice(), // The rest of their devices - textsecure.storage.protocol.archiveSiblingSessions( + window.textsecure.storage.protocol.archiveSiblingSessions( address.toString() ), ]).then( @@ -457,26 +528,25 @@ OutgoingMessage.prototype = { 'Failed to create or send message', error ); - return null; }); - }, + } - getStaleDeviceIdsForIdentifier(identifier) { - return textsecure.storage.protocol + async getStaleDeviceIdsForIdentifier(identifier: string) { + return window.textsecure.storage.protocol .getDeviceIds(identifier) - .then(deviceIds => { + .then(async deviceIds => { if (deviceIds.length === 0) { return [1]; } - const updateDevices = []; + const updateDevices: Array = []; return Promise.all( - deviceIds.map(deviceId => { - const address = new libsignal.SignalProtocolAddress( + deviceIds.map(async deviceId => { + const address = new window.libsignal.SignalProtocolAddress( identifier, deviceId ); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, address ); return sessionCipher.hasOpenSession().then(hasSession => { @@ -487,21 +557,24 @@ OutgoingMessage.prototype = { }) ).then(() => updateDevices); }); - }, + } - removeDeviceIdsForIdentifier(identifier, deviceIdsToRemove) { + async removeDeviceIdsForIdentifier( + identifier: string, + deviceIdsToRemove: Array + ) { let promise = Promise.resolve(); - // eslint-disable-next-line no-restricted-syntax, guard-for-in + // tslint:disable-next-line forin no-for-in no-for-in-array for (const j in deviceIdsToRemove) { - promise = promise.then(() => { + promise = promise.then(async () => { const encodedAddress = `${identifier}.${deviceIdsToRemove[j]}`; - return textsecure.storage.protocol.removeSession(encodedAddress); + return window.textsecure.storage.protocol.removeSession(encodedAddress); }); } return promise; - }, + } - async sendToIdentifier(identifier) { + async sendToIdentifier(identifier: string) { try { const updateDevices = await this.getStaleDeviceIdsForIdentifier( identifier @@ -510,8 +583,7 @@ OutgoingMessage.prototype = { await this.reloadDevicesAndSend(identifier, true)(); } catch (error) { if (error.message === 'Identity key changed') { - // eslint-disable-next-line no-param-reassign - const newError = new textsecure.OutgoingIdentityKeyError( + const newError = new OutgoingIdentityKeyError( identifier, error.originalMessage, error.timestamp, @@ -526,5 +598,5 @@ OutgoingMessage.prototype = { ); } } - }, -}; + } +} diff --git a/ts/textsecure/ProvisioningCipher.ts b/ts/textsecure/ProvisioningCipher.ts new file mode 100644 index 0000000000..1594d2596f --- /dev/null +++ b/ts/textsecure/ProvisioningCipher.ts @@ -0,0 +1,111 @@ +// tslint:disable no-default-export + +import { KeyPairType } from '../libsignal.d'; +import { ProvisionEnvelopeClass } from '../textsecure.d'; + +type ProvisionDecryptResult = { + identityKeyPair: KeyPairType; + number?: string; + uuid?: string; + provisioningCode?: string; + userAgent?: string; + readReceipts?: boolean; + profileKey?: ArrayBuffer; +}; + +class ProvisioningCipherInner { + keyPair?: KeyPairType; + + async decrypt( + provisionEnvelope: ProvisionEnvelopeClass + ): Promise { + const masterEphemeral = provisionEnvelope.publicKey.toArrayBuffer(); + const message = provisionEnvelope.body.toArrayBuffer(); + if (new Uint8Array(message)[0] !== 1) { + throw new Error('Bad version number on ProvisioningMessage'); + } + + const iv = message.slice(1, 16 + 1); + const mac = message.slice(message.byteLength - 32, message.byteLength); + const ivAndCiphertext = message.slice(0, message.byteLength - 32); + const ciphertext = message.slice(16 + 1, message.byteLength - 32); + + if (!this.keyPair) { + throw new Error('ProvisioningCipher.decrypt: No keypair!'); + } + + return window.libsignal.Curve.async + .calculateAgreement(masterEphemeral, this.keyPair.privKey) + .then(async ecRes => + window.libsignal.HKDF.deriveSecrets( + ecRes, + new ArrayBuffer(32), + 'TextSecure Provisioning Message' + ) + ) + .then(async keys => + window.libsignal.crypto + .verifyMAC(ivAndCiphertext, keys[1], mac, 32) + .then(async () => + window.libsignal.crypto.decrypt(keys[0], ciphertext, iv) + ) + ) + .then(async plaintext => { + const provisionMessage = window.textsecure.protobuf.ProvisionMessage.decode( + plaintext + ); + const privKey = provisionMessage.identityKeyPrivate.toArrayBuffer(); + + return window.libsignal.Curve.async + .createKeyPair(privKey) + .then(keyPair => { + const ret: ProvisionDecryptResult = { + identityKeyPair: keyPair, + number: provisionMessage.number, + provisioningCode: provisionMessage.provisioningCode, + userAgent: provisionMessage.userAgent, + readReceipts: provisionMessage.readReceipts, + }; + if (provisionMessage.profileKey) { + ret.profileKey = provisionMessage.profileKey.toArrayBuffer(); + } + return ret; + }); + }); + } + async getPublicKey(): Promise { + return Promise.resolve() + .then(async () => { + if (!this.keyPair) { + return window.libsignal.Curve.async + .generateKeyPair() + .then(keyPair => { + this.keyPair = keyPair; + }); + } + + return null; + }) + .then(() => { + if (!this.keyPair) { + throw new Error('ProvisioningCipher.decrypt: No keypair!'); + } + + return this.keyPair.pubKey; + }); + } +} + +export default class ProvisioningCipher { + constructor() { + const inner = new ProvisioningCipherInner(); + + this.decrypt = inner.decrypt.bind(inner); + this.getPublicKey = inner.getPublicKey.bind(inner); + } + + decrypt: ( + provisionEnvelope: ProvisionEnvelopeClass + ) => Promise; + getPublicKey: () => Promise; +} diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts new file mode 100644 index 0000000000..1a2e55cdfc --- /dev/null +++ b/ts/textsecure/SendMessage.ts @@ -0,0 +1,1583 @@ +// tslint:disable no-backbone-get-set-outside-model no-bitwise no-default-export + +import { without } from 'lodash'; +import PQueue from 'p-queue'; + +import { ProxiedRequestOptionsType, WebAPIType } from './WebAPI'; +import createTaskWithTimeout from './TaskWithTimeout'; +import OutgoingMessage from './OutgoingMessage'; +import Crypto from './Crypto'; +import { + AttachmentPointerClass, + ContentClass, + DataMessageClass, +} from '../textsecure.d'; +import { MessageError, SignedPreKeyRotationError } from './Errors'; + +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; +} +function hexStringToArrayBuffer(string: string): ArrayBuffer { + return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer(); +} +function base64ToArrayBuffer(string: string): ArrayBuffer { + return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer(); +} + +export type SendMetadataType = { + [identifier: string]: { + accessKey: string; + useUuidSenderCert: boolean; + }; +}; + +type GroupMemberType = { + uuid?: string; + e164?: string; +}; + +export type SendOptionsType = { + senderCertificate?: ArrayBuffer; + senderCertificateWithUuid?: ArrayBuffer; + sendMetadata?: SendMetadataType; + online?: boolean; +}; + +export type CallbackResultType = { + successfulIdentifiers?: Array; + failoverIdentifiers?: Array; + errors?: Array; + unidentifiedDeliveries?: Array; + dataMessage?: ArrayBuffer; +}; + +type PreviewType = { + url: string; + title: string; + image: AttachmentType; +}; + +type QuoteAttachmentType = { + thumbnail?: AttachmentType; + attachmentPointer?: AttachmentPointerClass; +}; + +type MessageOptionsType = { + attachments?: Array | null; + body?: string; + expireTimer?: number; + flags?: number; + group?: { + id: string; + type: number; + }; + needsSync?: boolean; + preview?: Array | null; + profileKey?: string; + quote?: any; + recipients: Array; + sticker?: any; + reaction?: any; + timestamp: number; +}; + +class Message { + attachments: Array; + body?: string; + expireTimer?: number; + flags?: number; + group?: { + id: string; + type: number; + }; + needsSync?: boolean; + preview: any; + profileKey?: string; + quote?: any; + recipients: Array; + sticker?: any; + reaction?: any; + timestamp: number; + + dataMessage: any; + attachmentPointers?: Array; + + // tslint:disable cyclomatic-complexity + constructor(options: MessageOptionsType) { + this.attachments = options.attachments || []; + this.body = options.body; + this.expireTimer = options.expireTimer; + this.flags = options.flags; + this.group = options.group; + this.needsSync = options.needsSync; + this.preview = options.preview; + this.profileKey = options.profileKey; + this.quote = options.quote; + this.recipients = options.recipients; + this.sticker = options.sticker; + this.reaction = options.reaction; + this.timestamp = options.timestamp; + + if (!(this.recipients instanceof Array)) { + throw new Error('Invalid recipient list'); + } + + if (!this.group && this.recipients.length !== 1) { + throw new Error('Invalid recipient list for non-group'); + } + + if (typeof this.timestamp !== 'number') { + throw new Error('Invalid timestamp'); + } + + if (this.expireTimer !== undefined && this.expireTimer !== null) { + if (typeof this.expireTimer !== 'number' || !(this.expireTimer >= 0)) { + throw new Error('Invalid expireTimer'); + } + } + + if (this.attachments) { + if (!(this.attachments instanceof Array)) { + throw new Error('Invalid message attachments'); + } + } + if (this.flags !== undefined) { + if (typeof this.flags !== 'number') { + throw new Error('Invalid message flags'); + } + } + if (this.isEndSession()) { + if ( + this.body !== null || + this.group !== null || + this.attachments.length !== 0 + ) { + throw new Error('Invalid end session message'); + } + } else { + if ( + typeof this.timestamp !== 'number' || + (this.body && typeof this.body !== 'string') + ) { + throw new Error('Invalid message body'); + } + if (this.group) { + if ( + typeof this.group.id !== 'string' || + typeof this.group.type !== 'number' + ) { + throw new Error('Invalid group context'); + } + } + } + } + + isEndSession() { + return ( + (this.flags || 0) & + window.textsecure.protobuf.DataMessage.Flags.END_SESSION + ); + } + + toProto(): DataMessageClass { + if (this.dataMessage instanceof window.textsecure.protobuf.DataMessage) { + return this.dataMessage; + } + const proto = new window.textsecure.protobuf.DataMessage(); + + proto.timestamp = this.timestamp; + proto.attachments = this.attachmentPointers; + + if (this.body) { + proto.body = this.body; + } + if (this.flags) { + proto.flags = this.flags; + } + if (this.group) { + proto.group = new window.textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(this.group.id); + proto.group.type = this.group.type; + } + if (this.sticker) { + proto.sticker = new window.textsecure.protobuf.DataMessage.Sticker(); + proto.sticker.packId = hexStringToArrayBuffer(this.sticker.packId); + proto.sticker.packKey = base64ToArrayBuffer(this.sticker.packKey); + proto.sticker.stickerId = this.sticker.stickerId; + + if (this.sticker.attachmentPointer) { + proto.sticker.data = this.sticker.attachmentPointer; + } + } + if (this.reaction) { + proto.reaction = this.reaction; + } + if (Array.isArray(this.preview)) { + proto.preview = this.preview.map(preview => { + const item = new window.textsecure.protobuf.DataMessage.Preview(); + item.title = preview.title; + item.url = preview.url; + item.image = preview.image || null; + return item; + }); + } + if (this.quote) { + const { QuotedAttachment } = window.textsecure.protobuf.DataMessage.Quote; + const { Quote } = window.textsecure.protobuf.DataMessage; + + proto.quote = new Quote(); + const { quote } = proto; + + quote.id = this.quote.id; + quote.author = this.quote.author; + quote.text = this.quote.text; + quote.attachments = (this.quote.attachments || []).map( + (attachment: AttachmentType) => { + const quotedAttachment = new QuotedAttachment(); + + quotedAttachment.contentType = attachment.contentType; + quotedAttachment.fileName = attachment.fileName; + if (attachment.attachmentPointer) { + quotedAttachment.thumbnail = attachment.attachmentPointer; + } + + return quotedAttachment; + } + ); + } + if (this.expireTimer) { + proto.expireTimer = this.expireTimer; + } + if (this.profileKey) { + proto.profileKey = this.profileKey; + } + + this.dataMessage = proto; + return proto; + } + + toArrayBuffer() { + return this.toProto().toArrayBuffer(); + } +} + +export type AttachmentType = { + size: number; + data: ArrayBuffer; + contentType: string; + + fileName: string; + flags: number; + width: number; + height: number; + caption: string; + + attachmentPointer?: AttachmentPointerClass; +}; + +export default class MessageSender { + server: WebAPIType; + pendingMessages: { + [id: string]: PQueue; + }; + + constructor(username: string, password: string) { + this.server = window.WebAPI.connect({ username, password }); + this.pendingMessages = {}; + } + + _getAttachmentSizeBucket(size: number) { + return Math.max( + 541, + Math.floor(1.05 ** Math.ceil(Math.log(size) / Math.log(1.05))) + ); + } + + getPaddedAttachment(data: ArrayBuffer) { + const size = data.byteLength; + const paddedSize = this._getAttachmentSizeBucket(size); + const padding = window.Signal.Crypto.getZeroes(paddedSize - size); + + return window.Signal.Crypto.concatenateBytes(data, padding); + } + + async makeAttachmentPointer(attachment: AttachmentType) { + if (typeof attachment !== 'object' || attachment == null) { + return Promise.resolve(undefined); + } + + const { data, size } = attachment; + if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) { + throw new Error( + `makeAttachmentPointer: data was a '${typeof data}' instead of ArrayBuffer/ArrayBufferView` + ); + } + if (data.byteLength !== size) { + throw new Error( + `makeAttachmentPointer: Size ${size} did not match data.byteLength ${data.byteLength}` + ); + } + + const padded = this.getPaddedAttachment(data); + const key = window.libsignal.crypto.getRandomBytes(64); + const iv = window.libsignal.crypto.getRandomBytes(16); + + const result = await Crypto.encryptAttachment(padded, key, iv); + const id = await this.server.putAttachment(result.ciphertext); + + const proto = new window.textsecure.protobuf.AttachmentPointer(); + proto.id = id; + proto.contentType = attachment.contentType; + proto.key = key; + proto.size = attachment.size; + proto.digest = result.digest; + + if (attachment.fileName) { + proto.fileName = attachment.fileName; + } + if (attachment.flags) { + proto.flags = attachment.flags; + } + if (attachment.width) { + proto.width = attachment.width; + } + if (attachment.height) { + proto.height = attachment.height; + } + if (attachment.caption) { + proto.caption = attachment.caption; + } + + return proto; + } + + async queueJobForIdentifier(identifier: string, runJob: () => Promise) { + const { id } = await window.ConversationController.getOrCreateAndWait( + identifier, + 'private' + ); + this.pendingMessages[id] = + this.pendingMessages[id] || new PQueue({ concurrency: 1 }); + + const queue = this.pendingMessages[id]; + + const taskWithTimeout = createTaskWithTimeout( + runJob, + `queueJobForIdentifier ${identifier} ${id}` + ); + + return queue.add(taskWithTimeout); + } + + async uploadAttachments(message: Message) { + return Promise.all( + message.attachments.map(this.makeAttachmentPointer.bind(this)) + ) + .then(attachmentPointers => { + // eslint-disable-next-line no-param-reassign + message.attachmentPointers = attachmentPointers; + }) + .catch(error => { + if (error instanceof Error && error.name === 'HTTPError') { + throw new MessageError(message, error); + } else { + throw error; + } + }); + } + + async uploadLinkPreviews(message: Message) { + try { + const preview = await Promise.all( + (message.preview || []).map(async (item: PreviewType) => ({ + ...item, + image: await this.makeAttachmentPointer(item.image), + })) + ); + // eslint-disable-next-line no-param-reassign + message.preview = preview; + } catch (error) { + if (error instanceof Error && error.name === 'HTTPError') { + throw new MessageError(message, error); + } else { + throw error; + } + } + } + + async uploadSticker(message: Message) { + try { + const { sticker } = message; + + if (!sticker || !sticker.data) { + return; + } + + // eslint-disable-next-line no-param-reassign + message.sticker = { + ...sticker, + attachmentPointer: await this.makeAttachmentPointer(sticker.data), + }; + } catch (error) { + if (error instanceof Error && error.name === 'HTTPError') { + throw new MessageError(message, error); + } else { + throw error; + } + } + } + + async uploadThumbnails(message: Message): Promise { + const makePointer = this.makeAttachmentPointer.bind(this); + const { quote } = message; + + if (!quote || !quote.attachments || quote.attachments.length === 0) { + return Promise.resolve(); + } + + await Promise.all( + quote.attachments.map((attachment: QuoteAttachmentType) => { + if (!attachment.thumbnail) { + return null; + } + + return makePointer(attachment.thumbnail).then(pointer => { + // eslint-disable-next-line no-param-reassign + attachment.attachmentPointer = pointer; + }); + }) + ).catch(error => { + if (error instanceof Error && error.name === 'HTTPError') { + throw new MessageError(message, error); + } else { + throw error; + } + }); + } + + async sendMessage(attrs: MessageOptionsType, options?: SendOptionsType) { + const message = new Message(attrs); + const silent = false; + + return Promise.all([ + this.uploadAttachments(message), + this.uploadThumbnails(message), + this.uploadLinkPreviews(message), + this.uploadSticker(message), + ]).then( + async () => + new Promise((resolve, reject) => { + this.sendMessageProto( + message.timestamp, + message.recipients || [], + message.toProto(), + (res: CallbackResultType) => { + res.dataMessage = message.toArrayBuffer(); + if (res.errors && res.errors.length > 0) { + reject(res); + } else { + resolve(res); + } + }, + silent, + options + ); + }) + ); + } + sendMessageProto( + timestamp: number, + recipients: Array, + messageProto: DataMessageClass, + callback: (result: CallbackResultType) => void, + silent?: boolean, + options?: SendOptionsType + ) { + const rejections = window.textsecure.storage.get( + 'signedKeyRotationRejected', + 0 + ); + if (rejections > 5) { + throw new SignedPreKeyRotationError(); + } + + const outgoing = new OutgoingMessage( + this.server, + timestamp, + recipients, + messageProto, + silent, + callback, + options + ); + + recipients.forEach(identifier => { + // tslint:disable-next-line no-floating-promises + this.queueJobForIdentifier(identifier, async () => + outgoing.sendToIdentifier(identifier) + ); + }); + } + + async sendMessageProtoAndWait( + timestamp: number, + identifiers: Array, + messageProto: DataMessageClass, + silent?: boolean, + options?: SendOptionsType + ) { + return new Promise((resolve, reject) => { + const callback = (result: CallbackResultType) => { + if (result && result.errors && result.errors.length > 0) { + reject(result); + return; + } + + resolve(result); + return; + }; + + this.sendMessageProto( + timestamp, + identifiers, + messageProto, + callback, + silent, + options + ); + }); + } + + async sendIndividualProto( + identifier: string, + proto: DataMessageClass | ContentClass, + timestamp: number, + silent?: boolean, + options?: SendOptionsType + ) { + return new Promise((resolve, reject) => { + const callback = (res: CallbackResultType) => { + if (res && res.errors && res.errors.length > 0) { + reject(res); + } else { + resolve(res); + } + }; + this.sendMessageProto( + timestamp, + [identifier], + proto, + callback, + silent, + options + ); + }); + } + + createSyncMessage() { + const syncMessage = new window.textsecure.protobuf.SyncMessage(); + + // Generate a random int from 1 and 512 + const buffer = window.libsignal.crypto.getRandomBytes(1); + const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; + + // Generate a random padding buffer of the chosen size + syncMessage.padding = window.libsignal.crypto.getRandomBytes(paddingLength); + + return syncMessage; + } + + async sendSyncMessage( + encodedDataMessage: ArrayBuffer, + timestamp: number, + destination: string, + destinationUuid: string | null, + expirationStartTimestamp: number | null, + sentTo: Array = [], + unidentifiedDeliveries: Array = [], + isUpdate: boolean = false, + options?: SendOptionsType + ) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + + if (myDevice === 1 || myDevice === '1') { + return Promise.resolve(); + } + + const dataMessage = window.textsecure.protobuf.DataMessage.decode( + encodedDataMessage + ); + const sentMessage = new window.textsecure.protobuf.SyncMessage.Sent(); + sentMessage.timestamp = timestamp; + sentMessage.message = dataMessage; + if (destination) { + sentMessage.destination = destination; + } + if (destinationUuid) { + sentMessage.destinationUuid = destinationUuid; + } + if (expirationStartTimestamp) { + sentMessage.expirationStartTimestamp = expirationStartTimestamp; + } + + const unidentifiedLookup = unidentifiedDeliveries.reduce( + (accumulator, item) => { + // eslint-disable-next-line no-param-reassign + accumulator[item] = true; + return accumulator; + }, + Object.create(null) + ); + + if (isUpdate) { + sentMessage.isRecipientUpdate = true; + } + + // Though this field has 'unidenified' in the name, it should have entries for each + // number we sent to. + if (sentTo && sentTo.length) { + sentMessage.unidentifiedStatus = sentTo.map(identifier => { + const status = new window.textsecure.protobuf.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'); + } + status.unidentified = Boolean(unidentifiedLookup[identifier]); + return status; + }); + } + + const syncMessage = this.createSyncMessage(); + syncMessage.sent = sentMessage; + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + timestamp, + silent, + options + ); + } + + async getProfile(number: string, { accessKey }: { accessKey?: string } = {}) { + if (accessKey) { + return this.server.getProfileUnauth(number, { accessKey }); + } + + return this.server.getProfile(number); + } + + async getAvatar(path: string) { + return this.server.getAvatar(path); + } + + async getSticker(packId: string, stickerId: string) { + return this.server.getSticker(packId, stickerId); + } + async getStickerPackManifest(packId: string) { + return this.server.getStickerPackManifest(packId); + } + + async sendRequestBlockSyncMessage(options?: SendOptionsType) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + if (myDevice !== 1 && myDevice !== '1') { + const request = new window.textsecure.protobuf.SyncMessage.Request(); + request.type = + window.textsecure.protobuf.SyncMessage.Request.Type.BLOCKED; + const syncMessage = this.createSyncMessage(); + syncMessage.request = request; + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + Date.now(), + silent, + options + ); + } + + return Promise.resolve(); + } + + async sendRequestConfigurationSyncMessage(options?: SendOptionsType) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + if (myDevice !== 1 && myDevice !== '1') { + const request = new window.textsecure.protobuf.SyncMessage.Request(); + request.type = + window.textsecure.protobuf.SyncMessage.Request.Type.CONFIGURATION; + const syncMessage = this.createSyncMessage(); + syncMessage.request = request; + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + Date.now(), + silent, + options + ); + } + + return Promise.resolve(); + } + + async sendRequestGroupSyncMessage(options?: SendOptionsType) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + if (myDevice !== 1 && myDevice !== '1') { + const request = new window.textsecure.protobuf.SyncMessage.Request(); + request.type = window.textsecure.protobuf.SyncMessage.Request.Type.GROUPS; + const syncMessage = this.createSyncMessage(); + syncMessage.request = request; + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + Date.now(), + silent, + options + ); + } + + return Promise.resolve(); + } + + async sendRequestContactSyncMessage(options?: SendOptionsType) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + + const myDevice = window.textsecure.storage.user.getDeviceId(); + if (myDevice !== 1 && myDevice !== '1') { + const request = new window.textsecure.protobuf.SyncMessage.Request(); + request.type = + window.textsecure.protobuf.SyncMessage.Request.Type.CONTACTS; + const syncMessage = this.createSyncMessage(); + syncMessage.request = request; + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + Date.now(), + silent, + options + ); + } + + return Promise.resolve(); + } + + async sendTypingMessage( + options: { + recipientId: string; + groupId: string; + groupNumbers: Array; + isTyping: boolean; + timestamp: number; + }, + sendOptions: SendOptionsType = {} + ) { + const ACTION_ENUM = window.textsecure.protobuf.TypingMessage.Action; + const { recipientId, groupId, groupNumbers, isTyping, timestamp } = options; + + // We don't want to send typing messages to our other devices, but we will + // in the group case. + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + if (recipientId && (myNumber === recipientId || myUuid === recipientId)) { + return null; + } + + if (!recipientId && !groupId) { + throw new Error('Need to provide either recipientId or groupId!'); + } + + const recipients = groupId + ? (without(groupNumbers, myNumber, myUuid) as Array) + : [recipientId]; + const groupIdBuffer = groupId + ? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId) + : null; + + const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED; + const finalTimestamp = timestamp || Date.now(); + + const typingMessage = new window.textsecure.protobuf.TypingMessage(); + typingMessage.groupId = groupIdBuffer; + typingMessage.action = action; + typingMessage.timestamp = finalTimestamp; + + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.typingMessage = typingMessage; + + const silent = true; + const online = true; + + return this.sendMessageProtoAndWait( + finalTimestamp, + recipients, + contentMessage, + silent, + { + ...sendOptions, + online, + } + ); + } + + async sendDeliveryReceipt( + recipientE164: string, + recipientUuid: string, + timestamps: Array, + options?: SendOptionsType + ) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + if ( + (myNumber === recipientE164 || myUuid === recipientUuid) && + (myDevice === 1 || myDevice === '1') + ) { + return Promise.resolve(); + } + + const receiptMessage = new window.textsecure.protobuf.ReceiptMessage(); + receiptMessage.type = + window.textsecure.protobuf.ReceiptMessage.Type.DELIVERY; + receiptMessage.timestamp = timestamps; + + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.receiptMessage = receiptMessage; + + const silent = true; + return this.sendIndividualProto( + recipientUuid || recipientE164, + contentMessage, + Date.now(), + silent, + options + ); + } + + async sendReadReceipts( + senderE164: string, + senderUuid: string, + timestamps: Array, + options?: SendOptionsType + ) { + const receiptMessage = new window.textsecure.protobuf.ReceiptMessage(); + receiptMessage.type = window.textsecure.protobuf.ReceiptMessage.Type.READ; + receiptMessage.timestamp = timestamps; + + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.receiptMessage = receiptMessage; + + const silent = true; + return this.sendIndividualProto( + senderUuid || senderE164, + contentMessage, + Date.now(), + silent, + options + ); + } + async syncReadMessages( + reads: Array<{ sender: string; timestamp: number }>, + options?: SendOptionsType + ) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + if (myDevice !== 1 && myDevice !== '1') { + 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].sender; + + syncMessage.read.push(read); + } + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + Date.now(), + silent, + options + ); + } + + return Promise.resolve(); + } + + async syncViewOnceOpen( + sender: string, + senderUuid: string, + timestamp: number, + options?: SendOptionsType + ) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + if (myDevice === 1 || myDevice === '1') { + return null; + } + + const syncMessage = this.createSyncMessage(); + + const viewOnceOpen = new window.textsecure.protobuf.SyncMessage.ViewOnceOpen(); + viewOnceOpen.sender = sender; + viewOnceOpen.senderUuid = senderUuid; + viewOnceOpen.timestamp = timestamp; + syncMessage.viewOnceOpen = viewOnceOpen; + + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + Date.now(), + silent, + options + ); + } + + async sendStickerPackSync( + operations: Array<{ + packId: string; + packKey: string; + installed: boolean; + }>, + options?: SendOptionsType + ) { + const myDevice = window.textsecure.storage.user.getDeviceId(); + if (myDevice === 1 || myDevice === '1') { + return null; + } + + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const ENUM = + window.textsecure.protobuf.SyncMessage.StickerPackOperation.Type; + + const packOperations = operations.map(item => { + const { packId, packKey, installed } = item; + + const operation = new window.textsecure.protobuf.SyncMessage.StickerPackOperation(); + operation.packId = hexStringToArrayBuffer(packId); + operation.packKey = base64ToArrayBuffer(packKey); + operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE; + + return operation; + }); + + const syncMessage = this.createSyncMessage(); + syncMessage.stickerPackOperation = packOperations; + + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.syncMessage = syncMessage; + + const silent = true; + return this.sendIndividualProto( + myUuid || myNumber, + contentMessage, + Date.now(), + silent, + options + ); + } + + async syncVerification( + destinationE164: string, + destinationUuid: string, + state: number, + identityKey: ArrayBuffer, + options?: SendOptionsType + ) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const myDevice = window.textsecure.storage.user.getDeviceId(); + const now = Date.now(); + + if (myDevice === 1 || myDevice === '1') { + return Promise.resolve(); + } + + // First send a null message to mask the sync message. + const nullMessage = new window.textsecure.protobuf.NullMessage(); + + // Generate a random int from 1 and 512 + const buffer = window.libsignal.crypto.getRandomBytes(1); + const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1; + + // Generate a random padding buffer of the chosen size + nullMessage.padding = window.libsignal.crypto.getRandomBytes(paddingLength); + + const contentMessage = new window.textsecure.protobuf.Content(); + contentMessage.nullMessage = nullMessage; + + // We want the NullMessage to look like a normal outgoing message; not silent + const silent = false; + const promise = this.sendIndividualProto( + destinationUuid || destinationE164, + contentMessage, + now, + silent, + options + ); + + return promise.then(async () => { + const verified = new window.textsecure.protobuf.Verified(); + verified.state = state; + if (destinationE164) { + verified.destination = destinationE164; + } + if (destinationUuid) { + verified.destinationUuid = destinationUuid; + } + verified.identityKey = identityKey; + verified.nullMessage = nullMessage.padding; + + const syncMessage = this.createSyncMessage(); + syncMessage.verified = verified; + + const secondMessage = new window.textsecure.protobuf.Content(); + secondMessage.syncMessage = syncMessage; + + const innerSilent = true; + return this.sendIndividualProto( + myUuid || myNumber, + secondMessage, + now, + innerSilent, + options + ); + }); + } + + async sendGroupProto( + providedIdentifiers: Array, + proto: DataMessageClass, + timestamp = Date.now(), + options = {} + ) { + const myE164 = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const identifiers = providedIdentifiers.filter( + id => id !== myE164 && id !== myUuid + ); + + if (identifiers.length === 0) { + return Promise.resolve({ + successfulIdentifiers: [], + failoverIdentifiers: [], + errors: [], + unidentifiedDeliveries: [], + dataMessage: proto.toArrayBuffer(), + }); + } + + return new Promise((resolve, reject) => { + const silent = true; + const callback = (res: CallbackResultType) => { + res.dataMessage = proto.toArrayBuffer(); + if (res.errors && res.errors.length > 0) { + reject(res); + } else { + resolve(res); + } + }; + + this.sendMessageProto( + timestamp, + providedIdentifiers, + proto, + callback, + silent, + options + ); + }); + } + + async getMessageProto( + destination: string, + body: string, + attachments: Array | null, + quote: any, + preview: Array | null, + sticker: any, + reaction: any, + timestamp: number, + expireTimer: number | undefined, + profileKey?: string, + flags?: number + ) { + const attributes = { + recipients: [destination], + destination, + body, + timestamp, + attachments, + quote, + preview, + sticker, + reaction, + expireTimer, + profileKey, + flags, + }; + + return this.getMessageProtoObj(attributes); + } + + async getMessageProtoObj(attributes: MessageOptionsType) { + const message = new Message(attributes); + await Promise.all([ + this.uploadAttachments(message), + this.uploadThumbnails(message), + this.uploadLinkPreviews(message), + this.uploadSticker(message), + ]); + + return message.toArrayBuffer(); + } + + async sendMessageToIdentifier( + identifier: string, + messageText: string, + attachments: Array | null, + quote: any, + preview: Array | null, + sticker: any, + reaction: any, + timestamp: number, + expireTimer: number | undefined, + profileKey?: string, + options?: SendOptionsType + ) { + return this.sendMessage( + { + recipients: [identifier], + body: messageText, + timestamp, + attachments, + quote, + preview, + sticker, + reaction, + expireTimer, + profileKey, + }, + options + ); + } + + async resetSession( + identifier: string, + timestamp: number, + options?: SendOptionsType + ) { + window.log.info('resetting secure session'); + const silent = false; + const proto = new window.textsecure.protobuf.DataMessage(); + proto.body = 'TERMINATE'; + proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION; + + const logError = (prefix: string) => (error: Error) => { + window.log.error(prefix, error && error.stack ? error.stack : error); + throw error; + }; + const deleteAllSessions = async (targetNumber: string) => + window.textsecure.storage.protocol + .getDeviceIds(targetNumber) + .then(async deviceIds => + Promise.all( + deviceIds.map(async deviceId => { + const address = new window.libsignal.SignalProtocolAddress( + targetNumber, + deviceId + ); + window.log.info('deleting sessions for', address.toString()); + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, + address + ); + return sessionCipher.deleteAllSessionsForDevice(); + }) + ) + ); + + const sendToContactPromise = deleteAllSessions(identifier) + .catch(logError('resetSession/deleteAllSessions1 error:')) + .then(async () => { + window.log.info( + 'finished closing local sessions, now sending to contact' + ); + return this.sendIndividualProto( + identifier, + proto, + timestamp, + silent, + options + ).catch(logError('resetSession/sendToContact error:')); + }) + .then(async () => + deleteAllSessions(identifier).catch( + logError('resetSession/deleteAllSessions2 error:') + ) + ); + + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + // We already sent the reset session to our other devices in the code above! + if (identifier === myNumber || identifier === myUuid) { + return sendToContactPromise; + } + + const buffer = proto.toArrayBuffer(); + const sendSyncPromise = this.sendSyncMessage( + buffer, + timestamp, + identifier, + null, + null, + [], + [], + false, + options + ).catch(logError('resetSession/sendSync error:')); + + return Promise.all([sendToContactPromise, sendSyncPromise]); + } + + async sendMessageToGroup( + groupId: string, + recipients: Array, + messageText: string, + attachments: Array, + quote: any, + preview: any, + sticker: any, + reaction: any, + timestamp: number, + expireTimer: number | undefined, + profileKey?: string, + options?: SendOptionsType + ) { + const myE164 = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getNumber(); + const attrs = { + recipients: recipients.filter(r => r !== myE164 && r !== myUuid), + body: messageText, + timestamp, + attachments, + quote, + preview, + sticker, + reaction, + expireTimer, + profileKey, + group: { + id: groupId, + type: window.textsecure.protobuf.GroupContext.Type.DELIVER, + }, + }; + + if (recipients.length === 0) { + return Promise.resolve({ + successfulIdentifiers: [], + failoverIdentifiers: [], + errors: [], + unidentifiedDeliveries: [], + dataMessage: await this.getMessageProtoObj(attrs), + }); + } + + return this.sendMessage(attrs, options); + } + + async createGroup( + targetIdentifiers: Array, + id: string, + name: string, + avatar: AttachmentType, + options?: SendOptionsType + ) { + const proto = new window.textsecure.protobuf.DataMessage(); + proto.group = new window.textsecure.protobuf.GroupContext(); + proto.group.id = stringToArrayBuffer(id); + + proto.group.type = window.textsecure.protobuf.GroupContext.Type.UPDATE; + proto.group.members = targetIdentifiers; + proto.group.name = name; + + return this.makeAttachmentPointer(avatar).then(async attachment => { + if (!proto.group) { + throw new Error('createGroup: proto.group was set to null'); + } + proto.group.avatar = attachment; + return this.sendGroupProto( + targetIdentifiers.map(item => { + const identifier = item.uuid || item.e164; + if (!identifier) { + throw new Error( + 'SendMessage.createGroup: Provided group member had neither uuid nor e164' + ); + } + return identifier; + }), + proto, + Date.now(), + options + ).then(() => { + if (!proto.group) { + throw new Error('createGroup: proto.group was set to null'); + } + + return proto.group.id; + }); + }); + } + + async updateGroup( + groupId: string, + name: string, + avatar: AttachmentType, + targetIdentifiers: Array, + options?: SendOptionsType + ) { + 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.UPDATE; + proto.group.name = name; + proto.group.members = targetIdentifiers; + + return this.makeAttachmentPointer(avatar).then(async attachment => { + if (!proto.group) { + throw new Error('updateGroup: proto.group was set to null'); + } + + proto.group.avatar = attachment; + return this.sendGroupProto( + targetIdentifiers.map(item => { + const identifier = item.uuid || item.e164; + if (!identifier) { + throw new Error( + 'SendMessage.updateGroup: Provided group member had neither uuid nor e164' + ); + } + return identifier; + }), + proto, + Date.now(), + options + ).then(() => { + if (!proto.group) { + throw new Error('updateGroup: proto.group was set to null'); + } + return proto.group.id; + }); + }); + } + + async addIdentifierToGroup( + groupId: string, + newIdentifiers: Array, + options: SendOptionsType + ) { + 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.UPDATE; + proto.group.members = newIdentifiers; + return this.sendGroupProto( + newIdentifiers.map(item => { + const identifier = item.uuid || item.e164; + if (!identifier) { + throw new Error( + 'SendMessage.addIdentifierToGroup: Provided group member had neither uuid nor e164' + ); + } + return identifier; + }), + proto, + Date.now(), + options + ); + } + + async setGroupName( + groupId: string, + name: string, + groupIdentifiers: Array, + options: SendOptionsType + ) { + 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.UPDATE; + proto.group.name = name; + proto.group.members = groupIdentifiers; + + return this.sendGroupProto( + groupIdentifiers.map(item => { + const identifier = item.uuid || item.e164; + if (!identifier) { + throw new Error( + 'SendMessage.setGroupName: Provided group member had neither uuid nor e164' + ); + } + return identifier; + }), + proto, + Date.now(), + options + ); + } + + async setGroupAvatar( + groupId: string, + avatar: AttachmentType, + groupIdentifiers: Array, + options: SendOptionsType + ) { + 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.UPDATE; + proto.group.members = groupIdentifiers; + + return this.makeAttachmentPointer(avatar).then(async attachment => { + if (!proto.group) { + throw new Error('setGroupAvatar: proto.group was set to null'); + } + + proto.group.avatar = attachment; + return this.sendGroupProto( + groupIdentifiers.map(item => { + const identifier = item.uuid || item.e164; + if (!identifier) { + throw new Error( + 'SendMessage.setGroupAvatar: Provided group member had neither uuid nor e164' + ); + } + return identifier; + }), + proto, + Date.now(), + options + ); + }); + } + + async leaveGroup( + groupId: string, + groupIdentifiers: Array, + options?: SendOptionsType + ) { + 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; + return this.sendGroupProto(groupIdentifiers, proto, Date.now(), options); + } + async sendExpirationTimerUpdateToGroup( + groupId: string, + groupIdentifiers: Array, + expireTimer: number | undefined, + timestamp: number, + profileKey?: string, + options?: SendOptionsType + ) { + const myNumber = window.textsecure.storage.user.getNumber(); + const myUuid = window.textsecure.storage.user.getUuid(); + const recipients = groupIdentifiers.filter( + identifier => identifier !== myNumber && identifier !== myUuid + ); + const attrs = { + recipients, + timestamp, + expireTimer, + profileKey, + flags: + window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + group: { + id: groupId, + type: window.textsecure.protobuf.GroupContext.Type.DELIVER, + }, + }; + + if (recipients.length === 0) { + return Promise.resolve({ + successfulIdentifiers: [], + failoverIdentifiers: [], + errors: [], + unidentifiedDeliveries: [], + dataMessage: await this.getMessageProtoObj(attrs), + }); + } + + return this.sendMessage(attrs, options); + } + async sendExpirationTimerUpdateToIdentifier( + identifier: string, + expireTimer: number | undefined, + timestamp: number, + profileKey?: string, + options?: SendOptionsType + ) { + return this.sendMessage( + { + recipients: [identifier], + timestamp, + expireTimer, + profileKey, + flags: + window.textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + }, + options + ); + } + async makeProxiedRequest(url: string, options?: ProxiedRequestOptionsType) { + return this.server.makeProxiedRequest(url, options); + } +} diff --git a/ts/textsecure/Storage.ts b/ts/textsecure/Storage.ts new file mode 100644 index 0000000000..f8d7772fb0 --- /dev/null +++ b/ts/textsecure/Storage.ts @@ -0,0 +1,49 @@ +// tslint:disable no-default-export + +import utils from './Helpers'; + +// Default implmentation working with localStorage +const localStorageImpl = { + put(key: string, value: any) { + if (value === undefined) { + throw new Error('Tried to store undefined'); + } + localStorage.setItem(`${key}`, utils.jsonThing(value)); + }, + + get(key: string, defaultValue: any) { + const value = localStorage.getItem(`${key}`); + if (value === null) { + return defaultValue; + } + return JSON.parse(value); + }, + + remove(key: string) { + localStorage.removeItem(`${key}`); + }, +}; + +export interface StorageInterface { + put(key: string, value: any): void | Promise; + get(key: string, defaultValue: any): any; + remove(key: string): void | Promise; +} + +const Storage = { + impl: localStorageImpl as StorageInterface, + + put(key: string, value: any) { + return Storage.impl.put(key, value); + }, + + get(key: string, defaultValue: any) { + return Storage.impl.get(key, defaultValue); + }, + + remove(key: string) { + return Storage.impl.remove(key); + }, +}; + +export default Storage; diff --git a/ts/textsecure/StringView.ts b/ts/textsecure/StringView.ts new file mode 100644 index 0000000000..46470c9d02 --- /dev/null +++ b/ts/textsecure/StringView.ts @@ -0,0 +1,97 @@ +// tslint:disable binary-expression-operand-order no-bitwise no-default-export + +const StringView = { + /* + * These functions from the Mozilla Developer Network + * and have been placed in the public domain. + * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses + */ + + // prettier-ignore + b64ToUint6(nChr: number) { + return nChr > 64 && nChr < 91 + ? nChr - 65 + : nChr > 96 && nChr < 123 + ? nChr - 71 + : nChr > 47 && nChr < 58 + ? nChr + 4 + : nChr === 43 + ? 62 + : nChr === 47 + ? 63 + : 0; + }, + + base64ToBytes(sBase64: string, nBlocksSize: number) { + const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ''); + const nInLen = sB64Enc.length; + const nOutLen = nBlocksSize + ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize + : (nInLen * 3 + 1) >> 2; + const aBBytes = new ArrayBuffer(nOutLen); + const taBytes = new Uint8Array(aBBytes); + + let nMod3; + let nMod4; + let nOutIdx = 0; + let nInIdx = 0; + for (let nUint24 = 0; nInIdx < nInLen; nInIdx += 1) { + nMod4 = nInIdx & 3; + nUint24 |= + StringView.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (18 - 6 * nMod4); + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for ( + nMod3 = 0; + nMod3 < 3 && nOutIdx < nOutLen; + nMod3 += 1, nOutIdx += 1 + ) { + taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; + } + nUint24 = 0; + } + } + return aBBytes; + }, + + // prettier-ignore + uint6ToB64(nUint6: number) { + return nUint6 < 26 + ? nUint6 + 65 + : nUint6 < 52 + ? nUint6 + 71 + : nUint6 < 62 + ? nUint6 - 4 + : nUint6 === 62 + ? 43 + : nUint6 === 63 + ? 47 + : 65; + }, + + bytesToBase64(aBytes: Uint8Array) { + let nMod3; + let sB64Enc = ''; + let nUint24 = 0; + const nLen = aBytes.length; + for (let nIdx = 0; nIdx < nLen; nIdx += 1) { + nMod3 = nIdx % 3; + if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) { + sB64Enc += '\r\n'; + } + nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) { + sB64Enc += String.fromCharCode( + StringView.uint6ToB64((nUint24 >>> 18) & 63), + StringView.uint6ToB64((nUint24 >>> 12) & 63), + StringView.uint6ToB64((nUint24 >>> 6) & 63), + StringView.uint6ToB64(nUint24 & 63) + ); + nUint24 = 0; + } + } + return sB64Enc.replace(/A(?=A$|$)/g, '='); + }, +}; + +export default StringView; diff --git a/ts/textsecure/SyncRequest.ts b/ts/textsecure/SyncRequest.ts new file mode 100644 index 0000000000..78d1f8fe83 --- /dev/null +++ b/ts/textsecure/SyncRequest.ts @@ -0,0 +1,100 @@ +import EventTarget from './EventTarget'; +import MessageReceiver from './MessageReceiver'; +import MessageSender from './SendMessage'; + +class SyncRequestInner extends EventTarget { + receiver: MessageReceiver; + contactSync?: boolean; + groupSync?: boolean; + timeout: any; + oncontact: Function; + ongroup: Function; + + constructor(sender: MessageSender, receiver: MessageReceiver) { + super(); + + if ( + !(sender instanceof MessageSender) || + !(receiver instanceof MessageReceiver) + ) { + throw new Error( + 'Tried to construct a SyncRequest without MessageSender and MessageReceiver' + ); + } + this.receiver = receiver; + + this.oncontact = this.onContactSyncComplete.bind(this); + receiver.addEventListener('contactsync', this.oncontact); + + this.ongroup = this.onGroupSyncComplete.bind(this); + receiver.addEventListener('groupsync', this.ongroup); + + const ourNumber = window.textsecure.storage.user.getNumber(); + const { wrap, sendOptions } = window.ConversationController.prepareForSend( + ourNumber, + { + syncMessage: true, + } + ); + + window.log.info('SyncRequest created. Sending config sync request...'); + // tslint:disable + wrap(sender.sendRequestConfigurationSyncMessage(sendOptions)); + + window.log.info('SyncRequest now sending block sync request...'); + wrap(sender.sendRequestBlockSyncMessage(sendOptions)); + + window.log.info('SyncRequest now sending contact sync message...'); + wrap(sender.sendRequestContactSyncMessage(sendOptions)) + .then(() => { + window.log.info('SyncRequest now sending group sync messsage...'); + return wrap(sender.sendRequestGroupSyncMessage(sendOptions)); + }) + .catch((error: Error) => { + window.log.error( + 'SyncRequest error:', + error && error.stack ? error.stack : error + ); + }); + this.timeout = setTimeout(this.onTimeout.bind(this), 60000); + } + onContactSyncComplete() { + this.contactSync = true; + this.update(); + } + onGroupSyncComplete() { + this.groupSync = true; + this.update(); + } + update() { + if (this.contactSync && this.groupSync) { + this.dispatchEvent(new Event('success')); + this.cleanup(); + } + } + onTimeout() { + if (this.contactSync || this.groupSync) { + this.dispatchEvent(new Event('success')); + } else { + this.dispatchEvent(new Event('timeout')); + } + this.cleanup(); + } + cleanup() { + clearTimeout(this.timeout); + this.receiver.removeEventListener('contactsync', this.oncontact); + this.receiver.removeEventListener('groupSync', this.ongroup); + delete this.listeners; + } +} + +export default class SyncRequest { + constructor(sender: MessageSender, receiver: MessageReceiver) { + const inner = new SyncRequestInner(sender, receiver); + this.addEventListener = inner.addEventListener.bind(inner); + this.removeEventListener = inner.removeEventListener.bind(inner); + } + + addEventListener: (name: string, handler: Function) => void; + removeEventListener: (name: string, handler: Function) => void; +} diff --git a/ts/textsecure/TaskWithTimeout.ts b/ts/textsecure/TaskWithTimeout.ts new file mode 100644 index 0000000000..e73dc2808f --- /dev/null +++ b/ts/textsecure/TaskWithTimeout.ts @@ -0,0 +1,78 @@ +// tslint:disable no-default-export + +export default function createTaskWithTimeout( + task: () => Promise, + id: string, + options: { timeout?: number } = {} +) { + const timeout = options.timeout || 1000 * 60 * 2; // two minutes + + const errorForStack = new Error('for stack'); + + return async () => + new Promise((resolve, reject) => { + let complete = false; + let timer: any = setTimeout(() => { + if (!complete) { + const message = `${id || + ''} task did not complete in time. Calling stack: ${ + errorForStack.stack + }`; + + window.log.error(message); + reject(new Error(message)); + + return; + } + + return null; + }, timeout); + const clearTimer = () => { + try { + const localTimer = timer; + if (localTimer) { + timer = null; + clearTimeout(localTimer); + } + } catch (error) { + window.log.error( + id || '', + 'task ran into problem canceling timer. Calling stack:', + errorForStack.stack + ); + } + }; + + const success = (result: any) => { + clearTimer(); + complete = true; + resolve(result); + + return; + }; + const failure = (error: Error) => { + clearTimer(); + complete = true; + reject(error); + + return; + }; + + let promise; + try { + promise = task(); + } catch (error) { + clearTimer(); + throw error; + } + if (!promise || !promise.then) { + clearTimer(); + complete = true; + resolve(promise); + + return; + } + + return promise.then(success, failure); + }); +} diff --git a/ts/WebAPI.ts b/ts/textsecure/WebAPI.ts similarity index 90% rename from ts/WebAPI.ts rename to ts/textsecure/WebAPI.ts index 2b7eff5cde..085a1f0542 100644 --- a/ts/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -4,8 +4,8 @@ import ProxyAgent from 'proxy-agent'; import { Agent } from 'https'; import is from '@sindresorhus/is'; -import { redactPackId } from '../js/modules/stickers'; -import { getRandomValue } from './Crypto'; +import { redactPackId } from '../../js/modules/stickers'; +import { getRandomValue } from '../Crypto'; import PQueue from 'p-queue'; import { v4 as getGuid } from 'uuid'; @@ -450,6 +450,7 @@ declare global { interface Error { code?: number | string; response?: any; + warn?: boolean; } } @@ -488,10 +489,6 @@ const URL_CALLS = { whoami: 'v1/accounts/whoami', }; -module.exports = { - initialize, -}; - type InitializeOptionsType = { url: string; cdnUrl: string; @@ -520,16 +517,128 @@ type AjaxOptionsType = { validateResponse?: any; }; +export type WebAPIConnectType = { + connect: (options: ConnectParametersType) => WebAPIType; +}; + +type StickerPackManifestType = any; + +export type WebAPIType = { + confirmCode: ( + number: string, + code: string, + newPassword: string, + registrationId: number, + deviceName?: string | null, + options?: { accessKey?: ArrayBuffer } + ) => Promise; + getAttachment: (id: string) => Promise; + getAvatar: (path: string) => Promise; + getDevices: () => Promise; + getKeysForIdentifier: ( + identifier: string, + deviceId?: number + ) => Promise; + getKeysForIdentifierUnauth: ( + identifier: string, + deviceId?: number, + options?: { accessKey?: string } + ) => Promise; + getMessageSocket: () => WebSocket; + getMyKeys: () => Promise; + getProfile: (identifier: string) => Promise; + getProfileUnauth: ( + identifier: string, + options?: { accessKey?: string } + ) => Promise; + getProvisioningSocket: () => WebSocket; + getSenderCertificate: (withUuid?: boolean) => Promise; + getSticker: (packId: string, stickerId: string) => Promise; + getStickerPackManifest: (packId: string) => Promise; + makeProxiedRequest: ( + targetUrl: string, + options?: ProxiedRequestOptionsType + ) => Promise; + putAttachment: (encryptedBin: ArrayBuffer) => Promise; + registerCapabilities: (capabilities: any) => Promise; + putStickers: ( + encryptedManifest: ArrayBuffer, + encryptedStickers: Array, + onProgress?: () => void + ) => Promise; + registerKeys: (genKeys: KeysType) => Promise; + registerSupportForUnauthenticatedDelivery: () => Promise; + removeSignalingKey: () => Promise; + requestVerificationSMS: (number: string) => Promise; + requestVerificationVoice: (number: string) => Promise; + sendMessages: ( + destination: string, + messageArray: Array, + timestamp: number, + silent?: boolean, + online?: boolean + ) => Promise; + sendMessagesUnauth: ( + destination: string, + messageArray: Array, + timestamp: number, + silent?: boolean, + online?: boolean, + options?: { accessKey?: string } + ) => Promise; + setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise; + updateDeviceName: (deviceName: string) => Promise; + whoami: () => Promise; +}; + +export type SignedPreKeyType = { + keyId: number; + publicKey: ArrayBuffer; + signature: ArrayBuffer; +}; + +export type KeysType = { + identityKey: ArrayBuffer; + signedPreKey: SignedPreKeyType; + preKeys: Array<{ + keyId: number; + publicKey: ArrayBuffer; + }>; +}; + +export type ServerKeysType = { + devices: Array<{ + deviceId: number; + registrationId: number; + signedPreKey: { + keyId: number; + publicKey: ArrayBuffer; + signature: ArrayBuffer; + }; + preKey?: { + keyId: number; + publicKey: ArrayBuffer; + }; + }>; + identityKey: ArrayBuffer; +}; + +export type ProxiedRequestOptionsType = { + returnArrayBuffer?: boolean; + start?: number; + end?: number; +}; + // We first set up the data that won't change during this session of the app // tslint:disable-next-line max-func-body-length -function initialize({ +export function initialize({ url, cdnUrl, certificateAuthority, contentProxyUrl, proxyUrl, version, -}: InitializeOptionsType) { +}: InitializeOptionsType): WebAPIConnectType { if (!is.string(url)) { throw new Error('WebAPI.initialize: Invalid server url'); } @@ -744,9 +853,9 @@ function initialize({ number: string, code: string, newPassword: string, - registrationId: string, - deviceName: string, - options: { accessKey?: string } = {} + registrationId: number, + deviceName?: string | null, + options: { accessKey?: ArrayBuffer } = {} ) { const { accessKey } = options; const jsonData: any = { @@ -812,21 +921,6 @@ function initialize({ }); } - type SignedPreKeyType = { - keyId: number; - publicKey: ArrayBuffer; - signature: ArrayBuffer; - }; - - type KeysType = { - identityKey: ArrayBuffer; - signedPreKey: SignedPreKeyType; - preKeys: Array<{ - keyId: number; - publicKey: ArrayBuffer; - }>; - }; - type JSONSignedPreKeyType = { keyId: number; publicKey: string; @@ -918,24 +1012,7 @@ function initialize({ identityKey: string; }; - type ServerKeyType = { - devices: Array<{ - deviceId: number; - registrationId: number; - signedPreKey: { - keyId: number; - publicKey: ArrayBuffer; - signature: ArrayBuffer; - }; - preKey?: { - keyId: number; - publicKey: ArrayBuffer; - }; - }>; - identityKey: ArrayBuffer; - }; - - function handleKeys(res: ServerKeyResponseType): ServerKeyType { + function handleKeys(res: ServerKeyResponseType): ServerKeysType { if (!Array.isArray(res.devices)) { throw new Error('Invalid response'); } @@ -984,11 +1061,11 @@ function initialize({ }; } - async function getKeysForIdentifier(identifier: string, deviceId = '*') { + async function getKeysForIdentifier(identifier: string, deviceId?: number) { return _ajax({ call: 'keys', httpType: 'GET', - urlParameters: `/${identifier}/${deviceId}`, + urlParameters: `/${identifier}/${deviceId || '*'}`, responseType: 'json', validateResponse: { identityKey: 'string', devices: 'object' }, }).then(handleKeys); @@ -996,13 +1073,13 @@ function initialize({ async function getKeysForIdentifierUnauth( identifier: string, - deviceId = '*', + deviceId?: number, { accessKey }: { accessKey?: string } = {} ) { return _ajax({ call: 'keys', httpType: 'GET', - urlParameters: `/${identifier}/${deviceId}`, + urlParameters: `/${identifier}/${deviceId || '*'}`, responseType: 'json', validateResponse: { identityKey: 'string', devices: 'object' }, unauthenticated: true, @@ -1014,8 +1091,8 @@ function initialize({ destination: string, messageArray: Array, timestamp: number, - silent: boolean, - online: boolean, + silent?: boolean, + online?: boolean, { accessKey }: { accessKey?: string } = {} ) { const jsonData: any = { messages: messageArray, timestamp }; @@ -1042,8 +1119,8 @@ function initialize({ destination: string, messageArray: Array, timestamp: number, - silent: boolean, - online: boolean + silent?: boolean, + online?: boolean ) { const jsonData: any = { messages: messageArray, timestamp }; @@ -1262,12 +1339,6 @@ function initialize({ return characters; } - type ProxiedRequestOptionsType = { - returnArrayBuffer?: boolean; - start?: number; - end?: number; - }; - async function makeProxiedRequest( targetUrl: string, options: ProxiedRequestOptionsType = {} diff --git a/ts/textsecure/WebsocketResources.ts b/ts/textsecure/WebsocketResources.ts new file mode 100644 index 0000000000..d4d45b7c25 --- /dev/null +++ b/ts/textsecure/WebsocketResources.ts @@ -0,0 +1,304 @@ +/* + * WebSocket-Resources + * + * Create a request-response interface over websockets using the + * WebSocket-Resources sub-protocol[1]. + * + * var client = new WebSocketResource(socket, function(request) { + * request.respond(200, 'OK'); + * }); + * + * client.sendRequest({ + * verb: 'PUT', + * path: '/v1/messages', + * body: '{ some: "json" }', + * success: function(message, status, request) {...}, + * error: function(message, status, request) {...} + * }); + * + * 1. https://github.com/signalapp/WebSocket-Resources + * + */ + +// tslint:disable max-classes-per-file no-default-export no-unnecessary-class + +import { w3cwebsocket as WebSocket } from 'websocket'; +import { ByteBufferClass } from '../window.d'; + +import EventTarget from './EventTarget'; + +class Request { + verb: string; + path: string; + headers: Array; + body: ByteBufferClass | null; + success: Function; + error: Function; + id: number; + response?: any; + + constructor(options: any) { + this.verb = options.verb || options.type; + this.path = options.path || options.url; + this.headers = options.headers; + this.body = options.body || options.data; + this.success = options.success; + this.error = options.error; + this.id = options.id; + + if (this.id === undefined) { + const bits = new Uint32Array(2); + window.crypto.getRandomValues(bits); + this.id = window.dcodeIO.Long.fromBits(bits[0], bits[1], true); + } + + if (this.body === undefined) { + this.body = null; + } + } +} + +export class IncomingWebSocketRequest { + verb: string; + path: string; + body: ByteBufferClass | null; + headers: Array; + respond: (status: number, message: string) => void; + + constructor(options: any) { + const request = new Request(options); + const { socket } = options; + + this.verb = request.verb; + this.path = request.path; + this.body = request.body; + this.headers = request.headers; + + this.respond = (status, message) => { + socket.send( + new window.textsecure.protobuf.WebSocketMessage({ + type: window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE, + response: { id: request.id, message, status }, + }) + .encode() + .toArrayBuffer() + ); + }; + } +} + +const outgoing: { + [id: number]: Request; +} = {}; +class OutgoingWebSocketRequest { + constructor(options: any, socket: WebSocket) { + const request = new Request(options); + outgoing[request.id] = request; + socket.send( + new window.textsecure.protobuf.WebSocketMessage({ + type: window.textsecure.protobuf.WebSocketMessage.Type.REQUEST, + request: { + verb: request.verb, + path: request.path, + body: request.body, + headers: request.headers, + id: request.id, + }, + }) + .encode() + .toArrayBuffer() + ); + } +} + +export default class WebSocketResource extends EventTarget { + closed?: boolean; + close: (code?: number, reason?: string) => void; + sendRequest: (options: any) => OutgoingWebSocketRequest; + keepalive?: KeepAlive; + + // tslint:disable-next-line max-func-body-length + constructor(socket: WebSocket, opts: any = {}) { + super(); + + let { handleRequest } = opts; + if (typeof handleRequest !== 'function') { + handleRequest = (request: IncomingWebSocketRequest) => { + request.respond(404, 'Not found'); + }; + } + this.sendRequest = options => new OutgoingWebSocketRequest(options, socket); + + // eslint-disable-next-line no-param-reassign + socket.onmessage = socketMessage => { + const blob = socketMessage.data; + const handleArrayBuffer = (buffer: ArrayBuffer) => { + const message = window.textsecure.protobuf.WebSocketMessage.decode( + buffer + ); + if ( + message.type === + window.textsecure.protobuf.WebSocketMessage.Type.REQUEST && + message.request + ) { + handleRequest( + new IncomingWebSocketRequest({ + verb: message.request.verb, + path: message.request.path, + body: message.request.body, + headers: message.request.headers, + id: message.request.id, + socket, + }) + ); + } else if ( + message.type === + window.textsecure.protobuf.WebSocketMessage.Type.RESPONSE && + message.response + ) { + const { response } = message; + const request = outgoing[response.id]; + if (request) { + request.response = response; + let callback = request.error; + if ( + response.status && + response.status >= 200 && + response.status < 300 + ) { + callback = request.success; + } + + if (typeof callback === 'function') { + callback(response.message, response.status, request); + } + } else { + throw new Error( + `Received response for unknown request ${message.response.id}` + ); + } + } + }; + + if (blob instanceof ArrayBuffer) { + handleArrayBuffer(blob); + } else { + const reader = new FileReader(); + reader.onload = () => { + handleArrayBuffer(reader.result as ArrayBuffer); + }; + reader.readAsArrayBuffer(blob as any); + } + }; + + if (opts.keepalive) { + this.keepalive = new KeepAlive(this, { + path: opts.keepalive.path, + disconnect: opts.keepalive.disconnect, + }); + const resetKeepAliveTimer = this.keepalive.reset.bind(this.keepalive); + + // websocket type definitions don't include an addEventListener, but it's there. And + // We can't use declaration merging on classes: + // https://www.typescriptlang.org/docs/handbook/declaration-merging.html#disallowed-merges) + // @ts-ignore + socket.addEventListener('open', resetKeepAliveTimer); + // @ts-ignore + socket.addEventListener('message', resetKeepAliveTimer); + // @ts-ignore + socket.addEventListener( + 'close', + this.keepalive.stop.bind(this.keepalive) + ); + } + + // @ts-ignore + socket.addEventListener('close', () => { + this.closed = true; + }); + + this.close = (code = 3000, reason) => { + if (this.closed) { + return; + } + + window.log.info('WebSocketResource.close()'); + if (this.keepalive) { + this.keepalive.stop(); + } + + socket.close(code, reason); + // @ts-ignore + socket.onmessage = null; + + // On linux the socket can wait a long time to emit its close event if we've + // lost the internet connection. On the order of minutes. This speeds that + // process up. + setTimeout(() => { + if (this.closed) { + return; + } + this.closed = true; + + window.log.warn('Dispatching our own socket close event'); + const ev = new Event('close'); + ev.code = code; + ev.reason = reason; + this.dispatchEvent(ev); + }, 5000); + }; + } +} + +type KeepAliveOptionsType = { + path?: string; + disconnect?: boolean; +}; + +class KeepAlive { + keepAliveTimer: any; + disconnectTimer: any; + path: string; + disconnect: boolean; + wsr: WebSocketResource; + + constructor( + websocketResource: WebSocketResource, + opts: KeepAliveOptionsType = {} + ) { + if (websocketResource instanceof WebSocketResource) { + this.path = opts.path !== undefined ? opts.path : '/'; + this.disconnect = opts.disconnect !== undefined ? opts.disconnect : true; + this.wsr = websocketResource; + } else { + throw new TypeError('KeepAlive expected a WebSocketResource'); + } + } + + stop() { + clearTimeout(this.keepAliveTimer); + clearTimeout(this.disconnectTimer); + } + + reset() { + clearTimeout(this.keepAliveTimer); + clearTimeout(this.disconnectTimer); + this.keepAliveTimer = setTimeout(() => { + if (this.disconnect) { + // automatically disconnect if server doesn't ack + this.disconnectTimer = setTimeout(() => { + clearTimeout(this.keepAliveTimer); + this.wsr.close(3001, 'No response to keepalive request'); + }, 10000); + } else { + this.reset(); + } + window.log.info('Sending a keepalive message'); + this.wsr.sendRequest({ + verb: 'GET', + path: this.path, + success: this.reset.bind(this), + }); + }, 55000); + } +} diff --git a/ts/textsecure/index.ts b/ts/textsecure/index.ts new file mode 100644 index 0000000000..1cc8a7ae97 --- /dev/null +++ b/ts/textsecure/index.ts @@ -0,0 +1,35 @@ +// tslint:disable no-default-export + +import EventTarget from './EventTarget'; +import AccountManager from './AccountManager'; +import MessageReceiver from './MessageReceiver'; +import utils from './Helpers'; +import Crypto from './Crypto'; +import { ContactBuffer, GroupBuffer } from './ContactsParser'; +import createTaskWithTimeout from './TaskWithTimeout'; +import SyncRequest from './SyncRequest'; +import MessageSender from './SendMessage'; +import StringView from './StringView'; +import Storage from './Storage'; +import * as WebAPI from './WebAPI'; +import WebSocketResource from './WebsocketResources'; + +export const textsecure = { + createTaskWithTimeout, + crypto: Crypto, + utils, + storage: Storage, + + AccountManager, + ContactBuffer, + EventTarget, + GroupBuffer, + MessageReceiver, + MessageSender, + SyncRequest, + StringView, + WebAPI, + WebSocketResource, +}; + +export default textsecure; diff --git a/ts/util/batcher.ts b/ts/util/batcher.ts index f4e620dd87..6338bbf4b0 100644 --- a/ts/util/batcher.ts +++ b/ts/util/batcher.ts @@ -9,13 +9,13 @@ window.waitForAllBatchers = async () => { await Promise.all(window.batchers.map(item => item.flushAndWait())); }; -type BatcherOptionsType = { +export type BatcherOptionsType = { wait: number; maxSize: number; processBatch: (items: Array) => Promise; }; -type BatcherType = { +export type BatcherType = { add: (item: ItemType) => void; anyPending: () => boolean; onIdle: () => Promise; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 150dc5e6b5..b45f7d2c82 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -1191,134 +1191,6 @@ "updated": "2018-09-15T00:38:04.183Z", "reasonDetail": "Getting the value, not setting it" }, - { - "rule": "jQuery-append(", - "path": "libtextsecure/contacts_parser.js", - "line": " this.buffer.append(arrayBuffer);", - "lineNumber": 6, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/crypto.js", - "line": " const data = dcodeIO.ByteBuffer.wrap(", - "lineNumber": 206, - "reasonCategory": "falseMatch", - "updated": "2018-09-19T18:13:29.628Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/crypto.js", - "line": " given: dcodeIO.ByteBuffer.wrap(padded)", - "lineNumber": 235, - "reasonCategory": "falseMatch", - "updated": "2020-01-10T23:53:06.768Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/crypto.js", - "line": " ? dcodeIO.ByteBuffer.wrap(padded)", - "lineNumber": 239, - "reasonCategory": "falseMatch", - "updated": "2020-01-10T23:53:06.768Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/message_receiver.js", - "line": " dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();", - "lineNumber": 72, - "reasonCategory": "falseMatch", - "updated": "2020-02-14T20:02:37.507Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/message_receiver.js", - "line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');", - "lineNumber": 74, - "reasonCategory": "falseMatch", - "updated": "2020-02-14T20:02:37.507Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/message_receiver.js", - "line": " dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", - "lineNumber": 76, - "reasonCategory": "falseMatch", - "updated": "2020-02-14T20:02:37.507Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/message_receiver.js", - "line": " dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');", - "lineNumber": 78, - "reasonCategory": "falseMatch", - "updated": "2020-02-14T20:02:37.507Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/message_receiver.js", - "line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);", - "lineNumber": 822, - "reasonCategory": "falseMatch", - "updated": "2020-03-20T17:24:11.472Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/message_receiver.js", - "line": " const buffer = dcodeIO.ByteBuffer.wrap(ciphertext);", - "lineNumber": 847, - "reasonCategory": "falseMatch", - "updated": "2020-03-20T17:24:11.472Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/sendmessage.js", - "line": " return dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();", - "lineNumber": 18, - "reasonCategory": "falseMatch", - "updated": "2020-02-14T20:02:37.507Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/sendmessage.js", - "line": " return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", - "lineNumber": 21, - "reasonCategory": "falseMatch", - "updated": "2020-02-14T20:02:37.507Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/sync_request.js", - "line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));", - "lineNumber": 33, - "reasonCategory": "falseMatch", - "updated": "2018-11-28T19:48:16.607Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/sync_request.js", - "line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));", - "lineNumber": 36, - "reasonCategory": "falseMatch", - "updated": "2019-12-03T00:28:08.683Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/sync_request.js", - "line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))", - "lineNumber": 39, - "reasonCategory": "falseMatch", - "updated": "2019-12-03T00:28:08.683Z" - }, - { - "rule": "jQuery-wrap(", - "path": "libtextsecure/sync_request.js", - "line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));", - "lineNumber": 42, - "reasonCategory": "falseMatch", - "updated": "2019-12-03T00:28:08.683Z" - }, { "rule": "DOM-innerHTML", "path": "node_modules/@electron/get/node_modules/@sindresorhus/is/dist/index.js", @@ -11624,5 +11496,253 @@ "lineNumber": 21, "reasonCategory": "falseMatch", "updated": "2020-02-07T19:52:28.522Z" + }, + { + "rule": "jQuery-append(", + "path": "ts/textsecure/ContactsParser.js", + "line": " this.buffer.append(arrayBuffer);", + "lineNumber": 7, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-append(", + "path": "ts/textsecure/ContactsParser.ts", + "line": " this.buffer.append(arrayBuffer);", + "lineNumber": 26, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/Crypto.js", + "line": " const data = window.dcodeIO.ByteBuffer.wrap(encryptedProfileName, 'base64').toArrayBuffer();", + "lineNumber": 157, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/Crypto.js", + "line": " given: window.dcodeIO.ByteBuffer.wrap(padded)", + "lineNumber": 176, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/Crypto.js", + "line": " ? window.dcodeIO.ByteBuffer.wrap(padded)", + "lineNumber": 180, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/Crypto.ts", + "line": " const data = window.dcodeIO.ByteBuffer.wrap(", + "lineNumber": 223, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/Crypto.ts", + "line": " given: window.dcodeIO.ByteBuffer.wrap(padded)", + "lineNumber": 252, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/Crypto.ts", + "line": " ? window.dcodeIO.ByteBuffer.wrap(padded)", + "lineNumber": 256, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.js", + "line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);", + "lineNumber": 665, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.js", + "line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);", + "lineNumber": 685, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.js", + "line": "MessageReceiverInner.stringToArrayBuffer = (string) => window.dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();", + "lineNumber": 1253, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.js", + "line": "MessageReceiverInner.arrayBufferToString = (arrayBuffer) => window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');", + "lineNumber": 1254, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.js", + "line": "MessageReceiverInner.arrayBufferToStringBase64 = (arrayBuffer) => window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');", + "lineNumber": 1256, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.ts", + "line": " window.dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();", + "lineNumber": 179, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.ts", + "line": " window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');", + "lineNumber": 181, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.ts", + "line": " window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", + "lineNumber": 183, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.ts", + "line": " window.dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');", + "lineNumber": 185, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.ts", + "line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);", + "lineNumber": 987, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/MessageReceiver.ts", + "line": " const buffer = window.dcodeIO.ByteBuffer.wrap(ciphertext);", + "lineNumber": 1016, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SendMessage.js", + "line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();", + "lineNumber": 25, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SendMessage.js", + "line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", + "lineNumber": 28, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SendMessage.ts", + "line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();", + "lineNumber": 29, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SendMessage.ts", + "line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", + "lineNumber": 32, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.js", + "line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));", + "lineNumber": 27, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.js", + "line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));", + "lineNumber": 29, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.js", + "line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))", + "lineNumber": 31, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.js", + "line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));", + "lineNumber": 34, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.ts", + "line": " wrap(sender.sendRequestConfigurationSyncMessage(sendOptions));", + "lineNumber": 42, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.ts", + "line": " wrap(sender.sendRequestBlockSyncMessage(sendOptions));", + "lineNumber": 45, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.ts", + "line": " wrap(sender.sendRequestContactSyncMessage(sendOptions))", + "lineNumber": 48, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" + }, + { + "rule": "jQuery-wrap(", + "path": "ts/textsecure/SyncRequest.ts", + "line": " return wrap(sender.sendRequestGroupSyncMessage(sendOptions));", + "lineNumber": 51, + "reasonCategory": "falseMatch", + "updated": "2020-04-05T23:45:16.746Z" } ] diff --git a/ts/window.d.ts b/ts/window.d.ts index 68c15d4a70..adf8e502e8 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -1,17 +1,32 @@ // Captures the globals put in place by preload.js, background.js and others +import { + LibSignalType, + SignalProtocolAddressClass, + StorageType, +} from './libsignal.d'; +import { TextSecureType } from './textsecure.d'; +import { WebAPIConnectType } from './textsecure/WebAPI'; +import * as Crypto from './Crypto'; + declare global { interface Window { dcodeIO: DCodeIOType; getExpiration: () => string; getEnvironment: () => string; getSocketStatus: () => number; + libphonenumber: { + util: { + getRegionCodeForNumber: (number: string) => string; + }; + }; libsignal: LibSignalType; log: { info: LoggerType; warn: LoggerType; error: LoggerType; }; + normalizeUuids: (obj: any, paths: Array, context: string) => any; restart: () => void; storage: { put: (key: string, value: any) => void; @@ -20,12 +35,32 @@ declare global { }; textsecure: TextSecureType; + Signal: { + Crypto: typeof Crypto; + Metadata: { + SecretSessionCipher: typeof SecretSessionCipherClass; + createCertificateValidator: ( + trustRoot: ArrayBuffer + ) => CertificateValidatorType; + }; + }; ConversationController: ConversationControllerType; + WebAPI: WebAPIConnectType; Whisper: WhisperType; } } +export type ConversationType = { + updateE164: (e164?: string) => void; + updateUuid: (uuid?: string) => void; + id: string; +}; + export type ConversationControllerType = { + getOrCreateAndWait: ( + identifier: string, + type: 'private' | 'group' + ) => Promise; getConversationId: (identifier: string) => string | null; prepareForSend: ( id: string, @@ -34,65 +69,65 @@ export type ConversationControllerType = { wrap: (promise: Promise) => Promise; sendOptions: Object; }; + get: ( + identifier: string + ) => null | { + get: (key: string) => any; + }; }; export type DCodeIOType = { - ByteBuffer: { - wrap: ( - value: any, - type?: string - ) => { - toString: (type: string) => string; - toArrayBuffer: () => ArrayBuffer; - }; + ByteBuffer: typeof ByteBufferClass; + Long: { + fromBits: (low: number, high: number, unsigned: boolean) => number; }; }; -export type LibSignalType = { - KeyHelper: { - generateIdentityKeyPair: () => Promise<{ - privKey: ArrayBuffer; - pubKey: ArrayBuffer; - }>; - }; - Curve: { - async: { - calculateAgreement: ( - publicKey: ArrayBuffer, - privateKey: ArrayBuffer - ) => Promise; - }; - }; - HKDF: { - deriveSecrets: ( - packKey: ArrayBuffer, - salt: ArrayBuffer, - info: ArrayBuffer - ) => Promise>; - }; -}; +export class CertificateValidatorType { + validate: (cerficate: any, certificateTime: number) => Promise; +} + +export class SecretSessionCipherClass { + constructor(storage: StorageType); + decrypt: ( + validator: CertificateValidatorType, + ciphertext: ArrayBuffer, + serverTimestamp: number, + me: any + ) => Promise<{ + isMe: boolean; + sender: SignalProtocolAddressClass; + senderUuid: SignalProtocolAddressClass; + content: ArrayBuffer; + }>; + getRemoteRegistrationId: ( + address: SignalProtocolAddressClass + ) => Promise; + closeOpenSessionForDevice: ( + address: SignalProtocolAddressClass + ) => Promise; + encrypt: ( + address: SignalProtocolAddressClass, + senderCertificate: any, + plaintext: ArrayBuffer | Uint8Array + ) => Promise; +} + +export class ByteBufferClass { + constructor(value?: any, encoding?: string); + static wrap: (value: any, type?: string) => ByteBufferClass; + toString: (type: string) => string; + toArrayBuffer: () => ArrayBuffer; + slice: (start: number, end?: number) => ByteBufferClass; + append: (data: ArrayBuffer) => void; + limit: number; + offset: 0; + readVarint32: () => number; + skip: (length: number) => void; +} export type LoggerType = (...args: Array) => void; -export type TextSecureType = { - storage: { - user: { - getNumber: () => string; - }; - get: (key: string) => any; - }; - messaging: { - sendStickerPackSync: ( - operations: Array<{ - packId: string; - packKey: string; - installed: boolean; - }>, - options: Object - ) => Promise; - }; -}; - export type WhisperType = { events: { trigger: (name: string, param1: any, param2: any) => void; diff --git a/tslint.json b/tslint.json index 868e0584ba..30b4206794 100644 --- a/tslint.json +++ b/tslint.json @@ -15,6 +15,12 @@ "import-spacing": false, "indent": [true, "spaces", 2], "interface-name": [true, "never-prefix"], + "member-access": false, + "member-ordering": false, + "newline-before-return": false, + "prefer-for-of": false, + "no-this-assignment": false, + "binary-expression-operand-order": false, // Allows us to write inline `style`s. Revisit when we have a more sophisticated // CSS-in-JS solution: @@ -98,6 +104,7 @@ true, { "function-regex": "^_?[a-z][\\w\\d]+$", + "method-regex": "^_?[a-z][\\w\\d]+$", "static-method-regex": "^_?[a-z][\\w\\d]+$" } ],