Refactor session reset handling

This commit is contained in:
Mikunj Varsani 2020-02-13 12:29:37 +11:00
parent 5fc14d2a7e
commit 8ffb1a0a10
4 changed files with 161 additions and 130 deletions

View File

@ -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,
},

View File

@ -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);

View File

@ -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,

View File

@ -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 (