Merge pull request #586 from BeaudanBrown/robust-sessions

[multi-device] Robust sessions
pull/591/head
Beaudan Campbell-Brown 6 years ago committed by GitHub
commit d52a08a9e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1008,6 +1008,12 @@
"message": "Send a message", "message": "Send a message",
"description": "Placeholder text in the message entry field" "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": { "sendMessageDisabledSecondary": {
"message": "message":
"This pubkey belongs to a secondary device. You should never see this message", "This pubkey belongs to a secondary device. You should never see this message",

@ -1958,6 +1958,10 @@
}); });
} }
const sendingDeviceConversation = await ConversationController.getOrCreateAndWait(
source,
'private'
);
if (dataMessage.profileKey) { if (dataMessage.profileKey) {
const profileKey = dataMessage.profileKey.toString('base64'); const profileKey = dataMessage.profileKey.toString('base64');
if (source === textsecure.storage.user.getNumber()) { if (source === textsecure.storage.user.getNumber()) {
@ -1965,18 +1969,10 @@
} else if (conversation.isPrivate()) { } else if (conversation.isPrivate()) {
conversation.setProfileKey(profileKey); conversation.setProfileKey(profileKey);
} else { } else {
ConversationController.getOrCreateAndWait(source, 'private').then( sendingDeviceConversation.setProfileKey(profileKey);
sender => {
sender.setProfileKey(profileKey);
}
);
} }
} else if (dataMessage.profile) { } else if (dataMessage.profile) {
ConversationController.getOrCreateAndWait(source, 'private').then( sendingDeviceConversation.setLokiProfile(dataMessage.profile);
sender => {
sender.setLokiProfile(dataMessage.profile);
}
);
} }
let autoAccept = false; let autoAccept = false;
@ -1996,16 +1992,16 @@
- We are friends with the user, and that user just sent us a friend request. - We are friends with the user, and that user just sent us a friend request.
*/ */
if ( if (
conversation.hasSentFriendRequest() || sendingDeviceConversation.hasSentFriendRequest() ||
conversation.isFriend() sendingDeviceConversation.isFriend()
) { ) {
// Automatically accept incoming friend requests if we have send one already // Automatically accept incoming friend requests if we have send one already
autoAccept = true; autoAccept = true;
message.set({ friendStatus: 'accepted' }); message.set({ friendStatus: 'accepted' });
await conversation.onFriendRequestAccepted(); await sendingDeviceConversation.onFriendRequestAccepted();
window.libloki.api.sendBackgroundMessage(message.get('source')); window.libloki.api.sendBackgroundMessage(message.get('source'));
} else { } else {
await conversation.onFriendRequestReceived(); await sendingDeviceConversation.onFriendRequestReceived();
} }
} else { } else {
await conversation.onFriendRequestAccepted(); await conversation.onFriendRequestAccepted();

@ -27,6 +27,13 @@
} }
async function sendOnlineBroadcastMessage(pubKey, isPing = false) { 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 p2pAddress = null;
let p2pPort = null; let p2pPort = null;
let type; let type;

@ -179,6 +179,7 @@
if (!conversation || conversation.isPublic() || conversation.isRss()) { if (!conversation || conversation.isPublic() || conversation.isRss()) {
return null; return null;
} }
await saveAllPairingAuthorisationsFor(secondaryPubKey);
const authorisation = await window.Signal.Data.getGrantAuthorisationForSecondaryPubKey( const authorisation = await window.Signal.Data.getGrantAuthorisationForSecondaryPubKey(
secondaryPubKey secondaryPubKey
); );
@ -220,6 +221,7 @@
} }
async function getAllDevicePubKeysForPrimaryPubKey(primaryDevicePubKey) { async function getAllDevicePubKeysForPrimaryPubKey(primaryDevicePubKey) {
await saveAllPairingAuthorisationsFor(primaryDevicePubKey);
const secondaryPubKeys = const secondaryPubKeys =
(await getSecondaryDevicesFor(primaryDevicePubKey)) || []; (await getSecondaryDevicesFor(primaryDevicePubKey)) || [];
return secondaryPubKeys.concat(primaryDevicePubKey); return secondaryPubKeys.concat(primaryDevicePubKey);

@ -1277,6 +1277,7 @@ MessageReceiver.prototype.extend({
deviceMapping deviceMapping
); );
if (autoAccepted) { if (autoAccepted) {
await conversation.onFriendRequestAccepted();
return this.removeFromCache(envelope); return this.removeFromCache(envelope);
} }
} }

