From 054a523738ea694da06465bb7002a42abbe9e68b Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Mon, 10 Feb 2020 18:39:26 +1100 Subject: [PATCH 1/9] Fix linked device sending automatic friend request when it already has keys for a device --- js/models/conversations.js | 6 +++++ libloki/storage.js | 5 ++++ libtextsecure/outgoing_message.js | 44 +++++++++++++++++++------------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 56ceea27f..39b8f2979 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -202,6 +202,12 @@ isMe() { return this.id === window.storage.get('primaryDevicePubKey'); }, + async isOurDevice() { + const ourDevices = await window.libloki.storage.getPairedDevicesFor( + this.ourNumber + ); + return this.id === this.ourNumber || ourDevices.includes(this.id); + }, isPublic() { return !!(this.id && this.id.match(/^publicChat:/)); }, diff --git a/libloki/storage.js b/libloki/storage.js index dd3889fba..3a2d084e2 100644 --- a/libloki/storage.js +++ b/libloki/storage.js @@ -240,6 +240,10 @@ return secondaryPubKeys.concat(primaryDevicePubKey); } + function getPairedDevicesFor(pubkey) { + return window.Signal.Data.getPairedDevicesFor(pubkey); + } + window.libloki.storage = { getPreKeyBundleForContact, saveContactPreKeyBundle, @@ -250,6 +254,7 @@ removePairingAuthorisationForSecondaryPubKey, getGrantAuthorisationForSecondaryPubKey, getAuthorisationForSecondaryPubKey, + getPairedDevicesFor, getAllDevicePubKeysForPrimaryPubKey, getSecondaryDevicesFor, getPrimaryDeviceMapping, diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 5be3b7368..00e4be293 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -350,23 +350,33 @@ OutgoingMessage.prototype = { } catch (e) { // do nothing } - if ( - conversation && - !conversation.isFriend() && - !conversation.hasReceivedFriendRequest() && - !this.isGroup - ) { - // We want to send an automated friend request if: - // - We aren't already friends - // - We haven't received a friend request from this device - // - We haven't sent a friend request recently - if (conversation.friendRequestTimerIsExpired()) { - isMultiDeviceRequest = true; - thisDeviceMessageType = 'friend-request'; - } else { - // Throttle automated friend requests - this.successfulNumbers.push(devicePubKey); - return null; + if (conversation && !this.isGroup) { + const isOurDevice = await conversation.isOurDevice(); + const isFriends = + conversation.isFriend() || + conversation.hasReceivedFriendRequest(); + // We should only send a friend request to our device if we don't have keys + const shouldSendAutomatedFR = isOurDevice ? !keysFound : !isFriends; + if (shouldSendAutomatedFR) { + // We want to send an automated friend request if: + // - We aren't already friends + // - We haven't received a friend request from this device + // - We haven't sent a friend request recently + if (conversation.friendRequestTimerIsExpired()) { + isMultiDeviceRequest = true; + thisDeviceMessageType = 'friend-request'; + } else { + // Throttle automated friend requests + this.successfulNumbers.push(devicePubKey); + return null; + } + } + + // If we're not friends with our own device then we should become friends + if (isOurDevice && keysFound && !isFriends) { + conversation.setFriendRequestStatus( + window.friends.friendRequestStatusEnum.friends + ); } } } From d9c521b09d1e9f5354e1d121dc3b133f91914caf Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Mon, 10 Feb 2020 18:57:00 +1100 Subject: [PATCH 2/9] Fix message syncing in closed groups --- README.md | 3 ++- js/models/messages.js | 16 +++++++++++----- js/modules/loki_app_dot_net_api.js | 5 ++++- libtextsecure/sendmessage.js | 27 ++++++++++++++++++++++----- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 84cde08ec..ce39ccced 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ [![Build Status](https://travis-ci.org/loki-project/loki-messenger.svg?branch=development)](https://travis-ci.org/loki-project/loki-messenger) Session allows for truly decentralized, end to end, and private encrypted chats. Session is built to handle both online and fully Asynchronous offline messages. Session implements the Signal protocol for message encryption. Our Client interface is a fork of [Signal Messenger](https://signal.org/). + ## Summary -Session integrates directly with Loki [Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Session works, read the [Loki whitepaper](https://loki.network/whitepaper). +Session integrates directly with Loki [Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Session works, read the [Loki whitepaper](https://loki.network/whitepaper). **Offline messages** diff --git a/js/models/messages.js b/js/models/messages.js index 0fb0a6e61..12339ac2b 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1898,6 +1898,8 @@ const authorisation = await libloki.storage.getGrantAuthorisationForSecondaryPubKey( source ); + const primarySource = + (authorisation && authorisation.primaryDevicePubKey) || source; const isGroupMessage = !!initialMessage.group; if (isGroupMessage) { conversationId = initialMessage.group.id; @@ -1916,10 +1918,12 @@ const knownMembers = conversation.get('members'); if (!newGroup && knownMembers) { - const fromMember = knownMembers.includes(source); + const fromMember = knownMembers.includes(primarySource); if (!fromMember) { - window.log.warn(`Ignoring group message from non-member: ${source}`); + window.log.warn( + `Ignoring group message from non-member: ${primarySource}` + ); confirm(); return null; } @@ -1938,7 +1942,9 @@ ); } - const fromAdmin = conversation.get('groupAdmins').includes(source); + const fromAdmin = conversation + .get('groupAdmins') + .includes(primarySource); if (!fromAdmin) { // Make sure the message is not removing members / renaming the group @@ -2016,11 +2022,11 @@ .getConversations() .models.filter(c => c.get('members')) .reduce((acc, x) => window.Lodash.concat(acc, x.get('members')), []) - .includes(source); + .includes(primarySource); if (groupMember) { window.log.info( - `Auto accepting a 'group' friend request for a known group member: ${groupMember}` + `Auto accepting a 'group' friend request for a known group member: ${primarySource}` ); window.libloki.api.sendBackgroundMessage(message.get('source')); diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index fab433da6..c1a182ae1 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -487,7 +487,10 @@ class LokiAppDotNetServerAPI { try { response = options.textResponse ? respStr : JSON.parse(respStr); } catch (e) { - log.warn(`_sendToProxy Could not parse inner JSON [${respStr}]`, endpoint); + log.warn( + `_sendToProxy Could not parse inner JSON [${respStr}]`, + endpoint + ); } } else { log.warn( diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index d59a06d0f..e4cea3e27 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -873,7 +873,14 @@ MessageSender.prototype = { }, sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) { - if (providedNumbers.length === 0) { + // We always assume that only primary device is a member in the group + const primaryDeviceKey = + window.storage.get('primaryDevicePubKey') || + textsecure.storage.user.getNumber(); + const numbers = providedNumbers.filter( + number => number !== primaryDeviceKey + ); + if (numbers.length === 0) { return Promise.resolve({ successfulNumbers: [], failoverNumbers: [], @@ -883,7 +890,7 @@ MessageSender.prototype = { }); } - return new Promise((resolve, reject) => { + const sendPromise = new Promise((resolve, reject) => { const silent = true; const callback = res => { res.dataMessage = proto.toArrayBuffer(); @@ -896,13 +903,20 @@ MessageSender.prototype = { this.sendMessageProto( timestamp, - providedNumbers, + numbers, proto, callback, silent, options ); }); + + return sendPromise.then(result => { + // Sync the group message to our other devices + const encoded = textsecure.protobuf.DataMessage.encode(proto); + this.sendSyncMessage(encoded, timestamp, null, null, [], [], options); + return result; + }); }, async getMessageProto( @@ -1087,8 +1101,11 @@ MessageSender.prototype = { profileKey, options ) { - const me = textsecure.storage.user.getNumber(); - let numbers = groupNumbers.filter(number => number !== me); + // We always assume that only primary device is a member in the group + const primaryDeviceKey = + window.storage.get('primaryDevicePubKey') || + textsecure.storage.user.getNumber(); + let numbers = groupNumbers.filter(number => number !== primaryDeviceKey); if (options.isPublic) { numbers = [groupId]; } From 68f1ba543b60850303ea423db0a5c6c2061be766 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Mon, 10 Feb 2020 19:55:49 +1100 Subject: [PATCH 3/9] Fix closed group creation from secondary device --- libtextsecure/sendmessage.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index e4cea3e27..0d30f83c3 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -1149,8 +1149,10 @@ MessageSender.prototype = { proto.group.name = name; proto.group.members = members; - const ourPK = textsecure.storage.user.getNumber(); - proto.group.admins = [ourPK]; + const primaryDeviceKey = + window.storage.get('primaryDevicePubKey') || + textsecure.storage.user.getNumber(); + proto.group.admins = [primaryDeviceKey]; return this.makeAttachmentPointer(avatar).then(attachment => { proto.group.avatar = attachment; From 746456200ad77243bd0258900fb976b0862c28e4 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Tue, 11 Feb 2020 08:36:48 +1100 Subject: [PATCH 4/9] If we received a regular message and we're not friends with a user then check to see if we should auto accept the request --- libtextsecure/message_receiver.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 6146f86dc..5d6fca573 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1366,7 +1366,11 @@ MessageReceiver.prototype.extend({ } } - if (friendRequest) { + // If we got a friend request message or + // if we're not friends with the current user that sent this private message + // Check to see if we need to auto accept their friend request + const isGroupMessage = !!groupId; + if (friendRequest || (!isGroupMessage && !conversation.isFriend())) { if (isMe) { window.log.info('refusing to add a friend request to ourselves'); throw new Error('Cannot add a friend request for ourselves!'); From e1d06fc9be97b091e27ee0c234ad2f24b5a15244 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Tue, 11 Feb 2020 09:09:58 +1100 Subject: [PATCH 5/9] Fix messages from secondary device not being mapped correctly to their primary device. Fix creating closed groups from secondary device. Fix primary device showing up in create group dialog. --- js/background.js | 9 +++++---- js/models/messages.js | 6 ++++++ ts/components/session/SessionClosableOverlay.tsx | 4 +++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/js/background.js b/js/background.js index 7ef8f8208..998ab0288 100644 --- a/js/background.js +++ b/js/background.js @@ -763,9 +763,10 @@ const ev = new Event('group'); - const ourKey = textsecure.storage.user.getNumber(); - - const allMembers = [ourKey, ...members]; + const primaryDeviceKey = + window.storage.get('primaryDevicePubKey') || + textsecure.storage.user.getNumber(); + const allMembers = [primaryDeviceKey, ...members]; ev.groupDetails = { id: groupId, @@ -794,7 +795,7 @@ window.friends.friendRequestStatusEnum.friends ); - convo.updateGroupAdmins([ourKey]); + convo.updateGroupAdmins([primaryDeviceKey]); appView.openConversation(groupId, {}); }; diff --git a/js/models/messages.js b/js/models/messages.js index 12339ac2b..03bd93642 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -2361,6 +2361,12 @@ await sendingDeviceConversation.onFriendRequestAccepted(); } } + + // We need to map the original message source to the primary device + if (source !== ourNumber) { + message.set({ source: primarySource }); + } + const id = await window.Signal.Data.saveMessage(message.attributes, { Message: Whisper.Message, }); diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index c9997e632..e632504fd 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -55,8 +55,10 @@ export class SessionClosableOverlay extends React.Component { const conversations = window.getConversations() || []; const conversationList = conversations.filter((conversation: any) => { + // TODO: We need to handle the case with > 1 secondary device + const isOurDevice = conversation.isMe() || conversation.isOurConversation(); return ( - !conversation.isOurConversation() && + !isOurDevice&& conversation.isPrivate() && !conversation.isSecondaryDevice() && conversation.isFriend() From a75ef365b853556b2308162886b1583ea0b1f4b1 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Tue, 11 Feb 2020 10:18:41 +1100 Subject: [PATCH 6/9] Fix device unlinking. Fix session request being sent even if we have keys to setup a session. Fix minor crash. --- js/models/messages.js | 2 +- libtextsecure/message_receiver.js | 3 ++- libtextsecure/sendmessage.js | 10 +++++++++- stylesheets/_modules.scss | 1 + ts/components/conversation/GroupNotification.tsx | 2 +- ts/components/session/SessionClosableOverlay.tsx | 5 +++-- 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index 03bd93642..c79ca9c11 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -205,7 +205,7 @@ }, getLokiNameForNumber(number) { const conversation = ConversationController.get(number); - if (!conversation) { + if (!conversation || !conversation.getLokiProfile()) { return number; } return conversation.getLokiProfile().displayName; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 5d6fca573..3195d2a35 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1314,8 +1314,9 @@ MessageReceiver.prototype.extend({ primaryPubKey ); + // If we don't have a mapping on the primary then we have been unlinked if (!primaryMapping) { - return false; + return true; } // We expect the primary device to have updated its mapping diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 0d30f83c3..a53c0c258 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -411,7 +411,7 @@ MessageSender.prototype = { const ourNumber = textsecure.storage.user.getNumber(); - numbers.forEach(number => { + numbers.forEach(async number => { // Note: if we are sending a private group message, we do our best to // ensure we have signal protocol sessions with every member, but if we // fail, let's at least send messages to those members with which we do: @@ -420,9 +420,17 @@ MessageSender.prototype = { s => s.number === number ); + let keysFound = false; + // If we don't have a session but we already have prekeys to + // start communication then we should use them + if (!haveSession && !options.isPublic) { + keysFound = await outgoing.getKeysForNumber(number, []); + } + if ( number === ourNumber || haveSession || + keysFound || options.isPublic || options.messageType === 'friend-request' ) { diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 097920c68..99234384c 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1241,6 +1241,7 @@ margin: 10px auto; padding: 5px 20px; border-radius: 4px; + word-break: break-word; } .module-group-notification__contact { diff --git a/ts/components/conversation/GroupNotification.tsx b/ts/components/conversation/GroupNotification.tsx index 9cec3d1bf..b1484bbb4 100644 --- a/ts/components/conversation/GroupNotification.tsx +++ b/ts/components/conversation/GroupNotification.tsx @@ -38,7 +38,7 @@ export class GroupNotification extends React.Component { key={`external-${contact.phoneNumber}`} className="module-group-notification__contact" > - {contact.profileName} + {contact.profileName || contact.phoneNumber} ); diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index e632504fd..d0d33cf14 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -56,9 +56,10 @@ export class SessionClosableOverlay extends React.Component { const conversationList = conversations.filter((conversation: any) => { // TODO: We need to handle the case with > 1 secondary device - const isOurDevice = conversation.isMe() || conversation.isOurConversation(); + const isOurDevice = + conversation.isMe() || conversation.isOurConversation(); return ( - !isOurDevice&& + !isOurDevice && conversation.isPrivate() && !conversation.isSecondaryDevice() && conversation.isFriend() From 4f6dd7a8d5f5c6aed4f0430102729925d5b871b2 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Tue, 11 Feb 2020 11:02:06 +1100 Subject: [PATCH 7/9] Rename functions to be less confusing --- js/models/conversations.js | 12 +++++++----- libtextsecure/sendmessage.js | 4 ++++ ts/components/session/SessionClosableOverlay.tsx | 5 +---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 39b8f2979..3abcecb58 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -198,15 +198,20 @@ isOnline() { return this.isMe() || this.get('isOnline'); }, - isMe() { + return this.isOurLocalDevice() || this.isOurPrimaryDevice(); + }, + isOurPrimaryDevice() { return this.id === window.storage.get('primaryDevicePubKey'); }, async isOurDevice() { const ourDevices = await window.libloki.storage.getPairedDevicesFor( this.ourNumber ); - return this.id === this.ourNumber || ourDevices.includes(this.id); + return this.isOurLocalDevice() || ourDevices.includes(this.id); + }, + isOurLocalDevice() { + return this.id === this.ourNumber; }, isPublic() { return !!(this.id && this.id.match(/^publicChat:/)); @@ -892,9 +897,6 @@ throw new Error('Invalid friend request state'); } }, - isOurConversation() { - return this.id === this.ourNumber; - }, isSecondaryDevice() { return !!this.get('secondaryStatus'); }, diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index a53c0c258..2f0dbc001 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -411,6 +411,10 @@ MessageSender.prototype = { const ourNumber = textsecure.storage.user.getNumber(); + // Note: Since we're just doing independant tasks, + // using `async` in the `forEach` loop should be fine. + // If however we want to use the results from forEach then + // we would need to convert this to a Promise.all(numbers.map(...)) numbers.forEach(async number => { // Note: if we are sending a private group message, we do our best to // ensure we have signal protocol sessions with every member, but if we diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index d0d33cf14..88024618c 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -55,11 +55,8 @@ export class SessionClosableOverlay extends React.Component { const conversations = window.getConversations() || []; const conversationList = conversations.filter((conversation: any) => { - // TODO: We need to handle the case with > 1 secondary device - const isOurDevice = - conversation.isMe() || conversation.isOurConversation(); return ( - !isOurDevice && + !conversation.isMe() && conversation.isPrivate() && !conversation.isSecondaryDevice() && conversation.isFriend() From 7d3a18e85564542b19ca123a3d2bd0287215b869 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Tue, 11 Feb 2020 11:04:11 +1100 Subject: [PATCH 8/9] Fix secondary device showing incorrect sessionID --- ts/components/EditProfileDialog.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index 555986cdc..caddc5c55 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -71,7 +71,9 @@ export class EditProfileDialog extends React.Component { const viewDefault = this.state.mode === 'default'; const viewEdit = this.state.mode === 'edit'; const viewQR = this.state.mode === 'qr'; - const sessionID = window.textsecure.storage.user.getNumber(); + const sessionID = + window.storage.get('primaryDevicePubKey') || + window.textsecure.storage.user.getNumber(); const backButton = viewEdit || viewQR From cc85de52506a56d92c618f2a511c26604b6ce634 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Tue, 11 Feb 2020 11:31:05 +1100 Subject: [PATCH 9/9] Optimisation and lint fixes --- js/models/conversations.js | 6 +++++- ts/components/EditProfileDialog.tsx | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 3abcecb58..7471c5798 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -205,10 +205,14 @@ return this.id === window.storage.get('primaryDevicePubKey'); }, async isOurDevice() { + if (this.isMe()) { + return true; + } + const ourDevices = await window.libloki.storage.getPairedDevicesFor( this.ourNumber ); - return this.isOurLocalDevice() || ourDevices.includes(this.id); + return ourDevices.includes(this.id); }, isOurLocalDevice() { return this.id === this.ourNumber; diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index caddc5c55..1a560dfc7 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -71,9 +71,12 @@ export class EditProfileDialog extends React.Component { const viewDefault = this.state.mode === 'default'; const viewEdit = this.state.mode === 'edit'; const viewQR = this.state.mode === 'qr'; + + /* tslint:disable:no-backbone-get-set-outside-model */ const sessionID = - window.storage.get('primaryDevicePubKey') || + window.textsecure.storage.get('primaryDevicePubKey') || window.textsecure.storage.user.getNumber(); + /* tslint:enable:no-backbone-get-set-outside-model */ const backButton = viewEdit || viewQR