Handle session reset, Loki style

pull/66/head
sachaaaaa 6 years ago
parent fd45da35ce
commit 7e612700f7

@ -891,8 +891,13 @@
"description":
"Confirmation dialog text that asks the user if they really wish to delete the conversation. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone."
},
"sessionResetOngoing": {
"message": "Secure session reset in progress",
"description":
"your secure session is currently being reset, waiting for the reset acknowledgment."
},
"sessionEnded": {
"message": "Secure session reset",
"message": "Secure session reset done",
"description":
"This is a past tense, informational message. In other words, your secure session has been reset."
},

@ -53,6 +53,16 @@
friends: 4,
});
// Possible session reset states
const SessionResetEnum = Object.freeze({
// No ongoing reset
none: 0,
// we initiated the session reset
initiated: 1,
// we received the session reset
request_received: 2,
});
const COLORS = [
'red',
'deep_orange',
@ -75,6 +85,7 @@
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
friendRequestStatus: FriendRequestStatusEnum.none,
unlockTimestamp: null, // Timestamp used for expiring friend requests.
sessionResetStatus: FriendStatusEnum.none,
};
},
@ -1421,30 +1432,77 @@
return !this.get('left');
},
async endSession() {
if (this.isPrivate()) {
const now = Date.now();
const message = this.messageCollection.add({
conversationId: this.id,
type: 'outgoing',
sent_at: now,
received_at: now,
destination: this.id,
recipients: this.getRecipients(),
flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
async onSessionResetInitiated() {
if (this.get('sessionResetStatus') === SessionResetEnum.none) {
this.set({ sessionResetStatus : SessionResetEnum.initiated });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
}
},
const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,
async onSessionResetReceived() {
if (this.get('sessionResetStatus') === SessionResetEnum.none) {
this.set({ sessionResetStatus : SessionResetEnum.request_received });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
message.set({ id });
// send empty message, this will trigger the new session to propagate
// to the reset initiator.
window.libloki.sendEmptyMessage(this.id);
}
},
const options = this.getSendOptions();
message.send(
this.wrapSend(
textsecure.messaging.resetSession(this.id, now, options)
)
);
isSessionResetReceived() {
return this.get('sessionResetStatus') === SessionResetEnum.request_received;
},
async createAndStoreEndSessionMessage(endSessionType) {
const now = Date.now();
const message = this.messageCollection.add({
conversationId: this.id,
type: 'outgoing',
sent_at: now,
received_at: now,
destination: this.id,
recipients: this.getRecipients(),
flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
endSessionType,
});
const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,
});
message.set({ id });
return message;
},
async onNewSessionAdopted() {
if (this.get('sessionResetStatus') === SessionResetEnum.initiated) {
// send empty message to confirm that we have adopted the new session
window.libloki.sendEmptyMessage(this.id);
}
this.createAndStoreEndSessionMessage('done');
this.set({ sessionResetStatus : SessionResetEnum.none });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
},
async endSession() {
if (this.isPrivate()) {
// Only create a new message if we initiated the session reset.
// On the receiver side, the actual message containing the END_SESSION flag
// will ensure the "session reset" message will be added to their conversation.
if (this.get('sessionResetStatus') === SessionResetEnum.initiated) {
const message = await this.createAndStoreEndSessionMessage('ongoing');
const options = this.getSendOptions();
message.send(
this.wrapSend(
textsecure.messaging.resetSession(this.id, message.get('sent_at'), options)
)
);
}
}
},

@ -108,6 +108,12 @@
// eslint-disable-next-line no-bitwise
return !!(this.get('flags') & flag);
},
getEndSessionTranslationKey() {
if (this.get('endSessionType') === 'ongoing') {
return 'sessionResetOngoing';
}
return 'sessionEnded';
},
isExpirationTimerUpdate() {
const flag =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
@ -174,7 +180,7 @@
return messages.join(', ');
}
if (this.isEndSession()) {
return i18n('sessionEnded');
return i18n(this.getEndSessionTranslationKey());
}
if (this.isIncoming() && this.hasErrors()) {
return i18n('incomingError');
@ -294,8 +300,9 @@
};
},
getPropsForResetSessionNotification() {
// It doesn't need anything right now!
return {};
return {
sessionResetMessageKey: this.getEndSessionTranslationKey(),
};
},
async acceptFriendRequest() {
@ -1303,6 +1310,11 @@
message.get('received_at')
);
}
} else {
const endSessionType = conversation.isSessionResetReceived()
? 'ongoing'
: 'done';
this.set({ endSessionType });
}
if (type === 'incoming' || type === 'friend-request') {
const readSync = Whisper.ReadSyncs.forMessage(message);

@ -1258,7 +1258,8 @@
}
},
endSession() {
async endSession() {
await this.model.onSessionResetInitiated();
this.model.endSession();
},

@ -130,6 +130,9 @@
}
async function sendFriendRequestAccepted(pubKey) {
return sendEmptyMessage(pubKey);
}
async function sendEmptyMessage(pubKey) {
// empty content message
const content = new textsecure.protobuf.Content();
@ -161,4 +164,5 @@
window.libloki.savePreKeyBundleForNumber = savePreKeyBundleForNumber;
window.libloki.removePreKeyBundleForNumber = removePreKeyBundleForNumber;
window.libloki.sendFriendRequestAccepted = sendFriendRequestAccepted;
window.libloki.sendEmptyMessage = sendEmptyMessage;
})();

