Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor session reset handling #843

Merged
merged 6 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
5 changes: 2 additions & 3 deletions js/modules/metadata/SecretSessionCipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand Down
141 changes: 141 additions & 0 deletions libloki/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
neuroscr marked this conversation as resolved.
Show resolved Hide resolved
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
);
neuroscr marked this conversation as resolved.
Show resolved Hide resolved
}

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) {
neuroscr marked this conversation as resolved.
Show resolved Hide resolved
if (conversation.isSessionResetReceived()) {
neuroscr marked this conversation as resolved.
Show resolved Hide resolved
// 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];
Mikunj marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -336,6 +476,7 @@
verifyAuthorisation,
validateAuthorisation,
PairingType,
LokiSessionCipher,
// for testing
_LokiSnodeChannel: LokiSnodeChannel,
_decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,
Expand Down
143 changes: 17 additions & 126 deletions libtextsecure/message_receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
neuroscr marked this conversation as resolved.
Show resolved Hide resolved
.then(this.unpad);
break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
Expand All @@ -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');
Expand Down Expand Up @@ -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 (
Expand Down