From b7e93ab59782fd7c18c453bb6f16b5e0c4e77ac7 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Mon, 20 Jan 2020 16:38:42 +1100 Subject: [PATCH] Support sealed sender for friend requests --- js/modules/metadata/CiphertextMessage.js | 2 + js/modules/metadata/SecretSessionCipher.js | 36 ++++++---- libloki/crypto.js | 1 + libtextsecure/message_receiver.js | 31 +++++---- libtextsecure/outgoing_message.js | 81 +++++++--------------- protos/UnidentifiedDelivery.proto | 1 + 6 files changed, 72 insertions(+), 80 deletions(-) diff --git a/js/modules/metadata/CiphertextMessage.js b/js/modules/metadata/CiphertextMessage.js index f381f3bec..1e2ddb6ea 100644 --- a/js/modules/metadata/CiphertextMessage.js +++ b/js/modules/metadata/CiphertextMessage.js @@ -10,4 +10,6 @@ module.exports = { SENDERKEY_DISTRIBUTION_TYPE: 5, ENCRYPTED_MESSAGE_OVERHEAD: 53, + + LOKI_FRIEND_REQUEST: 101, }; diff --git a/js/modules/metadata/SecretSessionCipher.js b/js/modules/metadata/SecretSessionCipher.js index ac2682a6d..61420a122 100644 --- a/js/modules/metadata/SecretSessionCipher.js +++ b/js/modules/metadata/SecretSessionCipher.js @@ -1,4 +1,4 @@ -/* global libsignal, textsecure */ +/* global libsignal, textsecure, dcodeIO, libloki */ /* eslint-disable no-bitwise */ @@ -199,6 +199,9 @@ function _createUnidentifiedSenderMessageContentFromBuffer(serialized) { case TypeEnum.PREKEY_MESSAGE: type = CiphertextMessage.PREKEY_TYPE; break; + case TypeEnum.LOKI_FRIEND_REQUEST: + type = CiphertextMessage.LOKI_FRIEND_REQUEST; + break; default: throw new Error(`Unknown type: ${message.type}`); } @@ -223,6 +226,8 @@ function _getProtoMessageType(type) { return TypeEnum.MESSAGE; case CiphertextMessage.PREKEY_TYPE: return TypeEnum.PREKEY_MESSAGE; + case CiphertextMessage.LOKI_FRIEND_REQUEST: + return TypeEnum.LOKI_FRIEND_REQUEST; default: throw new Error(`_getProtoMessageType: type '${type}' does not exist`); } @@ -255,24 +260,24 @@ SecretSessionCipher.prototype = { // SenderCertificate senderCertificate, // byte[] paddedPlaintext // ) - async encrypt(destinationAddress, senderCertificate, paddedPlaintext) { + async encrypt( + destinationAddress, + senderCertificate, + paddedPlaintext, + cipher + ) { // Capture this.xxx variables to replicate Java's implicit this syntax - const { SessionCipher } = this; const signalProtocolStore = this.storage; const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this); const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this); const _calculateStaticKeys = this._calculateStaticKeys.bind(this); - const sessionCipher = new SessionCipher( - signalProtocolStore, - destinationAddress - ); - - const message = await sessionCipher.encrypt(paddedPlaintext); + const message = await cipher.encrypt(paddedPlaintext); const ourIdentity = await signalProtocolStore.getIdentityKeyPair(); - const theirIdentity = fromEncodedBinaryToArrayBuffer( - await signalProtocolStore.loadIdentityKey(destinationAddress.getName()) - ); + const theirIdentity = dcodeIO.ByteBuffer.wrap( + destinationAddress.getName(), + 'hex' + ).toArrayBuffer(); const ephemeral = await libsignal.Curve.async.generateKeyPair(); const ephemeralSalt = concatenateBytes( @@ -322,7 +327,7 @@ SecretSessionCipher.prototype = { // public Pair decrypt( // CertificateValidator validator, byte[] ciphertext, long timestamp) - async decrypt(validator, ciphertext, timestamp, me) { + async decrypt(ciphertext, me) { // Capture this.xxx variables to replicate Java's implicit this syntax const signalProtocolStore = this.storage; const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this); @@ -383,6 +388,7 @@ SecretSessionCipher.prototype = { return { sender: address, content: await _decryptWithUnidentifiedSenderMessage(content), + type: content.type, }; } catch (error) { if (!error) { @@ -488,6 +494,10 @@ SecretSessionCipher.prototype = { signalProtocolStore, sender ).decryptPreKeyWhisperMessage(message.content); + case CiphertextMessage.LOKI_FRIEND_REQUEST: + return new libloki.crypto.FallBackSessionCipher(sender).decrypt( + message.content + ); default: throw new Error(`Unknown type: ${message.type}`); } diff --git a/libloki/crypto.js b/libloki/crypto.js index c62468bd4..f9888d70e 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -45,6 +45,7 @@ this.pubKey = StringView.hexToArrayBuffer(address.getName()); } + // Should we use ephemeral key pairs here rather than long term keys on each side? async encrypt(plaintext) { const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); const myPrivateKey = myKeyPair.privKey; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e651de6f0..bd2ea72b8 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -714,8 +714,6 @@ MessageReceiver.prototype.extend({ return plaintext; }, async decrypt(envelope, ciphertext) { - const { serverTrustRoot } = this; - let promise; const address = new libsignal.SignalProtocolAddress( envelope.source, @@ -867,15 +865,10 @@ MessageReceiver.prototype.extend({ case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: window.log.info('received unidentified sender message'); promise = secretSessionCipher - .decrypt( - window.Signal.Metadata.createCertificateValidator(serverTrustRoot), - ciphertext.toArrayBuffer(), - Math.min(envelope.serverTimestamp || Date.now(), Date.now()), - me - ) + .decrypt(ciphertext.toArrayBuffer(), me) .then( result => { - const { isMe, sender, content } = result; + const { isMe, sender, content, type } = result; // We need to drop incoming messages from ourself since server can't // do it for us @@ -883,6 +876,13 @@ MessageReceiver.prototype.extend({ return { isMe: true }; } + // We might have substituted the type based on decrypted content + if (type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) { + // eslint-disable-next-line no-param-reassign + envelope.type = + textsecure.protobuf.Envelope.Type.FRIEND_REQUEST; + } + if (this.isBlocked(sender.getName())) { window.log.info( 'Dropping blocked message after sealed sender decryption' @@ -903,7 +903,7 @@ MessageReceiver.prototype.extend({ envelope.unidentifiedDeliveryReceived = !originalSource; // Return just the content because that matches the signature of the other - // decrypt methods used above. + // decrypt methods used above. return this.unpad(content); }, error => { @@ -933,7 +933,8 @@ MessageReceiver.prototype.extend({ throw error; }); } - ); + ) + .then(handleSessionReset); break; default: promise = Promise.reject(new Error('Unknown message type')); @@ -946,6 +947,9 @@ MessageReceiver.prototype.extend({ this.removeFromCache(envelope); return null; } + + // Type here can actually be UNIDENTIFIED_SENDER even if + // the underlying message is FRIEND_REQUEST if ( envelope.type !== textsecure.protobuf.Envelope.Type.FRIEND_REQUEST ) { @@ -975,7 +979,10 @@ MessageReceiver.prototype.extend({ let errorToThrow = error; const noSession = - error && error.message.indexOf('No record for device') === 0; + error && + (error.message.indexOf('No record for device') === 0 || + error.messaeg.indexOf('decryptWithSessionList: list is empty') === + 0); if (error && error.message === 'Unknown identity key') { // create an error that the UI will pick up and ask the diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 991501dde..5761253aa 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -14,6 +14,23 @@ /* eslint-disable no-unreachable */ const NUM_SEND_CONNECTIONS = 3; +const getTTLForType = type => { + switch (type) { + case 'friend-request': + return 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message + 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 'typing': + return 60 * 1000; // 1 minute for typing indicators + case 'pairing-request': + return 2 * 60 * 1000; // 2 minutes for pairing requests + default: + return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message + } +}; + function OutgoingMessage( server, timestamp, @@ -289,36 +306,6 @@ OutgoingMessage.prototype = { this.numbers = devicesPubKeys; - /* Disabled because i'm not sure how senderCertificate works :thinking: - const { numberInfo, senderCertificate } = this; - const info = numberInfo && numberInfo[number] ? numberInfo[number] : {}; - const { accessKey } = info || {}; - - if (accessKey && !senderCertificate) { - return Promise.reject( - new Error( - 'OutgoingMessage.doSendMessage: accessKey was provided, ' + - 'but senderCertificate was not' - ) - ); - } - - const sealedSender = Boolean(accessKey && senderCertificate); - - // We don't send to ourselves if unless sealedSender is enabled - const ourNumber = textsecure.storage.user.getNumber(); - const ourDeviceId = textsecure.storage.user.getDeviceId(); - if (number === ourNumber && !sealedSender) { - // eslint-disable-next-line no-param-reassign - deviceIds = _.reject( - deviceIds, - deviceId => - // because we store our own device ID as a string at least sometimes - deviceId === ourDeviceId || deviceId === parseInt(ourDeviceId, 10) - ); - } - */ - return Promise.all( devicesPubKeys.map(async devicePubKey => { // Session Messenger doesn't use the deviceId scheme, it's always 1. @@ -339,9 +326,6 @@ OutgoingMessage.prototype = { ); const ourKey = textsecure.storage.user.getNumber(); const options = {}; - const fallBackCipher = new libloki.crypto.FallBackSessionCipher( - address - ); let isMultiDeviceRequest = false; let thisDeviceMessageType = this.messageType; @@ -387,8 +371,7 @@ OutgoingMessage.prototype = { flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; const signalCipher = new libsignal.SessionCipher( textsecure.storage.protocol, - address, - options + address ); if (enableFallBackEncryption || isEndSession) { // Encrypt them with the fallback @@ -418,7 +401,7 @@ OutgoingMessage.prototype = { } if (enableFallBackEncryption) { - sessionCipher = fallBackCipher; + sessionCipher = new libloki.crypto.FallBackSessionCipher(address); } else { sessionCipher = signalCipher; } @@ -439,7 +422,7 @@ OutgoingMessage.prototype = { ); ciphers[address.getDeviceId()] = secretSessionCipher; - var senderCert = new textsecure.protobuf.SenderCertificate(); + const senderCert = new textsecure.protobuf.SenderCertificate(); senderCert.sender = ourKey; senderCert.senderDevice = deviceId; @@ -447,7 +430,8 @@ OutgoingMessage.prototype = { const ciphertext = await secretSessionCipher.encrypt( address, senderCert, - plaintext + plaintext, + sessionCipher ); type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER; @@ -455,6 +439,8 @@ OutgoingMessage.prototype = { destinationRegistrationId = null; } else { + // TODO: probably remove this branch once + // mobile clients implement sealed sender ciphers[address.getDeviceId()] = sessionCipher; const ciphertext = await sessionCipher.encrypt(plaintext); @@ -465,28 +451,13 @@ OutgoingMessage.prototype = { ); } + // eslint-disable-next-line prefer-destructuring type = ciphertext.type; content = ciphertext.body; destinationRegistrationId = ciphertext.registrationId; } - const getTTL = type => { - switch (type) { - case 'friend-request': - return 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message - 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 'typing': - return 60 * 1000; // 1 minute for typing indicators - case 'pairing-request': - return 2 * 60 * 1000; // 2 minutes for pairing requests - default: - return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message - } - }; - const ttl = getTTL(thisDeviceMessageType); + const ttl = getTTLForType(thisDeviceMessageType); return { type, // FallBackSessionCipher sets this to FRIEND_REQUEST diff --git a/protos/UnidentifiedDelivery.proto b/protos/UnidentifiedDelivery.proto index 491bd0c16..9b49b8c9b 100644 --- a/protos/UnidentifiedDelivery.proto +++ b/protos/UnidentifiedDelivery.proto @@ -25,6 +25,7 @@ message UnidentifiedSenderMessage { enum Type { PREKEY_MESSAGE = 1; MESSAGE = 2; + LOKI_FRIEND_REQUEST = 3; } optional Type type = 1;