Merge pull request #843 from loki-project/session-reset-cleanup

Refactor session reset handling
pull/857/head
Mikunj Varsani 5 years ago committed by GitHub
commit cea5acf62e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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,154 @@
GRANT: 2, GRANT: 2,
}); });
/**
* A wrapper around Signal's SessionCipher.
* This handles specific session reset logic that we need.
*/
class LokiSessionCipher {
constructor(storage, protocolAddress) {
this.storage = storage;
this.protocolAddress = protocolAddress;
this.sessionCipher = new libsignal.SessionCipher(
storage,
protocolAddress
);
this.TYPE = Object.freeze({
MESSAGE: 1,
PREKEY: 2,
});
}
decryptWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.MESSAGE, buffer, encoding);
}
decryptPreKeyWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.PREKEY, buffer, encoding);
}
async _decryptMessage(type, buffer, encoding) {
// Capture active session
const activeSessionBaseKey = await this._getCurrentSessionBaseKey();
if (type === this.TYPE.PREKEY && !activeSessionBaseKey) {
const wrapped = dcodeIO.ByteBuffer.wrap(buffer);
await window.libloki.storage.verifyFriendRequestAcceptPreKey(
this.protocolAddress.getName(),
wrapped
);
}
const decryptFunction =
type === this.TYPE.PREKEY
? this.sessionCipher.decryptPreKeyWhisperMessage
: this.sessionCipher.decryptWhisperMessage;
const result = await decryptFunction(buffer, encoding);
// Handle session reset
// This needs to be done synchronously so that the next time we decrypt a message,
// we have the correct session
try {
await this._handleSessionResetIfNeeded(activeSessionBaseKey);
} catch (e) {
window.log.info('Failed to handle session reset: ', e);
}
return result;
}
async _handleSessionResetIfNeeded(previousSessionBaseKey) {
if (!previousSessionBaseKey) {
return;
}
let conversation;
try {
conversation = await window.ConversationController.getOrCreateAndWait(
this.protocolAddress.getName(),
'private'
);
} catch (e) {
window.log.info(
'Error getting conversation: ',
this.protocolAddress.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.protocolAddress.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.protocolAddress.toString()
);
if (!record) {
return;
}
record.archiveCurrentState();
const sessionToRestore = record.sessions[sessionBaseKey];
if (!sessionToRestore) {
throw new Error(`Cannot find session with base key ${sessionBaseKey}`);
}
record.promoteState(sessionToRestore);
record.updateSessionState(sessionToRestore);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
async _deleteAllSessionExcept(sessionBaseKey) {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return;
}
const sessionToKeep = record.sessions[sessionBaseKey];
record.sessions = {};
record.updateSessionState(sessionToKeep);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
}
window.libloki.crypto = { window.libloki.crypto = {
DHEncrypt, DHEncrypt,
DHDecrypt, DHDecrypt,
@ -336,6 +484,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 (
@ -1470,6 +1361,7 @@ MessageReceiver.prototype.extend({
content.preKeyBundleMessage content.preKeyBundleMessage
); );
} }
if (content.lokiAddressMessage) { if (content.lokiAddressMessage) {
return this.handleLokiAddressMessage( return this.handleLokiAddressMessage(
envelope, envelope,
@ -1833,7 +1725,7 @@ MessageReceiver.prototype.extend({
textsecure.storage.protocol, textsecure.storage.protocol,
address address
); );
builder.processPreKey(device); await builder.processPreKey(device);
}) })
); );
await conversation.onSessionResetReceived(); await conversation.onSessionResetReceived();

@ -391,6 +391,8 @@ OutgoingMessage.prototype = {
: null; : null;
const isEndSession = const isEndSession =
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
const isSessionRequest =
flags === textsecure.protobuf.DataMessage.Flags.SESSION_REQUEST;
const signalCipher = new libsignal.SessionCipher( const signalCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
address address
@ -485,6 +487,7 @@ OutgoingMessage.prototype = {
content, content,
pubKey: devicePubKey, pubKey: devicePubKey,
isFriendRequest: enableFallBackEncryption, isFriendRequest: enableFallBackEncryption,
isSessionRequest,
}; };
}) })
) )
@ -494,7 +497,12 @@ OutgoingMessage.prototype = {
if (!outgoingObject) { if (!outgoingObject) {
return; return;
} }
const destination = outgoingObject.pubKey; const {
pubKey: destination,
ttl,
isFriendRequest,
isSessionRequest,
} = outgoingObject;
try { try {
const socketMessage = await this.wrapInWebsocketMessage( const socketMessage = await this.wrapInWebsocketMessage(
outgoingObject outgoingObject
@ -503,9 +511,9 @@ OutgoingMessage.prototype = {
destination, destination,
socketMessage, socketMessage,
this.timestamp, this.timestamp,
outgoingObject.ttl ttl
); );
if (outgoingObject.isFriendRequest) { if (!this.isGroup && isFriendRequest && !isSessionRequest) {
const conversation = ConversationController.get(destination); const conversation = ConversationController.get(destination);
if (conversation) { if (conversation) {
// Redundant for primary device but marks secondary devices as pending // Redundant for primary device but marks secondary devices as pending

@ -1041,64 +1041,6 @@ MessageSender.prototype = {
silent, silent,
options options
).catch(logError('resetSession/sendToContact error:')); ).catch(logError('resetSession/sendToContact error:'));
/*
const deleteAllSessions = targetNumber =>
textsecure.storage.protocol.getDeviceIds(targetNumber).then(deviceIds =>
Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(
targetNumber,
deviceId
);
window.log.info('deleting sessions for', address.toString());
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
)
);
const sendToContactPromise = deleteAllSessions(number)
.catch(logError('resetSession/deleteAllSessions1 error:'))
.then(() => {
window.log.info(
'finished closing local sessions, now sending to contact'
);
return this.sendIndividualProto(
number,
proto,
timestamp,
silent,
options
).catch(logError('resetSession/sendToContact error:'));
})
.then(() =>
deleteAllSessions(number).catch(
logError('resetSession/deleteAllSessions2 error:')
)
);
const myNumber = textsecure.storage.user.getNumber();
// We already sent the reset session to our other devices in the code above!
if (number === myNumber) {
return sendToContactPromise;
}
const buffer = proto.toArrayBuffer();
const sendSyncPromise = this.sendSyncMessage(
buffer,
timestamp,
number,
null,
[],
[],
options
).catch(logError('resetSession/sendSync error:'));
return Promise.all([sendToContact, sendSyncPromise]);
*/
}, },
async sendMessageToGroup( async sendMessageToGroup(

Loading…
Cancel
Save