diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d31c1dd9e..1f4aa04cc 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1008,6 +1008,12 @@ "message": "Send a message", "description": "Placeholder text in the message entry field" }, + "secondaryDeviceDefaultFR": { + "message": + "This is an automated friend request because you are friends with another one of my devices", + "description": + "Placeholder text in the message entry field when it is disabled because a secondary device conversation is visible" + }, "sendMessageDisabledSecondary": { "message": "This pubkey belongs to a secondary device. You should never see this message", diff --git a/js/models/messages.js b/js/models/messages.js index ba00aab06..e458dcce9 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1980,6 +1980,10 @@ } let autoAccept = false; + const requestConversation = await ConversationController.getOrCreateAndWait( + source, + 'private' + ); if (message.get('type') === 'friend-request') { /* Here is the before and after state diagram for the operation before. @@ -1996,16 +2000,16 @@ - We are friends with the user, and that user just sent us a friend request. */ if ( - conversation.hasSentFriendRequest() || - conversation.isFriend() + requestConversation.hasSentFriendRequest() || + requestConversation.isFriend() ) { // Automatically accept incoming friend requests if we have send one already autoAccept = true; message.set({ friendStatus: 'accepted' }); - await conversation.onFriendRequestAccepted(); + await requestConversation.onFriendRequestAccepted(); window.libloki.api.sendBackgroundMessage(message.get('source')); } else { - await conversation.onFriendRequestReceived(); + await requestConversation.onFriendRequestReceived(); } } else { await conversation.onFriendRequestAccepted(); diff --git a/libloki/api.js b/libloki/api.js index 0dcd53e68..e94f280cb 100644 --- a/libloki/api.js +++ b/libloki/api.js @@ -27,6 +27,13 @@ } async function sendOnlineBroadcastMessage(pubKey, isPing = false) { + const authorisation = await window.libloki.storage.getGrantAuthorisationForSecondaryPubKey( + pubKey + ); + if (authorisation && authorisation.primaryDevicePubKey !== pubKey) { + sendOnlineBroadcastMessage(authorisation.primaryDevicePubKey); + return; + } let p2pAddress = null; let p2pPort = null; let type; diff --git a/libloki/storage.js b/libloki/storage.js index 1cb7fff51..4717331e2 100644 --- a/libloki/storage.js +++ b/libloki/storage.js @@ -179,6 +179,7 @@ if (!conversation || conversation.isPublic() || conversation.isRss()) { return null; } + await saveAllPairingAuthorisationsFor(secondaryPubKey); const authorisation = await window.Signal.Data.getGrantAuthorisationForSecondaryPubKey( secondaryPubKey ); @@ -220,6 +221,7 @@ } async function getAllDevicePubKeysForPrimaryPubKey(primaryDevicePubKey) { + await saveAllPairingAuthorisationsFor(primaryDevicePubKey); const secondaryPubKeys = (await getSecondaryDevicesFor(primaryDevicePubKey)) || []; return secondaryPubKeys.concat(primaryDevicePubKey); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 003c366f6..3556000e5 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1277,6 +1277,7 @@ MessageReceiver.prototype.extend({ deviceMapping ); if (autoAccepted) { + await conversation.onFriendRequestAccepted(); return this.removeFromCache(envelope); } } diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index e098689de..aea4db575 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -6,8 +6,8 @@ libloki, StringView, dcodeIO, - log, lokiMessageAPI, + i18n, */ /* eslint-disable more/no-then */ @@ -236,8 +236,7 @@ OutgoingMessage.prototype = { return messagePartCount * 160; }, - convertMessageToText(message) { - const messageBuffer = message.toArrayBuffer(); + convertMessageToText(messageBuffer) { const plaintext = new Uint8Array( this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 ); @@ -246,11 +245,8 @@ OutgoingMessage.prototype = { return plaintext; }, - getPlaintext() { - if (!this.plaintext) { - this.plaintext = this.convertMessageToText(this.message); - } - return this.plaintext; + getPlaintext(messageBuffer) { + return this.convertMessageToText(messageBuffer); }, async wrapInWebsocketMessage(outgoingObject) { const messageEnvelope = new textsecure.protobuf.Envelope({ @@ -328,6 +324,15 @@ OutgoingMessage.prototype = { // Loki Messenger doesn't use the deviceId scheme, it's always 1. // Instead, there are multiple device public keys. const deviceId = 1; + const updatedDevices = await this.getStaleDeviceIdsForNumber( + devicePubKey + ); + const keysFound = await this.getKeysForNumber( + devicePubKey, + updatedDevices + ); + let enableFallBackEncryption = !keysFound; + const address = new libsignal.SignalProtocolAddress( devicePubKey, deviceId @@ -338,34 +343,73 @@ OutgoingMessage.prototype = { address ); + let isMultiDeviceRequest = false; + let thisDeviceMessageType = this.messageType; + if ( + thisDeviceMessageType !== 'pairing-request' && + thisDeviceMessageType !== 'friend-request' + ) { + let conversation; + try { + conversation = ConversationController.get(devicePubKey); + } catch (e) { + // do nothing + } + // TODO: Make sure we retry sending friend request messages to all our friends + if (conversation && !conversation.isFriend()) { + isMultiDeviceRequest = true; + thisDeviceMessageType = 'friend-request'; + } + } + // Check if we need to attach the preKeys let sessionCipher; - const isFriendRequest = this.messageType === 'friend-request'; - this.fallBackEncryption = this.fallBackEncryption || isFriendRequest; + const isFriendRequest = thisDeviceMessageType === 'friend-request'; + enableFallBackEncryption = + enableFallBackEncryption || isFriendRequest || isMultiDeviceRequest; const flags = this.message.dataMessage ? this.message.dataMessage.get_flags() : null; const isEndSession = flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; - if (this.fallBackEncryption || isEndSession) { + const signalCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address, + options + ); + if (enableFallBackEncryption || isEndSession) { // Encrypt them with the fallback - const pkb = await libloki.storage.getPreKeyBundleForContact(number); + const pkb = await libloki.storage.getPreKeyBundleForContact( + devicePubKey + ); const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage( pkb ); this.message.preKeyBundleMessage = preKeyBundleMessage; window.log.info('attaching prekeys to outgoing message'); } - if (this.fallBackEncryption) { + + let messageBuffer; + if (isMultiDeviceRequest) { + const tempMessage = new textsecure.protobuf.Content(); + const tempDataMessage = new textsecure.protobuf.DataMessage(); + tempDataMessage.body = i18n('secondaryDeviceDefaultFR'); + if (this.message.dataMessage && this.message.dataMessage.profile) { + tempDataMessage.profile = this.message.dataMessage.profile; + } + tempMessage.preKeyBundleMessage = this.message.preKeyBundleMessage; + tempMessage.dataMessage = tempDataMessage; + messageBuffer = tempMessage.toArrayBuffer(); + } else { + messageBuffer = this.message.toArrayBuffer(); + } + + if (enableFallBackEncryption) { sessionCipher = fallBackCipher; } else { - sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address, - options - ); + sessionCipher = signalCipher; } - const plaintext = this.getPlaintext(); + const plaintext = this.getPlaintext(messageBuffer); // No limit on message keys if we're communicating with our other devices if (ourKey === number) { @@ -376,7 +420,7 @@ OutgoingMessage.prototype = { // Encrypt our plain text const ciphertext = await sessionCipher.encrypt(plaintext); - if (!this.fallBackEncryption) { + if (!enableFallBackEncryption) { // eslint-disable-next-line no-param-reassign ciphertext.body = new Uint8Array( dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer() @@ -396,7 +440,7 @@ OutgoingMessage.prototype = { return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message } }; - const ttl = getTTL(this.messageType); + const ttl = getTTL(thisDeviceMessageType); return { type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST @@ -423,6 +467,16 @@ OutgoingMessage.prototype = { this.timestamp, outgoingObject.ttl ); + if ( + outgoingObject.type === + textsecure.protobuf.Envelope.Type.FRIEND_REQUEST + ) { + const conversation = ConversationController.get(destination); + if (conversation) { + // Redundant for primary device but marks secondary devices as pending + await conversation.onFriendRequestSent(); + } + } this.successfulNumbers.push(destination); } catch (e) { e.number = destination; @@ -548,36 +602,25 @@ OutgoingMessage.prototype = { } catch (e) { // do nothing } - - return this.getStaleDeviceIdsForNumber(number).then(updateDevices => - this.getKeysForNumber(number, updateDevices) - .then(async keysFound => { - if (!keysFound) { - log.info('Fallback encryption enabled'); - this.fallBackEncryption = true; - } - }) - .then(this.reloadDevicesAndSend(number, true)) - .catch(error => { - conversation.resetPendingSend(); - 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 - ); - } - }) - ); + return this.reloadDevicesAndSend(number, true)().catch(error => { + conversation.resetPendingSend(); + 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 + ); + } + }); }, };