From 7e612700f7b36180ea0c485da14690f56386911c Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 30 Nov 2018 14:02:32 +1100 Subject: [PATCH 01/12] Handle session reset, Loki style --- _locales/en/messages.json | 7 +- js/models/conversations.js | 98 ++++++++++++---- js/models/messages.js | 18 ++- js/views/conversation_view.js | 3 +- libloki/libloki-protocol.js | 4 + libtextsecure/message_receiver.js | 111 ++++++++++++++++-- libtextsecure/outgoing_message.js | 14 ++- libtextsecure/sendmessage.js | 12 ++ .../conversation/ResetSessionNotification.tsx | 5 +- 9 files changed, 231 insertions(+), 41 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f079b38d0..39568a7d7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -891,8 +891,13 @@ "description": "Confirmation dialog text that asks the user if they really wish to delete the conversation. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone." }, + "sessionResetOngoing": { + "message": "Secure session reset in progress", + "description": + "your secure session is currently being reset, waiting for the reset acknowledgment." + }, "sessionEnded": { - "message": "Secure session reset", + "message": "Secure session reset done", "description": "This is a past tense, informational message. In other words, your secure session has been reset." }, diff --git a/js/models/conversations.js b/js/models/conversations.js index 636ed3b2f..322f19d36 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -53,6 +53,16 @@ friends: 4, }); + // Possible session reset states + const SessionResetEnum = Object.freeze({ + // No ongoing reset + none: 0, + // we initiated the session reset + initiated: 1, + // we received the session reset + request_received: 2, + }); + const COLORS = [ 'red', 'deep_orange', @@ -75,6 +85,7 @@ verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT, friendRequestStatus: FriendRequestStatusEnum.none, unlockTimestamp: null, // Timestamp used for expiring friend requests. + sessionResetStatus: FriendStatusEnum.none, }; }, @@ -1421,30 +1432,77 @@ return !this.get('left'); }, - async endSession() { - if (this.isPrivate()) { - const now = Date.now(); - const message = this.messageCollection.add({ - conversationId: this.id, - type: 'outgoing', - sent_at: now, - received_at: now, - destination: this.id, - recipients: this.getRecipients(), - flags: textsecure.protobuf.DataMessage.Flags.END_SESSION, + async onSessionResetInitiated() { + if (this.get('sessionResetStatus') === SessionResetEnum.none) { + this.set({ sessionResetStatus : SessionResetEnum.initiated }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, }); + } + }, - const id = await window.Signal.Data.saveMessage(message.attributes, { - Message: Whisper.Message, + async onSessionResetReceived() { + if (this.get('sessionResetStatus') === SessionResetEnum.none) { + this.set({ sessionResetStatus : SessionResetEnum.request_received }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, }); - message.set({ id }); + // send empty message, this will trigger the new session to propagate + // to the reset initiator. + window.libloki.sendEmptyMessage(this.id); + } + }, - const options = this.getSendOptions(); - message.send( - this.wrapSend( - textsecure.messaging.resetSession(this.id, now, options) - ) - ); + isSessionResetReceived() { + return this.get('sessionResetStatus') === SessionResetEnum.request_received; + }, + + async createAndStoreEndSessionMessage(endSessionType) { + const now = Date.now(); + const message = this.messageCollection.add({ + conversationId: this.id, + type: 'outgoing', + sent_at: now, + received_at: now, + destination: this.id, + recipients: this.getRecipients(), + flags: textsecure.protobuf.DataMessage.Flags.END_SESSION, + endSessionType, + }); + + const id = await window.Signal.Data.saveMessage(message.attributes, { + Message: Whisper.Message, + }); + message.set({ id }); + return message; + }, + + async onNewSessionAdopted() { + if (this.get('sessionResetStatus') === SessionResetEnum.initiated) { + // send empty message to confirm that we have adopted the new session + window.libloki.sendEmptyMessage(this.id); + } + this.createAndStoreEndSessionMessage('done'); + this.set({ sessionResetStatus : SessionResetEnum.none }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + }, + + async endSession() { + if (this.isPrivate()) { + // Only create a new message if we initiated the session reset. + // On the receiver side, the actual message containing the END_SESSION flag + // will ensure the "session reset" message will be added to their conversation. + if (this.get('sessionResetStatus') === SessionResetEnum.initiated) { + const message = await this.createAndStoreEndSessionMessage('ongoing'); + const options = this.getSendOptions(); + message.send( + this.wrapSend( + textsecure.messaging.resetSession(this.id, message.get('sent_at'), options) + ) + ); + } } }, diff --git a/js/models/messages.js b/js/models/messages.js index d528b524e..70a62c0f9 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -108,6 +108,12 @@ // eslint-disable-next-line no-bitwise return !!(this.get('flags') & flag); }, + getEndSessionTranslationKey() { + if (this.get('endSessionType') === 'ongoing') { + return 'sessionResetOngoing'; + } + return 'sessionEnded'; + }, isExpirationTimerUpdate() { const flag = textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; @@ -174,7 +180,7 @@ return messages.join(', '); } if (this.isEndSession()) { - return i18n('sessionEnded'); + return i18n(this.getEndSessionTranslationKey()); } if (this.isIncoming() && this.hasErrors()) { return i18n('incomingError'); @@ -294,8 +300,9 @@ }; }, getPropsForResetSessionNotification() { - // It doesn't need anything right now! - return {}; + return { + sessionResetMessageKey: this.getEndSessionTranslationKey(), + }; }, async acceptFriendRequest() { @@ -1303,6 +1310,11 @@ message.get('received_at') ); } + } else { + const endSessionType = conversation.isSessionResetReceived() + ? 'ongoing' + : 'done'; + this.set({ endSessionType }); } if (type === 'incoming' || type === 'friend-request') { const readSync = Whisper.ReadSyncs.forMessage(message); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 1ccafb00e..47290fcbc 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1258,7 +1258,8 @@ } }, - endSession() { + async endSession() { + await this.model.onSessionResetInitiated(); this.model.endSession(); }, diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js index 8d2a36cb5..ca3bf8c10 100644 --- a/libloki/libloki-protocol.js +++ b/libloki/libloki-protocol.js @@ -130,6 +130,9 @@ } async function sendFriendRequestAccepted(pubKey) { + return sendEmptyMessage(pubKey); + } + async function sendEmptyMessage(pubKey) { // empty content message const content = new textsecure.protobuf.Content(); @@ -161,4 +164,5 @@ window.libloki.savePreKeyBundleForNumber = savePreKeyBundleForNumber; window.libloki.removePreKeyBundleForNumber = removePreKeyBundleForNumber; window.libloki.sendFriendRequestAccepted = sendFriendRequestAccepted; + window.libloki.sendEmptyMessage = sendEmptyMessage; })(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index ea36300e6..77aa27b38 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -717,11 +717,70 @@ MessageReceiver.prototype.extend({ deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10), }; + let conversation; + try { + conversation = await window.ConversationController.getOrCreateAndWait(envelope.source, 'private'); + } catch (e) { + window.log.info('Error getting conversation: ', envelope.source); + } + const getCurrentSessionBaseKey = async () => { + const record = await sessionCipher.getRecord(address.toString()); + if (!record) + return null; + const openSession = record.getOpenSession(); + if (!openSession) + return null; + const { baseKey } = openSession.indexInfo; + return baseKey + }; + const captureActiveSession = async () => { + this.activeSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); + }; + const restoreActiveSession = async () => { + const record = await sessionCipher.getRecord(address.toString()); + if (!record) + return + record.archiveCurrentState(); + const sessionToRestore = record.sessions[this.activeSessionBaseKey]; + record.promoteState(sessionToRestore); + record.updateSessionState(sessionToRestore); + await textsecure.storage.protocol.storeSession(address.toString(), record.serialize()); + }; + const deleteAllSessionExcept = async (sessionBaseKey) => { + const record = await sessionCipher.getRecord(address.toString()); + if (!record) + return + const sessionToKeep = record.sessions[sessionBaseKey]; + record.sessions = {} + record.updateSessionState(sessionToKeep); + await textsecure.storage.protocol.storeSession(address.toString(), record.serialize()); + }; + const handleSessionReset = async () => { + const currentSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); + // console.warn('%cdecipher session %s', 'color:red;', currentSessionBaseKey); + if (this.activeSessionBaseKey && currentSessionBaseKey !== this.activeSessionBaseKey) { + if (conversation.isSessionResetReceived()) { + restoreActiveSession(); + } else { + deleteAllSessionExcept(currentSessionBaseKey); + conversation.onNewSessionAdopted(); + } + } else if (conversation.isSessionResetReceived()) { + deleteAllSessionExcept(this.activeSessionBaseKey); + conversation.onNewSessionAdopted(); + } + }; + switch (envelope.type) { case textsecure.protobuf.Envelope.Type.CIPHERTEXT: window.log.info('message from', this.getEnvelopeId(envelope)); - promise = sessionCipher.decryptWhisperMessage(ciphertext) - .then(this.unpad); + promise = captureActiveSession() + .then(() => sessionCipher.decryptWhisperMessage(ciphertext)) + .then(this.unpad) + .then((plainText) => { + handleSessionReset(); + return plainText; + }); break; case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { window.log.info('friend-request message from ', envelope.source); @@ -731,11 +790,16 @@ MessageReceiver.prototype.extend({ } case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: window.log.info('prekey message from', this.getEnvelopeId(envelope)); - promise = this.decryptPreKeyWhisperMessage( - ciphertext, - sessionCipher, - address - ); + promise = captureActiveSession(sessionCipher) + .then(() => this.decryptPreKeyWhisperMessage( + ciphertext, + sessionCipher, + address + )) + .then((plainText) => { + handleSessionReset(); + return plainText; + }); break; case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: window.log.info('received unidentified sender message'); @@ -911,6 +975,9 @@ MessageReceiver.prototype.extend({ if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { p = this.handleEndSession(envelope.source); } + const type = (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) + ? 'friend-request' + : 'data'; return p.then(() => this.processDecrypted(envelope, msg, envelope.source).then(message => { const groupId = message.group && message.group.id; @@ -1282,17 +1349,37 @@ MessageReceiver.prototype.extend({ async handleEndSession(number) { window.log.info('got end session'); const deviceIds = await textsecure.storage.protocol.getDeviceIds(number); + const identityKey = StringView.hexToArrayBuffer(number); + let conversation; + try { + conversation = window.ConversationController.get(number); + } catch (e) { + window.log.error('Error getting conversation: ', number); + } + + conversation.onSessionResetReceived(); return Promise.all( - deviceIds.map(deviceId => { + deviceIds.map(async deviceId => { const address = new libsignal.SignalProtocolAddress(number, deviceId); - const sessionCipher = new libsignal.SessionCipher( + // Instead of deleting the sessions now, + // we process the new prekeys and initiate a new session. + // The old sessions will get deleted once the correspondant + // has switch the the new session. + const [preKey, signedPreKey] = await Promise.all([ + textsecure.storage.protocol.loadContactPreKey(number), + textsecure.storage.protocol.loadContactSignedPreKey(number), + ]); + if (preKey === undefined || signedPreKey === undefined) { + return null; + } + const device = { identityKey, deviceId, preKey, signedPreKey, registrationId: 0 } + const builder = new libsignal.SessionBuilder( textsecure.storage.protocol, address ); - - window.log.info('deleting sessions for', address.toString()); - return sessionCipher.deleteAllSessionsForDevice(); + builder.processPreKey(device); + return null; }) ); }, diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 0e4be306c..d0e79665d 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -119,7 +119,12 @@ OutgoingMessage.prototype = { if (device.registrationId === 0) { window.log.info('device registrationId 0!'); } - return builder.processPreKey(device).then(() => true).catch(error => { + return builder.processPreKey(device).then(async () => { + // TODO: only remove the keys that were used above! + await window.libloki.removePreKeyBundleForNumber(number); + return true; + } + ).catch(error => { if (error.message === 'Identity key changed') { // eslint-disable-next-line no-param-reassign error.timestamp = this.timestamp; @@ -285,10 +290,15 @@ OutgoingMessage.prototype = { // Check if we need to attach the preKeys let sessionCipher; - if (this.messageType === 'friend-request') { + const isFriendRequest = this.messageType === 'friend-request'; + const flags = this.message.dataMessage ? this.message.dataMessage.get_flags() : null; + const isEndSession = flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; + if (isFriendRequest || isEndSession) { // Encrypt them with the fallback this.message.preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number); window.log.info('attaching prekeys to outgoing message'); + } + if (isFriendRequest) { sessionCipher = fallBackCipher; } else { sessionCipher = new libsignal.SessionCipher( diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 9eb2ec626..66c3978be 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -691,6 +691,17 @@ MessageSender.prototype = { window.log.error(prefix, error && error.stack ? error.stack : error); throw error; }; + // The actual deletion of the session now happens later + // as we need to ensure the other contact has successfully + // switch to a new session first. + return this.sendIndividualProto( + number, + proto, + timestamp, + silent, + options + ).catch(logError('resetSession/sendToContact error:')); + /* const deleteAllSessions = targetNumber => textsecure.storage.protocol.getDeviceIds(targetNumber).then(deviceIds => Promise.all( @@ -741,6 +752,7 @@ MessageSender.prototype = { ).catch(logError('resetSession/sendSync error:')); return Promise.all([sendToContact, sendSync]); + */ }, sendMessageToGroup( diff --git a/ts/components/conversation/ResetSessionNotification.tsx b/ts/components/conversation/ResetSessionNotification.tsx index 19141a869..f964a629a 100644 --- a/ts/components/conversation/ResetSessionNotification.tsx +++ b/ts/components/conversation/ResetSessionNotification.tsx @@ -4,15 +4,16 @@ import { Localizer } from '../../types/Util'; interface Props { i18n: Localizer; + sessionResetMessageKey: string; } export class ResetSessionNotification extends React.Component { public render() { - const { i18n } = this.props; + const { i18n, sessionResetMessageKey } = this.props; return (
- {i18n('sessionEnded')} + { i18n(sessionResetMessageKey) }
); } From 1dc621a9170398f2d042d3b6838b5bbba954cae8 Mon Sep 17 00:00:00 2001 From: Beaudan Campbell-Brown Date: Fri, 30 Nov 2018 14:58:00 +1100 Subject: [PATCH 02/12] Fix using FriendStatusEnum for session reset Co-Authored-By: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> --- js/models/conversations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 322f19d36..d288d3bf7 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -85,7 +85,7 @@ verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT, friendRequestStatus: FriendRequestStatusEnum.none, unlockTimestamp: null, // Timestamp used for expiring friend requests. - sessionResetStatus: FriendStatusEnum.none, + sessionResetStatus: SessionResetEnum.none, }; }, From 4cc614269e0e20257bb64236bd3cb91136718b46 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Fri, 30 Nov 2018 15:05:19 +1100 Subject: [PATCH 03/12] add setter for session reset state --- js/models/conversations.js | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index d288d3bf7..b80591061 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1431,26 +1431,25 @@ isSearchable() { return !this.get('left'); }, - - async onSessionResetInitiated() { - if (this.get('sessionResetStatus') === SessionResetEnum.none) { - this.set({ sessionResetStatus : SessionResetEnum.initiated }); + async setSessionResetStatus(newStatus) { + // Ensure that the new status is a valid SessionResetEnum value + if (!(newStatus in Object.values(SessionResetEnum))) + return; + if (this.get('sessionResetStatus') !== newStatus) { + this.set({ sessionResetStatus: newStatus }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, }); } }, - - async onSessionResetReceived() { - if (this.get('sessionResetStatus') === SessionResetEnum.none) { - this.set({ sessionResetStatus : SessionResetEnum.request_received }); - await window.Signal.Data.updateConversation(this.id, this.attributes, { - Conversation: Whisper.Conversation, - }); - // send empty message, this will trigger the new session to propagate - // to the reset initiator. - window.libloki.sendEmptyMessage(this.id); - } + async onSessionResetInitiated() { + this.setSessionResetStatus(SessionResetEnum.initiated); + }, + async onSessionResetReceived() { + this.setSessionResetStatus(SessionResetEnum.request_received); + // send empty message, this will trigger the new session to propagate + // to the reset initiator. + window.libloki.sendEmptyMessage(this.id); }, isSessionResetReceived() { @@ -1483,10 +1482,7 @@ window.libloki.sendEmptyMessage(this.id); } this.createAndStoreEndSessionMessage('done'); - this.set({ sessionResetStatus : SessionResetEnum.none }); - await window.Signal.Data.updateConversation(this.id, this.attributes, { - Conversation: Whisper.Conversation, - }); + this.setSessionResetStatus(SessionResetEnum.none); }, async endSession() { From 933d7730dbbf56db8379676b710bbf0c89201e78 Mon Sep 17 00:00:00 2001 From: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> Date: Fri, 30 Nov 2018 15:58:16 +1100 Subject: [PATCH 04/12] Remove superfluous commented out line --- libtextsecure/message_receiver.js | 1 - 1 file changed, 1 deletion(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 77aa27b38..e8eb0ca8b 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -757,7 +757,6 @@ MessageReceiver.prototype.extend({ }; const handleSessionReset = async () => { const currentSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); - // console.warn('%cdecipher session %s', 'color:red;', currentSessionBaseKey); if (this.activeSessionBaseKey && currentSessionBaseKey !== this.activeSessionBaseKey) { if (conversation.isSessionResetReceived()) { restoreActiveSession(); From df80249cbae3164c84359d07eda1304ff4159b38 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 3 Dec 2018 15:18:23 +1100 Subject: [PATCH 05/12] fix missing awaits --- _locales/en/messages.json | 7 ++++++- js/models/conversations.js | 23 ++++++++++++++--------- js/models/messages.js | 5 ++++- js/views/conversation_view.js | 1 - 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 39568a7d7..17e41fc90 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -891,13 +891,18 @@ "description": "Confirmation dialog text that asks the user if they really wish to delete the conversation. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone." }, + "sessionResetFailed": { + "message": "Secure session reset failed", + "description": + "your secure session could not been transmitted to the other participant." + }, "sessionResetOngoing": { "message": "Secure session reset in progress", "description": "your secure session is currently being reset, waiting for the reset acknowledgment." }, "sessionEnded": { - "message": "Secure session reset done", + "message": "Secure session reset succeeded", "description": "This is a past tense, informational message. In other words, your secure session has been reset." }, diff --git a/js/models/conversations.js b/js/models/conversations.js index b80591061..7a3e0bddb 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1443,13 +1443,13 @@ } }, async onSessionResetInitiated() { - this.setSessionResetStatus(SessionResetEnum.initiated); + await this.setSessionResetStatus(SessionResetEnum.initiated); }, async onSessionResetReceived() { - this.setSessionResetStatus(SessionResetEnum.request_received); + await this.setSessionResetStatus(SessionResetEnum.request_received); // send empty message, this will trigger the new session to propagate // to the reset initiator. - window.libloki.sendEmptyMessage(this.id); + await window.libloki.sendEmptyMessage(this.id); }, isSessionResetReceived() { @@ -1479,25 +1479,30 @@ async onNewSessionAdopted() { if (this.get('sessionResetStatus') === SessionResetEnum.initiated) { // send empty message to confirm that we have adopted the new session - window.libloki.sendEmptyMessage(this.id); + await window.libloki.sendEmptyMessage(this.id); } - this.createAndStoreEndSessionMessage('done'); - this.setSessionResetStatus(SessionResetEnum.none); + await this.createAndStoreEndSessionMessage('done'); + await this.setSessionResetStatus(SessionResetEnum.none); }, async endSession() { if (this.isPrivate()) { - // Only create a new message if we initiated the session reset. + // Only create a new message if *we* initiated the session reset. // On the receiver side, the actual message containing the END_SESSION flag // will ensure the "session reset" message will be added to their conversation. - if (this.get('sessionResetStatus') === SessionResetEnum.initiated) { + if (this.get('sessionResetStatus') === SessionResetEnum.none) { + await this.onSessionResetInitiated(); const message = await this.createAndStoreEndSessionMessage('ongoing'); const options = this.getSendOptions(); - message.send( + await message.send( this.wrapSend( textsecure.messaging.resetSession(this.id, message.get('sent_at'), options) ) ); + if (message.hasErrors()) { + await this.createAndStoreEndSessionMessage('failed'); + await this.setSessionResetStatus(SessionResetEnum.none); + } } } }, diff --git a/js/models/messages.js b/js/models/messages.js index 70a62c0f9..5d031bad5 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -109,8 +109,11 @@ return !!(this.get('flags') & flag); }, getEndSessionTranslationKey() { - if (this.get('endSessionType') === 'ongoing') { + const sessionType = this.get('endSessionType'); + if (sessionType === 'ongoing') { return 'sessionResetOngoing'; + } else if (sessionType === 'failed') { + return 'sessionResetFailed'; } return 'sessionEnded'; }, diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 47290fcbc..7e2730386 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1259,7 +1259,6 @@ }, async endSession() { - await this.model.onSessionResetInitiated(); this.model.endSession(); }, From f2e1b9b8de61b5a198bd2ddc5f7cb0ffc541efbe Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 3 Dec 2018 16:18:41 +1100 Subject: [PATCH 06/12] more missing awaits --- js/models/conversations.js | 4 +++ libtextsecure/message_receiver.js | 53 ++++++++++++++++--------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 7a3e0bddb..b1acd41eb 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1456,6 +1456,10 @@ return this.get('sessionResetStatus') === SessionResetEnum.request_received; }, + isSessionResetOngoing() { + return this.get('sessionResetStatus') !== SessionResetEnum.none; + }, + async createAndStoreEndSessionMessage(endSessionType) { const now = Date.now(); const message = this.messageCollection.add({ diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e8eb0ca8b..e0bf2129d 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -755,20 +755,26 @@ MessageReceiver.prototype.extend({ record.updateSessionState(sessionToKeep); await textsecure.storage.protocol.storeSession(address.toString(), record.serialize()); }; - const handleSessionReset = async () => { - const currentSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); - if (this.activeSessionBaseKey && currentSessionBaseKey !== this.activeSessionBaseKey) { - if (conversation.isSessionResetReceived()) { - restoreActiveSession(); - } else { - deleteAllSessionExcept(currentSessionBaseKey); - conversation.onNewSessionAdopted(); + let handleSessionReset; + if (conversation.isSessionResetOngoing()) { + handleSessionReset = async (result) => { + const currentSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); + if (this.activeSessionBaseKey && currentSessionBaseKey !== this.activeSessionBaseKey) { + if (conversation.isSessionResetReceived()) { + await restoreActiveSession(); + } else { + await deleteAllSessionExcept(currentSessionBaseKey); + await conversation.onNewSessionAdopted(); + } + } else if (conversation.isSessionResetReceived()) { + await deleteAllSessionExcept(this.activeSessionBaseKey); + await conversation.onNewSessionAdopted(); } - } else if (conversation.isSessionResetReceived()) { - deleteAllSessionExcept(this.activeSessionBaseKey); - conversation.onNewSessionAdopted(); - } - }; + return result; + }; + } else { + handleSessionReset = async (result) => result; + } switch (envelope.type) { case textsecure.protobuf.Envelope.Type.CIPHERTEXT: @@ -776,10 +782,7 @@ MessageReceiver.prototype.extend({ promise = captureActiveSession() .then(() => sessionCipher.decryptWhisperMessage(ciphertext)) .then(this.unpad) - .then((plainText) => { - handleSessionReset(); - return plainText; - }); + .then(handleSessionReset); break; case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { window.log.info('friend-request message from ', envelope.source); @@ -795,10 +798,7 @@ MessageReceiver.prototype.extend({ sessionCipher, address )) - .then((plainText) => { - handleSessionReset(); - return plainText; - }); + .then(handleSessionReset); break; case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: window.log.info('received unidentified sender message'); @@ -1356,9 +1356,12 @@ MessageReceiver.prototype.extend({ window.log.error('Error getting conversation: ', number); } - conversation.onSessionResetReceived(); + // Bail early if a session reset is already ongoing + if (conversation.isSessionResetOngoing()) { + return; + } - return Promise.all( + await Promise.all( deviceIds.map(async deviceId => { const address = new libsignal.SignalProtocolAddress(number, deviceId); // Instead of deleting the sessions now, @@ -1370,7 +1373,7 @@ MessageReceiver.prototype.extend({ textsecure.storage.protocol.loadContactSignedPreKey(number), ]); if (preKey === undefined || signedPreKey === undefined) { - return null; + return; } const device = { identityKey, deviceId, preKey, signedPreKey, registrationId: 0 } const builder = new libsignal.SessionBuilder( @@ -1378,9 +1381,9 @@ MessageReceiver.prototype.extend({ address ); builder.processPreKey(device); - return null; }) ); + await conversation.onSessionResetReceived(); }, processDecrypted(envelope, decrypted, source) { /* eslint-disable no-bitwise, no-param-reassign */ From 962c9476059fac3f3fc0b0ead19d692463831ce9 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Mon, 3 Dec 2018 17:43:32 +1100 Subject: [PATCH 07/12] Prevent "secure session reset succeeded" message from being cleaned up at startup --- js/background.js | 4 ++++ js/models/conversations.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/js/background.js b/js/background.js index ebc613361..dc94d55ba 100644 --- a/js/background.js +++ b/js/background.js @@ -347,6 +347,10 @@ return; } + if (message.isEndSession()) { + return; + } + if (message.hasErrors()) { return; } diff --git a/js/models/conversations.js b/js/models/conversations.js index b1acd41eb..8b5c7540b 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1494,7 +1494,7 @@ // Only create a new message if *we* initiated the session reset. // On the receiver side, the actual message containing the END_SESSION flag // will ensure the "session reset" message will be added to their conversation. - if (this.get('sessionResetStatus') === SessionResetEnum.none) { + if (this.get('sessionResetStatus') !== SessionResetEnum.request_received) { await this.onSessionResetInitiated(); const message = await this.createAndStoreEndSessionMessage('ongoing'); const options = this.getSendOptions(); From e62c661ed9b4b4142a457e8ef867e1961f9b207c Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Tue, 4 Dec 2018 13:59:44 +1100 Subject: [PATCH 08/12] Treat session reset success messages as incoming, handle failure differently --- js/models/conversations.js | 9 ++++----- js/models/messages.js | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 8b5c7540b..bd4504a2b 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1460,7 +1460,7 @@ return this.get('sessionResetStatus') !== SessionResetEnum.none; }, - async createAndStoreEndSessionMessage(endSessionType) { + async createAndStoreEndSessionMessage(attributes) { const now = Date.now(); const message = this.messageCollection.add({ conversationId: this.id, @@ -1470,7 +1470,7 @@ destination: this.id, recipients: this.getRecipients(), flags: textsecure.protobuf.DataMessage.Flags.END_SESSION, - endSessionType, + ...attributes, }); const id = await window.Signal.Data.saveMessage(message.attributes, { @@ -1485,7 +1485,7 @@ // send empty message to confirm that we have adopted the new session await window.libloki.sendEmptyMessage(this.id); } - await this.createAndStoreEndSessionMessage('done'); + await this.createAndStoreEndSessionMessage({ type: 'incoming', endSessionType: 'done' }); await this.setSessionResetStatus(SessionResetEnum.none); }, @@ -1496,7 +1496,7 @@ // will ensure the "session reset" message will be added to their conversation. if (this.get('sessionResetStatus') !== SessionResetEnum.request_received) { await this.onSessionResetInitiated(); - const message = await this.createAndStoreEndSessionMessage('ongoing'); + const message = await this.createAndStoreEndSessionMessage({ type: 'outgoing', endSessionType: 'ongoing' }); const options = this.getSendOptions(); await message.send( this.wrapSend( @@ -1504,7 +1504,6 @@ ) ); if (message.hasErrors()) { - await this.createAndStoreEndSessionMessage('failed'); await this.setSessionResetStatus(SessionResetEnum.none); } } diff --git a/js/models/messages.js b/js/models/messages.js index 5d031bad5..23ee2fb3c 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1136,6 +1136,10 @@ }); errors = errors.concat(this.get('errors') || []); + if (this.isEndSession) { + this.set({ endSessionType: 'failed'}); + } + this.set({ errors }); await window.Signal.Data.saveMessage(this.attributes, { Message: Whisper.Message, From 8afbb9b3e57d1310c61727ce0adbfd701bab7d47 Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Wed, 5 Dec 2018 16:29:21 +1100 Subject: [PATCH 09/12] Update js/views/conversation_view.js Co-Authored-By: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> --- js/views/conversation_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 7e2730386..1ccafb00e 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1258,7 +1258,7 @@ } }, - async endSession() { + endSession() { this.model.endSession(); }, From 86e04b89e2b3470c8d562e49c8a578f0447dcedc Mon Sep 17 00:00:00 2001 From: Beaudan Campbell-Brown Date: Thu, 6 Dec 2018 11:51:00 +1100 Subject: [PATCH 10/12] Update js/models/conversations.js Co-Authored-By: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> --- js/models/conversations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index bd4504a2b..eeac30e13 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1445,7 +1445,7 @@ async onSessionResetInitiated() { await this.setSessionResetStatus(SessionResetEnum.initiated); }, - async onSessionResetReceived() { + async onSessionResetReceived() { await this.setSessionResetStatus(SessionResetEnum.request_received); // send empty message, this will trigger the new session to propagate // to the reset initiator. From 91f018da575f5a25005de550905e4ed800f6e730 Mon Sep 17 00:00:00 2001 From: sachaaaaa Date: Thu, 6 Dec 2018 12:19:36 +1100 Subject: [PATCH 11/12] remove unused var --- libtextsecure/message_receiver.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e0bf2129d..be8ef731e 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -974,9 +974,6 @@ MessageReceiver.prototype.extend({ if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) { p = this.handleEndSession(envelope.source); } - const type = (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) - ? 'friend-request' - : 'data'; return p.then(() => this.processDecrypted(envelope, msg, envelope.source).then(message => { const groupId = message.group && message.group.id; From c0ee584a2ca4fc397a00587bc682cec8683959df Mon Sep 17 00:00:00 2001 From: Beaudan Campbell-Brown Date: Thu, 6 Dec 2018 12:40:53 +1100 Subject: [PATCH 12/12] Update libloki/libloki-protocol.js Co-Authored-By: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> --- libloki/libloki-protocol.js | 1 + 1 file changed, 1 insertion(+) diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js index ca3bf8c10..2216f979d 100644 --- a/libloki/libloki-protocol.js +++ b/libloki/libloki-protocol.js @@ -132,6 +132,7 @@ async function sendFriendRequestAccepted(pubKey) { return sendEmptyMessage(pubKey); } + async function sendEmptyMessage(pubKey) { // empty content message const content = new textsecure.protobuf.Content();