@ -6,8 +6,8 @@
libloki, libloki,
StringView, StringView,
dcodeIO, dcodeIO,
log,
lokiMessageAPI, lokiMessageAPI,
i18n,
*/ */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -236,8 +236,7 @@ OutgoingMessage.prototype = {
return messagePartCount * 160; return messagePartCount * 160;
}, },
convertMessageToText(message) { convertMessageToText(messageBuffer) {
const messageBuffer = message.toArrayBuffer();
const plaintext = new Uint8Array( const plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
); );
@ -246,11 +245,8 @@ OutgoingMessage.prototype = {
return plaintext; return plaintext;
}, },
getPlaintext() { getPlaintext(messageBuffer) {
if (!this.plaintext) { return this.convertMessageToText(messageBuffer);
this.plaintext = this.convertMessageToText(this.message);
}
return this.plaintext;
}, },
async wrapInWebsocketMessage(outgoingObject) { async wrapInWebsocketMessage(outgoingObject) {
const messageEnvelope = new textsecure.protobuf.Envelope({ const messageEnvelope = new textsecure.protobuf.Envelope({
@ -328,6 +324,15 @@ OutgoingMessage.prototype = {
// Loki Messenger doesn't use the deviceId scheme, it's always 1. // Loki Messenger doesn't use the deviceId scheme, it's always 1.
// Instead, there are multiple device public keys. // Instead, there are multiple device public keys.
const deviceId = 1; const deviceId = 1;
const updatedDevices = await this.getStaleDeviceIdsForNumber(
devicePubKey
);
const keysFound = await this.getKeysForNumber(
devicePubKey,
updatedDevices
);
let enableFallBackEncryption = !keysFound;
const address = new libsignal.SignalProtocolAddress( const address = new libsignal.SignalProtocolAddress(
devicePubKey, devicePubKey,
deviceId deviceId
@ -338,34 +343,73 @@ OutgoingMessage.prototype = {
address 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 // Check if we need to attach the preKeys
let sessionCipher; let sessionCipher;
const isFriendRequest = this.messageType === 'friend-request'; const isFriendRequest = thisDeviceMessageType === 'friend-request';
this.fallBackEncryption = this.fallBackEncryption || isFriendRequest; enableFallBackEncryption =
enableFallBackEncryption || isFriendRequest || isMultiDeviceRequest;
const flags = this.message.dataMessage const flags = this.message.dataMessage
? this.message.dataMessage.get_flags() ? this.message.dataMessage.get_flags()
: null; : null;
const isEndSession = const isEndSession =
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; 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 // 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( const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage(
pkb pkb
); );
this.message.preKeyBundleMessage = preKeyBundleMessage; this.message.preKeyBundleMessage = preKeyBundleMessage;
window.log.info('attaching prekeys to outgoing message'); 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; sessionCipher = fallBackCipher;
} else { } else {
sessionCipher = new libsignal.SessionCipher( sessionCipher = signalCipher;
textsecure.storage.protocol,
address,
options
);
} }
const plaintext = this.getPlaintext(); const plaintext = this.getPlaintext(messageBuffer);
// No limit on message keys if we're communicating with our other devices // No limit on message keys if we're communicating with our other devices
if (ourKey === number) { if (ourKey === number) {
@ -376,7 +420,7 @@ OutgoingMessage.prototype = {
// Encrypt our plain text // Encrypt our plain text
const ciphertext = await sessionCipher.encrypt(plaintext); const ciphertext = await sessionCipher.encrypt(plaintext);
if (!this.fallBackEncryption) { if (!enableFallBackEncryption) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
ciphertext.body = new Uint8Array( ciphertext.body = new Uint8Array(
dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer() 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 return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message
} }
}; };
const ttl = getTTL(this.messageType); const ttl = getTTL(thisDeviceMessageType);
return { return {
type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST
@ -423,6 +467,16 @@ OutgoingMessage.prototype = {
this.timestamp, this.timestamp,
outgoingObject.ttl 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); this.successfulNumbers.push(destination);
} catch (e) { } catch (e) {
e.number = destination; e.number = destination;
@ -548,36 +602,25 @@ OutgoingMessage.prototype = {
} catch (e) { } catch (e) {
// do nothing // do nothing
} }
return this.reloadDevicesAndSend(number, true)().catch(error => {
return this.getStaleDeviceIdsForNumber(number).then(updateDevices => conversation.resetPendingSend();
this.getKeysForNumber(number, updateDevices) if (error.message === 'Identity key changed') {
.then(async keysFound => { // eslint-disable-next-line no-param-reassign
if (!keysFound) { error = new textsecure.OutgoingIdentityKeyError(
log.info('Fallback encryption enabled'); number,
this.fallBackEncryption = true; error.originalMessage,
} error.timestamp,
}) error.identityKey
.then(this.reloadDevicesAndSend(number, true)) );
.catch(error => { this.registerError(number, 'Identity key changed', error);
conversation.resetPendingSend(); } else {
if (error.message === 'Identity key changed') { this.registerError(
// eslint-disable-next-line no-param-reassign number,
error = new textsecure.OutgoingIdentityKeyError( `Failed to retrieve new device keys for number ${number}`,
number, error
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
);
}
})
);
}, },
}; };

Loading…
Cancel
Save