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

Prompting the user to reset session on invalid ciphertext #669

Merged
merged 2 commits into from
Dec 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2241,5 +2241,18 @@
},
"noFriendsToAdd": {
"message": "no friends to add"
},
"couldNotDecryptMessage": {
"message": "Couldn't decrypt a message"
},
"confirmSessionRestore": {
"message":
"Would you like to start a new session with $pubkey$? Only do so if you know this pubkey.",
"placeholders": {
"pubkey": {
"content": "$1",
"example": ""
}
}
}
}
1 change: 1 addition & 0 deletions background.html
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,7 @@ <h4 class='section-toggle'>Link device to an existing account</h4>
<script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script>
<script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script>
<script type='text/javascript' src='js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/confirm_session_reset_view.js'></script>
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/views/invite_friends_dialog_view.js'></script>

Expand Down
48 changes: 48 additions & 0 deletions js/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,12 @@
}
});

Whisper.events.on('showSessionRestoreConfirmation', options => {
if (appView) {
appView.showSessionRestoreConfirmation(options);
}
});

Whisper.events.on('showNicknameDialog', options => {
if (appView) {
appView.showNicknameDialog(options);
Expand Down Expand Up @@ -1889,6 +1895,48 @@
}

async function onError(ev) {
const noSession =
ev.error &&
ev.error.message &&
ev.error.message.indexOf('No record for device') === 0;
const pubkey = ev.proto.source;

if (noSession) {
const convo = await ConversationController.getOrCreateAndWait(
pubkey,
'private'
);

if (!convo.get('sessionRestoreSeen')) {
convo.set({ sessionRestoreSeen: true });

await window.Signal.Data.updateConversation(
convo.id,
convo.attributes,
{ Conversation: Whisper.Conversation }
);

window.Whisper.events.trigger('showSessionRestoreConfirmation', {
pubkey,
onOk: () => {
convo.sendMessage('', null, null, null, null, {
sessionRestoration: true,
});
},
});
} else {
window.log.verbose(
`Already seen session restore for pubkey: ${pubkey}`
);
if (ev.confirm) {
ev.confirm();
}
}

// We don't want to display any failed messages in the conversation:
return;
}

const { error } = ev;
window.log.error('background onError:', Errors.toLogFormat(error));

Expand Down
8 changes: 7 additions & 1 deletion js/models/conversations.js
Original file line number Diff line number Diff line change
Expand Up @@ -1423,7 +1423,8 @@
attachments,
quote,
preview,
groupInvitation = null
groupInvitation = null,
otherOptions = {}
) {
this.clearTypingTimers();

Expand Down Expand Up @@ -1514,9 +1515,13 @@
messageWithSchema.source = textsecure.storage.user.getNumber();
messageWithSchema.sourceDevice = 1;
}

const { sessionRestoration = false } = otherOptions;

const attributes = {
...messageWithSchema,
groupInvitation,
sessionRestoration,
id: window.getGuid(),
};

Expand Down Expand Up @@ -1597,6 +1602,7 @@
}

options.groupInvitation = groupInvitation;
options.sessionRestoration = sessionRestoration;

const groupNumbers = this.getRecipients();

Expand Down
28 changes: 28 additions & 0 deletions js/models/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
this.propsForResetSessionNotification = this.getPropsForResetSessionNotification();
} else if (this.isGroupUpdate()) {
this.propsForGroupNotification = this.getPropsForGroupNotification();
} else if (this.isSessionRestoration()) {
// do nothing
} else if (this.isFriendRequest()) {
this.propsForFriendRequest = this.getPropsForFriendRequest();
} else if (this.isGroupInvitation()) {
Expand Down Expand Up @@ -270,6 +272,13 @@
isGroupInvitation() {
return !!this.get('groupInvitation');
},
isSessionRestoration() {
const flag = textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
// eslint-disable-next-line no-bitwise
const sessionRestoreFlag = !!(this.get('flags') & flag);

return !!this.get('sessionRestoration') || sessionRestoreFlag;
},
getNotificationText() {
const description = this.getDescription();
if (description) {
Expand Down Expand Up @@ -390,6 +399,16 @@
}
const conversation = await this.getSourceDeviceConversation();

// If we somehow received an old friend request (e.g. after having restored
// from seed, we won't be able to accept it, we should initiate our own
// friend request to reset the session:
if (conversation.get('sessionRestoreSeen')) {
conversation.sendMessage('', null, null, null, null, {
sessionRestoration: true,
});
return;
}

this.set({ friendStatus: 'accepted' });
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
Expand Down Expand Up @@ -1922,6 +1941,15 @@
initialMessage.flags ===
textsecure.protobuf.DataMessage.Flags.BACKGROUND_FRIEND_REQUEST;

if (
// eslint-disable-next-line no-bitwise
initialMessage.flags &
textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE
) {
// Show that the session reset is "in progress" even though we had a valid session
this.set({ endSessionType: 'ongoing' });
}

if (message.isFriendRequest() && backgroundFrReq) {
// Check if the contact is a member in one of our private groups:
const groupMember = window
Expand Down
4 changes: 4 additions & 0 deletions js/views/app_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@
const dialog = new Whisper.UpdateGroupDialogView(groupConvo);
this.el.append(dialog.el);
},
showSessionRestoreConfirmation(options) {
const dialog = new Whisper.ConfirmSessionResetView(options);
this.el.append(dialog.el);
},
showLeaveGroupDialog(groupConvo) {
const dialog = new Whisper.LeaveGroupDialogView(groupConvo);
this.el.append(dialog.el);
Expand Down
50 changes: 50 additions & 0 deletions js/views/confirm_session_reset_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* global Whisper, i18n */

// eslint-disable-next-line func-names
(function() {
'use strict';

window.Whisper = window.Whisper || {};

Whisper.ConfirmSessionResetView = Whisper.View.extend({
className: 'loki-dialog modal',
initialize({ pubkey, onOk }) {
this.title = i18n('couldNotDecryptMessage');

this.onOk = onOk;
this.messageText = i18n('confirmSessionRestore', pubkey);
this.okText = i18n('yes');
this.cancelText = i18n('cancel');

this.close = this.close.bind(this);
this.confirm = this.confirm.bind(this);

this.$el.focus();
this.render();
},
render() {
this.dialogView = new Whisper.ReactWrapperView({
className: 'leave-group-dialog',
Component: window.Signal.Components.ConfirmDialog,
props: {
titleText: this.title,
messageText: this.messageText,
okText: this.okText,
cancelText: this.cancelText,
onConfirm: this.confirm,
onClose: this.close,
},
});

this.$el.append(this.dialogView.el);
return this;
},
async confirm() {
this.onOk();
this.close();
},
close() {
this.remove();
},
});
})();
5 changes: 5 additions & 0 deletions js/views/message_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
Component: Components.GroupNotification,
props: this.model.propsForGroupNotification,
};
} else if (this.model.isSessionRestoration()) {
return {
Component: Components.ResetSessionNotification,
props: this.model.getPropsForResetSessionNotification(),
};
} else if (this.model.propsForFriendRequest) {
return {
Component: Components.FriendRequest,
Expand Down
10 changes: 7 additions & 3 deletions libtextsecure/message_receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,6 @@ MessageReceiver.prototype.extend({
} else {
handleSessionReset = async result => result;
}

switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
window.log.info('message from', this.getEnvelopeId(envelope));
Expand Down Expand Up @@ -971,6 +970,9 @@ MessageReceiver.prototype.extend({
.catch(error => {
let errorToThrow = error;

const noSession =
error && error.message.indexOf('No record for device') === 0;

if (error && error.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
// user if they want to re-negotiate
Expand All @@ -980,8 +982,8 @@ MessageReceiver.prototype.extend({
buffer.toArrayBuffer(),
error.identityKey
);
} else {
// re-throw
} else if (!noSession) {
// We want to handle "no-session" error, not re-throw it
throw error;
}
const ev = new Event('error');
Expand Down Expand Up @@ -1850,6 +1852,8 @@ MessageReceiver.prototype.extend({
decrypted.attachments = [];
} else if (decrypted.flags & FLAGS.BACKGROUND_FRIEND_REQUEST) {
// do nothing
} else if (decrypted.flags & FLAGS.SESSION_RESTORE) {
// do nothing
} else if (decrypted.flags & FLAGS.UNPAIRING_REQUEST) {
// do nothing
} else if (decrypted.flags !== 0) {
Expand Down
8 changes: 7 additions & 1 deletion libtextsecure/sendmessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function Message(options) {
this.profileKey = options.profileKey;
this.profile = options.profile;
this.groupInvitation = options.groupInvitation;
this.sessionRestoration = options.sessionRestoration || false;

if (!(this.recipients instanceof Array)) {
throw new Error('Invalid recipient list');
Expand Down Expand Up @@ -171,6 +172,10 @@ Message.prototype = {
);
}

if (this.sessionRestoration) {
proto.flags = textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
}

this.dataMessage = proto;
return proto;
},
Expand Down Expand Up @@ -952,7 +957,7 @@ MessageSender.prototype = {
? textsecure.protobuf.DataMessage.Flags.BACKGROUND_FRIEND_REQUEST
: undefined;

const { groupInvitation } = options;
const { groupInvitation, sessionRestoration } = options;

return this.sendMessage(
{
Expand All @@ -968,6 +973,7 @@ MessageSender.prototype = {
profile,
flags,
groupInvitation,
sessionRestoration,
},
options
);
Expand Down
1 change: 1 addition & 0 deletions protos/SignalService.proto
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ message DataMessage {
END_SESSION = 1;
EXPIRATION_TIMER_UPDATE = 2;
PROFILE_KEY_UPDATE = 4;
SESSION_RESTORE = 64;
UNPAIRING_REQUEST = 128;
BACKGROUND_FRIEND_REQUEST = 256;
}
Expand Down
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ <h3>{{ message }}</h3>
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
<script type='text/javascript' src='../js/views/create_group_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/confirm_session_reset_view.js'></script>
<script type='text/javascript' src='../js/views/invite_friends_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/beta_release_disclaimer_view.js'></script>

Expand Down