diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 38ae0a326..b38ab944b 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -2267,5 +2267,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": ""
+ }
+ }
}
}
diff --git a/background.html b/background.html
index a888d3a37..eeda839c3 100644
--- a/background.html
+++ b/background.html
@@ -819,6 +819,7 @@
+
diff --git a/js/background.js b/js/background.js
index b191176ea..d5bb0898f 100644
--- a/js/background.js
+++ b/js/background.js
@@ -1042,6 +1042,12 @@
}
});
+ Whisper.events.on('showSessionRestoreConfirmation', options => {
+ if (appView) {
+ appView.showSessionRestoreConfirmation(options);
+ }
+ });
+
Whisper.events.on('showNicknameDialog', options => {
if (appView) {
appView.showNicknameDialog(options);
@@ -1933,6 +1939,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));
diff --git a/js/models/conversations.js b/js/models/conversations.js
index af7c85828..0833f96c4 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -1437,7 +1437,8 @@
attachments,
quote,
preview,
- groupInvitation = null
+ groupInvitation = null,
+ otherOptions = {}
) {
this.clearTypingTimers();
@@ -1533,9 +1534,13 @@
messageWithSchema.source = textsecure.storage.user.getNumber();
messageWithSchema.sourceDevice = 1;
}
+
+ const { sessionRestoration = false } = otherOptions;
+
const attributes = {
...messageWithSchema,
groupInvitation,
+ sessionRestoration,
id: window.getGuid(),
};
@@ -1616,6 +1621,7 @@
}
options.groupInvitation = groupInvitation;
+ options.sessionRestoration = sessionRestoration;
const groupNumbers = this.getRecipients();
diff --git a/js/models/messages.js b/js/models/messages.js
index df8601e5c..4bdf62f14 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -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()) {
@@ -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) {
@@ -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,
@@ -1979,6 +1998,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
diff --git a/js/views/app_view.js b/js/views/app_view.js
index df13f0a3d..dc183c445 100644
--- a/js/views/app_view.js
+++ b/js/views/app_view.js
@@ -254,6 +254,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);
diff --git a/js/views/confirm_session_reset_view.js b/js/views/confirm_session_reset_view.js
new file mode 100644
index 000000000..aa34e420f
--- /dev/null
+++ b/js/views/confirm_session_reset_view.js
@@ -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();
+ },
+ });
+})();
diff --git a/js/views/message_view.js b/js/views/message_view.js
index 32d87a78b..1f82fcdb3 100644
--- a/js/views/message_view.js
+++ b/js/views/message_view.js
@@ -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,
diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js
index 7092f2ab6..daf166796 100644
--- a/libtextsecure/message_receiver.js
+++ b/libtextsecure/message_receiver.js
@@ -825,7 +825,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));
@@ -975,6 +974,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
@@ -984,8 +986,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');
@@ -1895,6 +1897,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) {
diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js
index b2b23d2ea..0a71ec302 100644
--- a/libtextsecure/sendmessage.js
+++ b/libtextsecure/sendmessage.js
@@ -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');
@@ -171,6 +172,10 @@ Message.prototype = {
);
}
+ if (this.sessionRestoration) {
+ proto.flags = textsecure.protobuf.DataMessage.Flags.SESSION_RESTORE;
+ }
+
this.dataMessage = proto;
return proto;
},
@@ -952,7 +957,7 @@ MessageSender.prototype = {
? textsecure.protobuf.DataMessage.Flags.BACKGROUND_FRIEND_REQUEST
: undefined;
- const { groupInvitation } = options;
+ const { groupInvitation, sessionRestoration } = options;
return this.sendMessage(
{
@@ -968,6 +973,7 @@ MessageSender.prototype = {
profile,
flags,
groupInvitation,
+ sessionRestoration,
},
options
);
diff --git a/protos/SignalService.proto b/protos/SignalService.proto
index e76906d2b..9335e32a5 100644
--- a/protos/SignalService.proto
+++ b/protos/SignalService.proto
@@ -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;
}
diff --git a/test/index.html b/test/index.html
index cdeaea87e..1e2c31aad 100644
--- a/test/index.html
+++ b/test/index.html
@@ -576,6 +576,7 @@
+