@ -717,11 +717,70 @@ MessageReceiver.prototype.extend({
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
};
let conversation;
try {
conversation = await window.ConversationController.getOrCreateAndWait(envelope.source, 'private');
} catch (e) {
window.log.info('Error getting conversation: ', envelope.source);
}
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
};
const captureActiveSession = async () => {
this.activeSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher);
};
const restoreActiveSession = async () => {
const record = await sessionCipher.getRecord(address.toString());
if (!record)
return
record.archiveCurrentState();
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());
};
const handleSessionReset = async () => {
const currentSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher);
// console.warn('%cdecipher session %s', 'color:red;', currentSessionBaseKey);
if (this.activeSessionBaseKey && currentSessionBaseKey !== this.activeSessionBaseKey) {
if (conversation.isSessionResetReceived()) {
restoreActiveSession();
} else {
deleteAllSessionExcept(currentSessionBaseKey);
conversation.onNewSessionAdopted();
}
} else if (conversation.isSessionResetReceived()) {
deleteAllSessionExcept(this.activeSessionBaseKey);
conversation.onNewSessionAdopted();
}
};
switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
window.log.info('message from', this.getEnvelopeId(envelope));
promise = sessionCipher.decryptWhisperMessage(ciphertext)
.then(this.unpad);
promise = captureActiveSession()
.then(() => sessionCipher.decryptWhisperMessage(ciphertext))
.then(this.unpad)
.then((plainText) => {
handleSessionReset();
return plainText;
});
break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
window.log.info('friend-request message from ', envelope.source);
@ -731,11 +790,16 @@ MessageReceiver.prototype.extend({
}
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', this.getEnvelopeId(envelope));
promise = this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
);
promise = captureActiveSession(sessionCipher)
.then(() => this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
))
.then((plainText) => {
handleSessionReset();
return plainText;
});
break;
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER:
window.log.info('received unidentified sender message');
@ -911,6 +975,9 @@ MessageReceiver.prototype.extend({
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(envelope.source);
}
const type = (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST)
? 'friend-request'
: 'data';
return p.then(() =>
this.processDecrypted(envelope, msg, envelope.source).then(message => {
const groupId = message.group && message.group.id;
@ -1282,17 +1349,37 @@ MessageReceiver.prototype.extend({
async handleEndSession(number) {
window.log.info('got end session');
const deviceIds = await textsecure.storage.protocol.getDeviceIds(number);
const identityKey = StringView.hexToArrayBuffer(number);
let conversation;
try {
conversation = window.ConversationController.get(number);
} catch (e) {
window.log.error('Error getting conversation: ', number);
}
conversation.onSessionResetReceived();
return Promise.all(
deviceIds.map(deviceId => {
deviceIds.map(async deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
const sessionCipher = new libsignal.SessionCipher(
// Instead of deleting the sessions now,
// we process the new prekeys and initiate a new session.
// The old sessions will get deleted once the correspondant
// has switch the the new session.
const [preKey, signedPreKey] = await Promise.all([
textsecure.storage.protocol.loadContactPreKey(number),
textsecure.storage.protocol.loadContactSignedPreKey(number),
]);
if (preKey === undefined || signedPreKey === undefined) {
return null;
}
const device = { identityKey, deviceId, preKey, signedPreKey, registrationId: 0 }
const builder = new libsignal.SessionBuilder(
textsecure.storage.protocol,
address
);
window.log.info('deleting sessions for', address.toString());
return sessionCipher.deleteAllSessionsForDevice();
builder.processPreKey(device);
return null;
})
);
},

@ -119,7 +119,12 @@ OutgoingMessage.prototype = {
if (device.registrationId === 0) {
window.log.info('device registrationId 0!');
}
return builder.processPreKey(device).then(() => true).catch(error => {
return builder.processPreKey(device).then(async () => {
// TODO: only remove the keys that were used above!
await window.libloki.removePreKeyBundleForNumber(number);
return true;
}
).catch(error => {
if (error.message === 'Identity key changed') {
// eslint-disable-next-line no-param-reassign
error.timestamp = this.timestamp;
@ -285,10 +290,15 @@ OutgoingMessage.prototype = {
// Check if we need to attach the preKeys
let sessionCipher;
if (this.messageType === 'friend-request') {
const isFriendRequest = this.messageType === 'friend-request';
const flags = this.message.dataMessage ? this.message.dataMessage.get_flags() : null;
const isEndSession = flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
if (isFriendRequest || isEndSession) {
// Encrypt them with the fallback
this.message.preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number);
window.log.info('attaching prekeys to outgoing message');
}
if (isFriendRequest) {
sessionCipher = fallBackCipher;
} else {
sessionCipher = new libsignal.SessionCipher(

@ -691,6 +691,17 @@ MessageSender.prototype = {
window.log.error(prefix, error && error.stack ? error.stack : error);
throw error;
};
// The actual deletion of the session now happens later
// as we need to ensure the other contact has successfully
// switch to a new session first.
return this.sendIndividualProto(
number,
proto,
timestamp,
silent,
options
).catch(logError('resetSession/sendToContact error:'));
/*
const deleteAllSessions = targetNumber =>
textsecure.storage.protocol.getDeviceIds(targetNumber).then(deviceIds =>
Promise.all(
@ -741,6 +752,7 @@ MessageSender.prototype = {
).catch(logError('resetSession/sendSync error:'));
return Promise.all([sendToContact, sendSync]);
*/
},
sendMessageToGroup(

@ -4,15 +4,16 @@ import { Localizer } from '../../types/Util';
interface Props {
i18n: Localizer;
sessionResetMessageKey: string;
}
export class ResetSessionNotification extends React.Component<Props> {
public render() {
const { i18n } = this.props;
const { i18n, sessionResetMessageKey } = this.props;
return (
<div className="module-reset-session-notification">
{i18n('sessionEnded')}
{ i18n(sessionResetMessageKey) }
</div>
);
}

Loading…
Cancel
Save