Refactor session reset handling

pull/843/head
Mikunj Varsani 5 years ago
parent 5fc14d2a7e
commit 8ffb1a0a10

@ -63,7 +63,7 @@ module.exports = {
// high value as a buffer to let Prettier control the line length: // high value as a buffer to let Prettier control the line length:
code: 999, code: 999,
// We still want to limit comments as before: // We still want to limit comments as before:
comments: 90, comments: 150,
ignoreUrls: true, ignoreUrls: true,
ignoreRegExpLiterals: true, ignoreRegExpLiterals: true,
}, },

@ -475,7 +475,6 @@ SecretSessionCipher.prototype = {
// private byte[] decrypt(UnidentifiedSenderMessageContent message) // private byte[] decrypt(UnidentifiedSenderMessageContent message)
_decryptWithUnidentifiedSenderMessage(message) { _decryptWithUnidentifiedSenderMessage(message) {
const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const sender = new libsignal.SignalProtocolAddress( const sender = new libsignal.SignalProtocolAddress(
@ -485,12 +484,12 @@ SecretSessionCipher.prototype = {
switch (message.type) { switch (message.type) {
case CiphertextMessage.WHISPER_TYPE: case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher( return new libloki.crypto.LokiSessionCipher(
signalProtocolStore, signalProtocolStore,
sender sender
).decryptWhisperMessage(message.content); ).decryptWhisperMessage(message.content);
case CiphertextMessage.PREKEY_TYPE: case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher( return new libloki.crypto.LokiSessionCipher(
signalProtocolStore, signalProtocolStore,
sender sender
).decryptPreKeyWhisperMessage(message.content); ).decryptPreKeyWhisperMessage(message.content);

@ -324,6 +324,146 @@
GRANT: 2, 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 = { window.libloki.crypto = {
DHEncrypt, DHEncrypt,
DHDecrypt, DHDecrypt,
@ -336,6 +476,7 @@
verifyAuthorisation, verifyAuthorisation,
validateAuthorisation, validateAuthorisation,
PairingType, PairingType,
LokiSessionCipher,
// for testing // for testing
_LokiSnodeChannel: LokiSnodeChannel, _LokiSnodeChannel: LokiSnodeChannel,
_decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey, _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,

@ -667,58 +667,29 @@ MessageReceiver.prototype.extend({
async decrypt(envelope, ciphertext) { async decrypt(envelope, ciphertext) {
let promise; 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 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 = { const me = {
number: ourNumber, number: ourNumber,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10), deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
}; };
// Will become obsolete // Envelope.source will be null on UNIDENTIFIED_SENDER
const getCurrentSessionBaseKey = async () => { // Don't use it there!
const record = await sessionCipher.getRecord(address.toString()); const address = new libsignal.SignalProtocolAddress(
if (!record) { envelope.source,
return null; envelope.sourceDevice
} );
const openSession = record.getOpenSession();
if (!openSession) {
return null;
}
const { baseKey } = openSession.indexInfo;
return baseKey;
};
// Will become obsolete const lokiSessionCipher = new libloki.crypto.LokiSessionCipher(
const captureActiveSession = async () => { textsecure.storage.protocol,
this.activeSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); address
}; );
switch (envelope.type) { switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT: case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
window.log.info('message from', this.getEnvelopeId(envelope)); window.log.info('message from', this.getEnvelopeId(envelope));
promise = captureActiveSession() promise = lokiSessionCipher
.then(() => sessionCipher.decryptWhisperMessage(ciphertext)) .decryptWhisperMessage(ciphertext)
.then(this.unpad); .then(this.unpad);
break; break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
@ -735,25 +706,11 @@ MessageReceiver.prototype.extend({
} }
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', this.getEnvelopeId(envelope)); window.log.info('prekey message from', this.getEnvelopeId(envelope));
promise = captureActiveSession(sessionCipher).then(async () => { promise = this.decryptPreKeyWhisperMessage(
if (!this.activeSessionBaseKey) { ciphertext,
try { lokiSessionCipher,
const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); address
await window.libloki.storage.verifyFriendRequestAcceptPreKey( );
envelope.source,
buffer
);
} catch (e) {
await this.removeFromCache(envelope);
throw e;
}
}
return this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
);
});
break; break;
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: { case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: {
window.log.info('received unidentified sender message'); window.log.info('received unidentified sender message');
@ -856,72 +813,6 @@ MessageReceiver.prototype.extend({
window.log.info('Error getting conversation: ', envelope.source); 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 // Type here can actually be UNIDENTIFIED_SENDER even if
// the underlying message is FRIEND_REQUEST // the underlying message is FRIEND_REQUEST
if ( if (

Loading…
Cancel
Save