From 9bfccd408f97d6eaff2710d3460cf832aafcbf05 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 14 Jul 2020 13:29:53 +1000 Subject: [PATCH] rm-outgoing-message --- Gruntfile.js | 1 - js/modules/metadata/SecretSessionCipher.js | 10 - libloki/test/index.html | 1 - libtextsecure/outgoing_message.js | 618 --------------------- 4 files changed, 630 deletions(-) delete mode 100644 libtextsecure/outgoing_message.js diff --git a/Gruntfile.js b/Gruntfile.js index 0ad59c44e..3966d5852 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -90,7 +90,6 @@ module.exports = grunt => { 'libtextsecure/websocket-resources.js', 'libtextsecure/http-resources.js', 'libtextsecure/message_receiver.js', - 'libtextsecure/outgoing_message.js', 'libtextsecure/sendmessage.js', 'libtextsecure/sync_request.js', 'libtextsecure/contacts_parser.js', diff --git a/js/modules/metadata/SecretSessionCipher.js b/js/modules/metadata/SecretSessionCipher.js index 71ef1df69..f9b868943 100644 --- a/js/modules/metadata/SecretSessionCipher.js +++ b/js/modules/metadata/SecretSessionCipher.js @@ -411,16 +411,6 @@ SecretSessionCipher.prototype = { return cipher.getRemoteRegistrationId(); }, - // Used by outgoing_message.js - closeOpenSessionForDevice(remoteAddress) { - const { SessionCipher } = this; - const signalProtocolStore = this.storage; - - const cipher = new SessionCipher(signalProtocolStore, remoteAddress); - - return cipher.closeOpenSessionForDevice(); - }, - // private EphemeralKeys calculateEphemeralKeys( // ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt) async _calculateEphemeralKeys(ephemeralPublic, ephemeralPrivate, salt) { diff --git a/libloki/test/index.html b/libloki/test/index.html index dc6d74e2e..f18213d6f 100644 --- a/libloki/test/index.html +++ b/libloki/test/index.html @@ -23,7 +23,6 @@ - diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js deleted file mode 100644 index 0dd3ed43a..000000000 --- a/libtextsecure/outgoing_message.js +++ /dev/null @@ -1,618 +0,0 @@ -/* global - textsecure, - libsignal, - window, - libloki, - StringView, - lokiMessageAPI, - log -*/ - -/* eslint-disable more/no-then */ -/* eslint-disable no-unreachable */ -const NUM_SEND_CONNECTIONS = 3; - -const getTTLForType = type => { - switch (type) { - case 'device-unpairing': - return 4 * 24 * 60 * 60 * 1000; // 4 days for device unpairing - case 'onlineBroadcast': - return 60 * 1000; // 1 minute for online broadcast message - case 'pairing-request': - return 2 * 60 * 1000; // 2 minutes for pairing requests - default: - return 24 * 60 * 60 * 1000; // 1 day default for any other message - } -}; - -function _getPaddedMessageLength(messageLength) { - const messageLengthWithTerminator = messageLength + 1; - let messagePartCount = Math.floor(messageLengthWithTerminator / 160); - - if (messageLengthWithTerminator % 160 !== 0) { - messagePartCount += 1; - } - - return messagePartCount * 160; -} - -function _convertMessageToText(messageBuffer) { - const plaintext = new Uint8Array( - _getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 - ); - plaintext.set(new Uint8Array(messageBuffer)); - plaintext[messageBuffer.byteLength] = 0x80; - - return plaintext; -} - -function _getPlaintext(messageBuffer) { - return _convertMessageToText(messageBuffer); -} - -function wrapInWebsocketMessage(outgoingObject, timestamp) { - const source = - outgoingObject.type === - textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER - ? null - : outgoingObject.ourKey; - - const messageEnvelope = new textsecure.protobuf.Envelope({ - type: outgoingObject.type, - source, - sourceDevice: outgoingObject.sourceDevice, - timestamp, - content: outgoingObject.content, - }); - const requestMessage = new textsecure.protobuf.WebSocketRequestMessage({ - id: new Uint8Array(libsignal.crypto.getRandomBytes(1))[0], // random ID for now - verb: 'PUT', - path: '/api/v1/message', - body: messageEnvelope.encode().toArrayBuffer(), - }); - const websocketMessage = new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: requestMessage, - }); - const bytes = new Uint8Array(websocketMessage.encode().toArrayBuffer()); - return bytes; -} - -function getStaleDeviceIdsForNumber(number) { - return textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => { - if (deviceIds.length === 0) { - return [1]; - } - const updateDevices = []; - return Promise.all( - deviceIds.map(deviceId => { - const address = new libsignal.SignalProtocolAddress(number, deviceId); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - return sessionCipher.hasOpenSession().then(hasSession => { - if (!hasSession) { - updateDevices.push(deviceId); - } - }); - }) - ).then(() => updateDevices); - }); -} - -function OutgoingMessage( - server, - timestamp, - numbers, - 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; - } - this.server = server; - this.timestamp = timestamp; - this.numbers = numbers; - this.message = message; // ContentMessage proto - this.callback = callback; - this.silent = silent; - - this.numbersCompleted = 0; - this.errors = []; - this.successfulNumbers = []; - this.fallBackEncryption = false; - this.failoverNumbers = []; - this.unidentifiedDeliveries = []; - - const { - numberInfo, - senderCertificate, - online, - messageType, - isPublic, - isMediumGroup, - publicSendData, - autoSession, - } = options || {}; - this.numberInfo = numberInfo; - this.isPublic = isPublic; - this.isMediumGroup = !!isMediumGroup; - this.isGroup = !!( - this.message && - this.message.dataMessage && - this.message.dataMessage.group - ); - this.publicSendData = publicSendData; - this.senderCertificate = senderCertificate; - this.online = online; - this.messageType = messageType || 'outgoing'; - this.autoSession = autoSession || false; -} - -OutgoingMessage.prototype = { - constructor: OutgoingMessage, - numberCompleted() { - this.numbersCompleted += 1; - if (this.numbersCompleted >= this.numbers.length) { - this.callback({ - successfulNumbers: this.successfulNumbers, - failoverNumbers: this.failoverNumbers, - errors: this.errors, - unidentifiedDeliveries: this.unidentifiedDeliveries, - messageType: this.messageType, - }); - } - }, - registerError(number, reason, error) { - if (!error || (error.name === 'HTTPError' && error.code !== 404)) { - // eslint-disable-next-line no-param-reassign - error = new textsecure.OutgoingMessageError( - number, - this.message.toArrayBuffer(), - this.timestamp, - error - ); - } - - // eslint-disable-next-line no-param-reassign - error.number = number; - // eslint-disable-next-line no-param-reassign - error.reason = reason; - this.errors[this.errors.length] = error; - this.numberCompleted(); - }, - reloadDevicesAndSend(primaryPubKey, multiDevice = true) { - const ourNumber = textsecure.storage.user.getNumber(); - - if (!multiDevice) { - if (primaryPubKey === ourNumber) { - return Promise.resolve(); - } - - return this.doSendMessage(primaryPubKey, [primaryPubKey]); - } - - return ( - window.libsession.Protocols.MultiDeviceProtocol.getAllDevices( - primaryPubKey - ) - // Don't send to ourselves - .then(devicesPubKeys => - devicesPubKeys.filter(pubKey => pubKey.key !== ourNumber) - ) - .then(devicesPubKeys => { - if (devicesPubKeys.length === 0) { - // No need to start the sending of message without a recipient - return Promise.resolve(); - } - return this.doSendMessage(primaryPubKey, devicesPubKeys); - }) - ); - }, - - getKeysForNumber(number, updateDevices) { - const handleResult = response => - Promise.all( - response.devices.map(device => { - // eslint-disable-next-line no-param-reassign - device.identityKey = response.identityKey; - if ( - updateDevices === undefined || - updateDevices.indexOf(device.deviceId) > -1 - ) { - const address = new libsignal.SignalProtocolAddress( - number, - device.deviceId - ); - const builder = new libsignal.SessionBuilder( - textsecure.storage.protocol, - address - ); - if (device.registrationId === 0) { - window.log.info('device registrationId 0!'); - } - return builder - .processPreKey(device) - .then(async () => { - // TODO: only remove the keys that were used above! - await libloki.storage.removeContactPreKeyBundle(number); - return true; - }) - .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; - } - throw error; - }); - } - - return false; - }) - ); - let promise = Promise.resolve(true); - updateDevices.forEach(device => { - promise = promise.then(() => - Promise.all([ - textsecure.storage.protocol.loadContactPreKey(number), - textsecure.storage.protocol.loadContactSignedPreKey(number), - ]) - .then(keys => { - const [preKey, signedPreKey] = keys; - if (preKey === undefined || signedPreKey === undefined) { - return false; - } - const identityKey = StringView.hexToArrayBuffer(number); - return handleResult({ - identityKey, - devices: [ - { deviceId: device, preKey, signedPreKey, registrationId: 0 }, - ], - }).then(results => results.every(value => value === true)); - }) - .catch(e => { - throw e; - }) - ); - }); - - return promise; - }, - - // Default ttl to 24 hours if no value provided - async transmitMessage(number, data, timestamp, ttl = 24 * 60 * 60 * 1000) { - const pubKey = number; - - try { - // TODO: Make NUM_CONCURRENT_CONNECTIONS a global constant - const options = { - numConnections: NUM_SEND_CONNECTIONS, - }; - options.isPublic = this.isPublic; - if (this.isPublic) { - options.publicSendData = this.publicSendData; - } - await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); - } catch (e) { - if (e.name === 'HTTPError' && e.code !== 409 && e.code !== 410) { - throw new textsecure.SendMessageNetworkError(number, '', e, timestamp); - } else if (e.name === 'TimedOutError') { - throw new textsecure.PoWError(number, e); - } - throw e; - } - }, - - async buildMessage(devicePubKey) { - const updatedDevices = await getStaleDeviceIdsForNumber(devicePubKey); - const keysFound = await this.getKeysForNumber(devicePubKey, updatedDevices); - - // Check if we need to attach the preKeys - const enableFallBackEncryption = !keysFound; - const flags = this.message.dataMessage - ? this.message.dataMessage.get_flags() - : null; - // END_SESSION means Session reset message - const isEndSession = - flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; - const isSessionRequest = false; - - if (enableFallBackEncryption || isEndSession) { - // Encrypt them with the fallback - const pkb = await libloki.storage.getPreKeyBundleForContact(devicePubKey); - this.message.preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage( - pkb - ); - window.log.info('attaching prekeys to outgoing message'); - } - - const messageBuffer = this.message.toArrayBuffer(); - const logDetails = { - message: this.message, - }; - - const ourPubKey = textsecure.storage.user.getNumber(); - const ourPrimaryPubkey = window.storage.get('primaryDevicePubKey'); - const secondaryPubKeys = - (await window.libsession.Protocols.MultiDeviceProtocol.getSecondaryDevices( - ourPubKey - )) || []; - let aliasedPubkey = devicePubKey; - if (devicePubKey === ourPubKey) { - aliasedPubkey = 'OUR_PUBKEY'; // should not happen - } else if (devicePubKey === ourPrimaryPubkey) { - aliasedPubkey = 'OUR_PRIMARY_PUBKEY'; - } else if (secondaryPubKeys.some(device => device.key === devicePubKey)) { - aliasedPubkey = 'OUR SECONDARY PUBKEY'; - } - libloki.api.debug.logSessionMessageSending( - `Sending :${this.messageType} message to ${aliasedPubkey} details:`, - logDetails - ); - - const plaintext = _getPlaintext(messageBuffer); - - // No limit on message keys if we're communicating with our other devices - // FIXME options not used at all; if (ourPubkey === number) { - // options.messageKeysLimit = false; - // } - const ttl = getTTLForType(this.messageType); - const ourKey = textsecure.storage.user.getNumber(); - - return { - ttl, - ourKey, - sourceDevice: 1, - plaintext, - pubKey: devicePubKey, - isSessionRequest, - enableFallBackEncryption, - }; - }, - - async encryptMessage(clearMessage) { - if (clearMessage === null) { - window.log.warn( - 'clearMessage is null on encryptMessage... Returning null' - ); - return null; - } - const { - ttl, - ourKey, - sourceDevice, - plaintext, - pubKey, - isSessionRequest, - enableFallBackEncryption, - } = clearMessage; - // Session doesn't use the deviceId scheme, it's always 1. - // Instead, there are multiple device public keys. - const deviceId = 1; - - const address = new libsignal.SignalProtocolAddress(pubKey, deviceId); - - let sessionCipher; - - if (enableFallBackEncryption) { - sessionCipher = new libloki.crypto.FallBackSessionCipher(address); - } else { - sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - } - - const innerCiphertext = await sessionCipher.encrypt(plaintext); - - const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( - textsecure.storage.protocol - ); - - const senderCert = new textsecure.protobuf.SenderCertificate(); - - senderCert.sender = ourKey; - senderCert.senderDevice = deviceId; - - const ciphertext = await secretSessionCipher.encrypt( - address.getName(), - senderCert, - innerCiphertext - ); - const type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER; - const content = window.Signal.Crypto.arrayBufferToBase64(ciphertext); - - return { - type, - ttl, - ourKey, - sourceDevice, - content, - pubKey, - isSessionRequest, - }; - }, - // Send a message to a public group - async sendPublicMessage(number) { - await this.transmitMessage( - number, - this.message.dataMessage, - this.timestamp, - 0 // ttl - ); - - this.successfulNumbers[this.successfulNumbers.length] = number; - this.numberCompleted(); - }, - async sendMediumGroupMessage(groupId) { - const ttl = getTTLForType(this.messageType); - - const plaintext = this.message.toArrayBuffer(); - - const ourIdentity = textsecure.storage.user.getNumber(); - - const { - ciphertext, - keyIdx, - } = await window.SenderKeyAPI.encryptWithSenderKey( - plaintext, - groupId, - ourIdentity - ); - - if (!ciphertext) { - log.error('could not encrypt for medium group'); - return; - } - - const source = ourIdentity; - - // We should include ciphertext idx in the message - const content = new textsecure.protobuf.MediumGroupCiphertext({ - ciphertext, - source, - keyIdx, - }); - - // Encrypt for the group's identity key to hide source and key idx: - const { - ciphertext: ciphertextOuter, - ephemeralKey, - } = await libloki.crypto.encryptForPubkey( - groupId, - content.encode().toArrayBuffer() - ); - - const contentOuter = new textsecure.protobuf.MediumGroupContent({ - ciphertext: ciphertextOuter, - ephemeralKey, - }); - - log.debug( - 'Group ciphertext: ', - window.Signal.Crypto.arrayBufferToBase64(ciphertext) - ); - - const outgoingObject = { - type: textsecure.protobuf.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT, - ttl, - ourKey: ourIdentity, - sourceDevice: 1, - content: contentOuter.encode().toArrayBuffer(), - }; - - // TODO: Rather than using sealed sender, we just generate a key pair, perform an ECDH against - // the group's public key and encrypt using the derived key - - const socketMessage = wrapInWebsocketMessage( - outgoingObject, - this.timestamp - ); - - await this.transmitMessage(groupId, socketMessage, this.timestamp, ttl); - - this.successfulNumbers[this.successfulNumbers.length] = groupId; - this.numberCompleted(); - }, - // Send a message to a private group member or a session chat (one to one) - async sendSessionMessage(outgoingObjects) { - // TODO: handle multiple devices/messages per transmit - const promises = outgoingObjects.map(async outgoingObject => { - if (!outgoingObject) { - return; - } - const { pubKey: destination, ttl } = outgoingObject; - - try { - const socketMessage = wrapInWebsocketMessage( - outgoingObject, - this.timestamp - ); - await this.transmitMessage( - destination, - socketMessage, - this.timestamp, - ttl - ); - this.successfulNumbers.push(destination); - } catch (e) { - e.number = destination; - this.errors.push(e); - } - }); - - await Promise.all(promises); - - this.numbersCompleted += this.successfulNumbers.length; - this.numberCompleted(); - }, - async buildAndEncrypt(devicePubKey) { - const clearMessage = await this.buildMessage(devicePubKey); - return this.encryptMessage(clearMessage); - }, - // eslint-disable-next-line no-unused-vars - async doSendMessage(primaryPubKey, devicesPubKeys) { - if (this.isPublic) { - await this.sendPublicMessage(primaryPubKey); - return; - } - this.numbers = devicesPubKeys; - - if (this.isMediumGroup) { - await this.sendMediumGroupMessage(primaryPubKey); - return; - } - - const outgoingObjects = await Promise.all( - devicesPubKeys.map(pk => this.buildAndEncrypt(pk, primaryPubKey)) - ); - - this.sendSessionMessage(outgoingObjects); - }, - - removeDeviceIdsForNumber(number, deviceIdsToRemove) { - let promise = Promise.resolve(); - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const j in deviceIdsToRemove) { - promise = promise.then(() => { - const encodedNumber = `${number}.${deviceIdsToRemove[j]}`; - return textsecure.storage.protocol.removeSession(encodedNumber); - }); - } - return promise; - }, - - sendToNumber(number, multiDevice = true) { - return this.reloadDevicesAndSend(number, multiDevice).catch(error => { - if (error.message === 'Identity key changed') { - // eslint-disable-next-line no-param-reassign - error = new textsecure.OutgoingIdentityKeyError( - number, - error.originalMessage, - error.timestamp, - error.identityKey - ); - this.registerError(number, 'Identity key changed', error); - } else { - this.registerError( - number, - `Failed to retrieve new device keys for number ${number}`, - error - ); - } - }); - }, -}; - -window.textsecure = window.textsecure || {}; -window.textsecure.OutgoingMessage = OutgoingMessage;