Skip to content

Commit

Permalink
Merge pull request #834 from loki-project/multi-device-fix
Browse files Browse the repository at this point in the history
Multi device fixes
  • Loading branch information
Mikunj committed Feb 11, 2020
2 parents b0c71d9 + cc85de5 commit 13634a4
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 46 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
9 changes: 5 additions & 4 deletions js/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -794,7 +795,7 @@
window.friends.friendRequestStatusEnum.friends
);

convo.updateGroupAdmins([ourKey]);
convo.updateGroupAdmins([primaryDeviceKey]);

appView.openConversation(groupId, {});
};
Expand Down
20 changes: 16 additions & 4 deletions js/models/conversations.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,25 @@
isOnline() {
return this.isMe() || this.get('isOnline');
},

isMe() {
return this.isOurLocalDevice() || this.isOurPrimaryDevice();
},
isOurPrimaryDevice() {
return this.id === window.storage.get('primaryDevicePubKey');
},
async isOurDevice() {
if (this.isMe()) {
return true;
}

const ourDevices = await window.libloki.storage.getPairedDevicesFor(
this.ourNumber
);
return ourDevices.includes(this.id);
},
isOurLocalDevice() {
return this.id === this.ourNumber;
},
isPublic() {
return !!(this.id && this.id.match(/^publicChat:/));
},
Expand Down Expand Up @@ -886,9 +901,6 @@
throw new Error('Invalid friend request state');
}
},
isOurConversation() {
return this.id === this.ourNumber;
},
isSecondaryDevice() {
return !!this.get('secondaryStatus');
},
Expand Down
24 changes: 18 additions & 6 deletions js/models/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
},
getLokiNameForNumber(number) {
const conversation = ConversationController.get(number);
if (!conversation) {
if (!conversation || !conversation.getLokiProfile()) {
return number;
}
return conversation.getLokiProfile().displayName;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -2355,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,
});
Expand Down
5 changes: 4 additions & 1 deletion js/modules/loki_app_dot_net_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions libloki/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@
return secondaryPubKeys.concat(primaryDevicePubKey);
}

function getPairedDevicesFor(pubkey) {
return window.Signal.Data.getPairedDevicesFor(pubkey);
}

window.libloki.storage = {
getPreKeyBundleForContact,
saveContactPreKeyBundle,
Expand All @@ -250,6 +254,7 @@
removePairingAuthorisationForSecondaryPubKey,
getGrantAuthorisationForSecondaryPubKey,
getAuthorisationForSecondaryPubKey,
getPairedDevicesFor,
getAllDevicePubKeysForPrimaryPubKey,
getSecondaryDevicesFor,
getPrimaryDeviceMapping,
Expand Down
9 changes: 7 additions & 2 deletions libtextsecure/message_receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1366,7 +1367,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!');
Expand Down
44 changes: 27 additions & 17 deletions libtextsecure/outgoing_message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}
}
Expand Down
47 changes: 39 additions & 8 deletions libtextsecure/sendmessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,11 @@ MessageSender.prototype = {

const ourNumber = textsecure.storage.user.getNumber();

numbers.forEach(number => {
// 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
// fail, let's at least send messages to those members with which we do:
Expand All @@ -420,9 +424,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'
) {
Expand Down Expand Up @@ -873,7 +885,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: [],
Expand All @@ -883,7 +902,7 @@ MessageSender.prototype = {
});
}

return new Promise((resolve, reject) => {
const sendPromise = new Promise((resolve, reject) => {
const silent = true;
const callback = res => {
res.dataMessage = proto.toArrayBuffer();
Expand All @@ -896,13 +915,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(
Expand Down Expand Up @@ -1087,8 +1113,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];
}
Expand Down Expand Up @@ -1132,8 +1161,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;
Expand Down
1 change: 1 addition & 0 deletions stylesheets/_modules.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,7 @@
margin: 10px auto;
padding: 5px 20px;
border-radius: 4px;
word-break: break-word;
}

.module-group-notification__contact {
Expand Down
7 changes: 6 additions & 1 deletion ts/components/EditProfileDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ export class EditProfileDialog extends React.Component<Props, State> {
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();

/* tslint:disable:no-backbone-get-set-outside-model */
const sessionID =
window.textsecure.storage.get('primaryDevicePubKey') ||
window.textsecure.storage.user.getNumber();
/* tslint:enable:no-backbone-get-set-outside-model */

const backButton =
viewEdit || viewQR
Expand Down
2 changes: 1 addition & 1 deletion ts/components/conversation/GroupNotification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class GroupNotification extends React.Component<Props> {
key={`external-${contact.phoneNumber}`}
className="module-group-notification__contact"
>
{contact.profileName}
{contact.profileName || contact.phoneNumber}
</span>
);

Expand Down
2 changes: 1 addition & 1 deletion ts/components/session/SessionClosableOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {

const conversationList = conversations.filter((conversation: any) => {
return (
!conversation.isOurConversation() &&
!conversation.isMe() &&
conversation.isPrivate() &&
!conversation.isSecondaryDevice() &&
conversation.isFriend()
Expand Down

0 comments on commit 13634a4

Please sign in to comment.