From 8ffb1a0a10997e8f108accc58d95940d428799ea Mon Sep 17 00:00:00 2001 From: Mikunj Varsani Date: Thu, 13 Feb 2020 12:29:37 +1100 Subject: [PATCH] Refactor session reset handling --- .eslintrc.js | 2 +- js/modules/metadata/SecretSessionCipher.js | 5 +- libloki/crypto.js | 141 ++++++++++++++++++++ libtextsecure/message_receiver.js | 143 +++------------------ 4 files changed, 161 insertions(+), 130 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 4ce192bbb..5b34cf3da 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -63,7 +63,7 @@ module.exports = { // high value as a buffer to let Prettier control the line length: code: 999, // We still want to limit comments as before: - comments: 90, + comments: 150, ignoreUrls: true, ignoreRegExpLiterals: true, }, diff --git a/js/modules/metadata/SecretSessionCipher.js b/js/modules/metadata/SecretSessionCipher.js index 61420a122..bf1fb54ed 100644 --- a/js/modules/metadata/SecretSessionCipher.js +++ b/js/modules/metadata/SecretSessionCipher.js @@ -475,7 +475,6 @@ SecretSessionCipher.prototype = { // private byte[] decrypt(UnidentifiedSenderMessageContent message) _decryptWithUnidentifiedSenderMessage(message) { - const { SessionCipher } = this; const signalProtocolStore = this.storage; const sender = new libsignal.SignalProtocolAddress( @@ -485,12 +484,12 @@ SecretSessionCipher.prototype = { switch (message.type) { case CiphertextMessage.WHISPER_TYPE: - return new SessionCipher( + return new libloki.crypto.LokiSessionCipher( signalProtocolStore, sender ).decryptWhisperMessage(message.content); case CiphertextMessage.PREKEY_TYPE: - return new SessionCipher( + return new libloki.crypto.LokiSessionCipher( signalProtocolStore, sender ).decryptPreKeyWhisperMessage(message.content); diff --git a/libloki/crypto.js b/libloki/crypto.js index dfaaf78e4..740d7d65a 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -324,6 +324,146 @@ GRANT: 2, }); + /** + * A wrapper around Signal's SessionCipher. + * This handles specific session reset logic that we need. + */ + class LokiSessionCipher { + constructor(storage, address) { + this.storage = storage; + this.address = address; + this.sessionCipher = new libsignal.SessionCipher(storage, address); + } + + async decryptWhisperMessage(buffer, encoding) { + // Capture active session + const activeSessionBaseKey = await this._getCurrentSessionBaseKey(); + + const promise = this.sessionCipher.decryptWhisperMessage( + buffer, + encoding + ); + + // Handle session reset + // eslint-disable-next-line more/no-then + promise.then(() => { + this._handleSessionResetIfNeeded(activeSessionBaseKey); + }); + + return promise; + } + + async decryptPreKeyWhisperMessage(buffer, encoding) { + // Capture active session + const activeSessionBaseKey = await this._getCurrentSessionBaseKey(); + + if (!activeSessionBaseKey) { + const wrapped = dcodeIO.ByteBuffer.wrap(buffer); + await window.libloki.storage.verifyFriendRequestAcceptPreKey( + this.address.getName(), + wrapped + ); + } + + const promise = this.sessionCipher.decryptPreKeyWhisperMessage( + buffer, + encoding + ); + + // Handle session reset + // eslint-disable-next-line more/no-then + promise.then(() => { + this._handleSessionResetIfNeeded(activeSessionBaseKey); + }); + + return promise; + } + + async _handleSessionResetIfNeeded(previousSessionBaseKey) { + if (!previousSessionBaseKey) { + return; + } + + let conversation; + try { + conversation = await window.ConversationController.getOrCreateAndWait( + this.address.getName(), + 'private' + ); + } catch (e) { + window.log.info('Error getting conversation: ', this.address.getName()); + return; + } + + if (conversation.isSessionResetOngoing()) { + const currentSessionBaseKey = await this._getCurrentSessionBaseKey(); + if (currentSessionBaseKey !== previousSessionBaseKey) { + if (conversation.isSessionResetReceived()) { + // The other user used an old session to contact us; wait for them to switch to a new one. + await this._restoreSession(previousSessionBaseKey); + } else { + // Our session reset was successful; we initiated one and got a new session back from the other user. + await this._deleteAllSessionExcept(currentSessionBaseKey); + await conversation.onNewSessionAdopted(); + } + } else if (conversation.isSessionResetReceived()) { + // Our session reset was successful; we received a message with the same session from the other user. + await this._deleteAllSessionExcept(previousSessionBaseKey); + await conversation.onNewSessionAdopted(); + } + } + } + + async _getCurrentSessionBaseKey() { + const record = await this.sessionCipher.getRecord( + this.address.toString() + ); + if (!record) { + return null; + } + const openSession = record.getOpenSession(); + if (!openSession) { + return null; + } + const { baseKey } = openSession.indexInfo; + return baseKey; + } + + async _restoreSession(sessionBaseKey) { + const record = await this.sessionCipher.getRecord( + this.address.toString() + ); + if (!record) { + return; + } + record.archiveCurrentState(); + + const sessionToRestore = record.sessions[sessionBaseKey]; + record.promoteState(sessionToRestore); + record.updateSessionState(sessionToRestore); + await this.storage.storeSession( + this.address.toString(), + record.serialize() + ); + } + + async _deleteAllSessionExcept(sessionBaseKey) { + const record = await this.sessionCipher.getRecord( + this.address.toString() + ); + if (!record) { + return; + } + const sessionToKeep = record.sessions[sessionBaseKey]; + record.sessions = {}; + record.updateSessionState(sessionToKeep); + await this.storage.storeSession( + this.address.toString(), + record.serialize() + ); + } + } + window.libloki.crypto = { DHEncrypt, DHDecrypt, @@ -336,6 +476,7 @@ verifyAuthorisation, validateAuthorisation, PairingType, + LokiSessionCipher, // for testing _LokiSnodeChannel: LokiSnodeChannel, _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey, diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 0af1a190b..c3aec94d0 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -667,58 +667,29 @@ MessageReceiver.prototype.extend({ async decrypt(envelope, ciphertext) { let promise; - // We don't have source at this point yet (with sealed sender) - // This needs a massive cleanup! - const address = new libsignal.SignalProtocolAddress( - envelope.source, - envelope.sourceDevice - ); - const ourNumber = textsecure.storage.user.getNumber(); - const number = address.toString().split('.')[0]; - const options = {}; - - // No limit on message keys if we're communicating with our other devices - if (ourNumber === number) { - options.messageKeysLimit = false; - } - - // Will become obsolete - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address, - options - ); - const me = { number: ourNumber, deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10), }; - // Will become obsolete - 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; - }; + // Envelope.source will be null on UNIDENTIFIED_SENDER + // Don't use it there! + const address = new libsignal.SignalProtocolAddress( + envelope.source, + envelope.sourceDevice + ); - // Will become obsolete - const captureActiveSession = async () => { - this.activeSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); - }; + const lokiSessionCipher = new libloki.crypto.LokiSessionCipher( + textsecure.storage.protocol, + address + ); switch (envelope.type) { case textsecure.protobuf.Envelope.Type.CIPHERTEXT: window.log.info('message from', this.getEnvelopeId(envelope)); - promise = captureActiveSession() - .then(() => sessionCipher.decryptWhisperMessage(ciphertext)) + promise = lokiSessionCipher + .decryptWhisperMessage(ciphertext) .then(this.unpad); break; case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { @@ -735,25 +706,11 @@ MessageReceiver.prototype.extend({ } case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: window.log.info('prekey message from', this.getEnvelopeId(envelope)); - promise = captureActiveSession(sessionCipher).then(async () => { - if (!this.activeSessionBaseKey) { - try { - const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); - await window.libloki.storage.verifyFriendRequestAcceptPreKey( - envelope.source, - buffer - ); - } catch (e) { - await this.removeFromCache(envelope); - throw e; - } - } - return this.decryptPreKeyWhisperMessage( - ciphertext, - sessionCipher, - address - ); - }); + promise = this.decryptPreKeyWhisperMessage( + ciphertext, + lokiSessionCipher, + address + ); break; case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: { window.log.info('received unidentified sender message'); @@ -856,72 +813,6 @@ MessageReceiver.prototype.extend({ window.log.info('Error getting conversation: ', envelope.source); } - // lint hates anything after // (so /// is no good) - // *** BEGIN: session reset *** - - // we have address in scope from parent scope - // seems to be the same input parameters - // going to comment out due to lint complaints - /* - const address = new libsignal.SignalProtocolAddress( - envelope.source, - envelope.sourceDevice - ); - */ - - const restoreActiveSession = async () => { - const record = await sessionCipher.getRecord(address.toString()); - if (!record) { - return; - } - record.archiveCurrentState(); - - // NOTE: activeSessionBaseKey will be undefined here... - 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() - ); - }; - - if (conversation.isSessionResetOngoing()) { - 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(); - } - } - - // lint hates anything after // (so /// is no good) - // *** END *** - // Type here can actually be UNIDENTIFIED_SENDER even if // the underlying message is FRIEND_REQUEST if (