From b405b150cc29def9a8db2c21aff37f33654eadb4 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Wed, 15 Jan 2020 13:11:36 +1100 Subject: [PATCH 1/3] Sealed sender support --- js/modules/metadata/SecretSessionCipher.js | 43 +++--------------- libtextsecure/outgoing_message.js | 51 +++++++++++++++++----- preload.js | 2 + protos/UnidentifiedDelivery.proto | 13 ++---- 4 files changed, 52 insertions(+), 57 deletions(-) diff --git a/js/modules/metadata/SecretSessionCipher.js b/js/modules/metadata/SecretSessionCipher.js index c3aad2538..ac2682a6d 100644 --- a/js/modules/metadata/SecretSessionCipher.js +++ b/js/modules/metadata/SecretSessionCipher.js @@ -102,37 +102,17 @@ function _createServerCertificateFromBuffer(serialized) { // public SenderCertificate(byte[] serialized) function _createSenderCertificateFromBuffer(serialized) { - const wrapper = textsecure.protobuf.SenderCertificate.decode(serialized); + const cert = textsecure.protobuf.SenderCertificate.decode(serialized); - if (!wrapper.signature || !wrapper.certificate) { - throw new Error('Missing fields'); - } - - const certificate = textsecure.protobuf.SenderCertificate.Certificate.decode( - wrapper.certificate.toArrayBuffer() - ); - - if ( - !certificate.signer || - !certificate.identityKey || - !certificate.senderDevice || - !certificate.expires || - !certificate.sender - ) { + if (!cert.senderDevice || !cert.sender) { throw new Error('Missing fields'); } return { - sender: certificate.sender, - senderDevice: certificate.senderDevice, - expires: certificate.expires.toNumber(), - identityKey: certificate.identityKey.toArrayBuffer(), - signer: _createServerCertificateFromBuffer( - certificate.signer.toArrayBuffer() - ), + sender: cert.sender, + senderDevice: cert.senderDevice, - certificate: wrapper.certificate.toArrayBuffer(), - signature: wrapper.signature.toArrayBuffer(), + certificate: cert.toArrayBuffer(), serialized, }; @@ -257,9 +237,7 @@ function _createUnidentifiedSenderMessageContent( ) { const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message(); innerMessage.type = _getProtoMessageType(type); - innerMessage.senderCertificate = textsecure.protobuf.SenderCertificate.decode( - senderCertificate.serialized - ); + innerMessage.senderCertificate = senderCertificate; innerMessage.content = content; return { @@ -392,15 +370,6 @@ SecretSessionCipher.prototype = { messageBytes ); - await validator.validate(content.senderCertificate, timestamp); - if ( - !constantTimeEqual(content.senderCertificate.identityKey, staticKeyBytes) - ) { - throw new Error( - "Sender's certificate key does not match key used in message" - ); - } - const { sender, senderDevice } = content.senderCertificate; const { number, deviceId } = me || {}; if (sender === number && senderDevice === deviceId) { diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index f1c7dd93a..991501dde 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -429,16 +429,47 @@ OutgoingMessage.prototype = { options.messageKeysLimit = false; } - ciphers[address.getDeviceId()] = sessionCipher; + let content; + let type; + let destinationRegistrationId; - // Encrypt our plain text - const ciphertext = await sessionCipher.encrypt(plaintext); - if (!enableFallBackEncryption) { - // eslint-disable-next-line no-param-reassign - ciphertext.body = new Uint8Array( - dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer() + if (window.lokiFeatureFlags.useSealedSender) { + const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( + textsecure.storage.protocol ); + ciphers[address.getDeviceId()] = secretSessionCipher; + + var senderCert = new textsecure.protobuf.SenderCertificate(); + + senderCert.sender = ourKey; + senderCert.senderDevice = deviceId; + + const ciphertext = await secretSessionCipher.encrypt( + address, + senderCert, + plaintext + ); + + type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER; + content = window.Signal.Crypto.arrayBufferToBase64(ciphertext); + + destinationRegistrationId = null; + } else { + ciphers[address.getDeviceId()] = sessionCipher; + + const ciphertext = await sessionCipher.encrypt(plaintext); + if (!enableFallBackEncryption) { + // eslint-disable-next-line no-param-reassign + ciphertext.body = new Uint8Array( + dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer() + ); + } + + type = ciphertext.type; + content = ciphertext.body; + destinationRegistrationId = ciphertext.registrationId; } + const getTTL = type => { switch (type) { case 'friend-request': @@ -458,12 +489,12 @@ OutgoingMessage.prototype = { const ttl = getTTL(thisDeviceMessageType); return { - type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST + type, // FallBackSessionCipher sets this to FRIEND_REQUEST ttl, ourKey, sourceDevice: 1, - destinationRegistrationId: ciphertext.registrationId, - content: ciphertext.body, + destinationRegistrationId, + content, pubKey: devicePubKey, }; }) diff --git a/preload.js b/preload.js index 6598e7874..c5e887ae0 100644 --- a/preload.js +++ b/preload.js @@ -495,10 +495,12 @@ window.pubkeyPattern = /@[a-fA-F0-9]{64,66}\b/g; // Limited due to the proof-of-work requirement window.SMALL_GROUP_SIZE_LIMIT = 10; +// TODO: activate SealedSender once it is ready on all platforms window.lokiFeatureFlags = { multiDeviceUnpairing: true, privateGroupChats: false, useSnodeProxy: false, + useSealedSender: false, }; // eslint-disable-next-line no-extend-native,func-names diff --git a/protos/UnidentifiedDelivery.proto b/protos/UnidentifiedDelivery.proto index da9295aa6..491bd0c16 100644 --- a/protos/UnidentifiedDelivery.proto +++ b/protos/UnidentifiedDelivery.proto @@ -13,17 +13,10 @@ message ServerCertificate { optional bytes signature = 2; } +// This should perhaps be renamed to something like `SenderInfo` message SenderCertificate { - message Certificate { - optional string sender = 1; - optional uint32 senderDevice = 2; - optional fixed64 expires = 3; - optional bytes identityKey = 4; - optional ServerCertificate signer = 5; - } - - optional bytes certificate = 1; - optional bytes signature = 2; + optional string sender = 1; + optional uint32 senderDevice = 2; } message UnidentifiedSenderMessage { From b7e93ab59782fd7c18c453bb6f16b5e0c4e77ac7 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Mon, 20 Jan 2020 16:38:42 +1100 Subject: [PATCH 2/3] 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; From 3472a1a938583602c100908622f6c60bf23135bb Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Wed, 29 Jan 2020 12:11:15 +1100 Subject: [PATCH 3/3] Remove unused destinationRegistrationId; lint --- config/swarm-testing.json | 21 ++++++++++----------- config/swarm-testing2.json | 21 ++++++++++----------- js/views/create_group_dialog_view.js | 2 +- libloki/api.js | 1 - libtextsecure/outgoing_message.js | 5 ----- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/config/swarm-testing.json b/config/swarm-testing.json index 9bdd086d9..9df9b3340 100644 --- a/config/swarm-testing.json +++ b/config/swarm-testing.json @@ -1,12 +1,11 @@ { - "storageProfile": "swarm-testing", - "seedNodeList": [ - { - "ip": "localhost", - "port": "22129" - } - ], - "openDevTools": true, - "defaultPublicChatServer": "https://team-chat.lokinet.org/" - } - \ No newline at end of file + "storageProfile": "swarm-testing", + "seedNodeList": [ + { + "ip": "localhost", + "port": "22129" + } + ], + "openDevTools": true, + "defaultPublicChatServer": "https://team-chat.lokinet.org/" +} diff --git a/config/swarm-testing2.json b/config/swarm-testing2.json index e3799a713..5469d9dab 100644 --- a/config/swarm-testing2.json +++ b/config/swarm-testing2.json @@ -1,12 +1,11 @@ { - "storageProfile": "swarm-testing2", - "seedNodeList": [ - { - "ip": "localhost", - "port": "22129" - } - ], - "openDevTools": true, - "defaultPublicChatServer": "https://team-chat.lokinet.org/" - } - \ No newline at end of file + "storageProfile": "swarm-testing2", + "seedNodeList": [ + { + "ip": "localhost", + "port": "22129" + } + ], + "openDevTools": true, + "defaultPublicChatServer": "https://team-chat.lokinet.org/" +} diff --git a/js/views/create_group_dialog_view.js b/js/views/create_group_dialog_view.js index a2863e061..8b5864e1b 100644 --- a/js/views/create_group_dialog_view.js +++ b/js/views/create_group_dialog_view.js @@ -110,7 +110,7 @@ let existingMembers = groupConvo.get('members'); // Show a contact if they are our friend or if they are a member - let friendsAndMembers = convos.filter( + const friendsAndMembers = convos.filter( d => (d.isFriend() || existingMembers.includes(d.id)) && d.isPrivate() && diff --git a/libloki/api.js b/libloki/api.js index 54a7a2100..858a247de 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -226,7 +226,6 @@ // Send const options = { messageType: 'pairing-request' }; const p = new Promise((resolve, reject) => { - const timestamp = Date.now(); const outgoingMessage = new textsecure.OutgoingMessage( diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 5761253aa..a133c0bce 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -414,7 +414,6 @@ OutgoingMessage.prototype = { let content; let type; - let destinationRegistrationId; if (window.lokiFeatureFlags.useSealedSender) { const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( @@ -436,8 +435,6 @@ OutgoingMessage.prototype = { type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER; content = window.Signal.Crypto.arrayBufferToBase64(ciphertext); - - destinationRegistrationId = null; } else { // TODO: probably remove this branch once // mobile clients implement sealed sender @@ -454,7 +451,6 @@ OutgoingMessage.prototype = { // eslint-disable-next-line prefer-destructuring type = ciphertext.type; content = ciphertext.body; - destinationRegistrationId = ciphertext.registrationId; } const ttl = getTTLForType(thisDeviceMessageType); @@ -464,7 +460,6 @@ OutgoingMessage.prototype = { ttl, ourKey, sourceDevice: 1, - destinationRegistrationId, content, pubKey: devicePubKey, };