diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 9da43268023c..f09c5027d4f9 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -48,6 +48,7 @@ Meteor.startup(function() { { _id: 'mention-here', roles : ['admin', 'owner', 'moderator', 'user'] }, { _id: 'mute-user', roles : ['admin', 'owner', 'moderator'] }, { _id: 'remove-user', roles : ['admin', 'owner', 'moderator'] }, + { _id: 'reset-other-user-e2e-key', roles : ['admin'] }, { _id: 'run-import', roles : ['admin'] }, { _id: 'run-migration', roles : ['admin'] }, { _id: 'set-moderator', roles : ['admin', 'owner'] }, diff --git a/packages/rocketchat-e2e/client/rocketchat.e2e.js b/packages/rocketchat-e2e/client/rocketchat.e2e.js index 8a401a6b8f2c..2aec83f981fe 100644 --- a/packages/rocketchat-e2e/client/rocketchat.e2e.js +++ b/packages/rocketchat-e2e/client/rocketchat.e2e.js @@ -67,6 +67,10 @@ class E2E { _id: roomId, }); + if (!room) { + return; + } + if (room.encrypted !== true) { return; } @@ -185,6 +189,7 @@ class E2E { } async stopClient() { + console.log('E2E -> Stop Client'); // This flag is used to avoid closing unrelated alerts. if (showingE2EAlert) { alerts.close(); @@ -192,7 +197,16 @@ class E2E { localStorage.removeItem('public_key'); localStorage.removeItem('private_key'); + this.instancesByRoomId = {}; + this.privateKey = null; + this.enabled.set(false); + this._ready.set(false); this.started = false; + + this.readyPromise = new Deferred(); + this.readyPromise.then(() => { + this._ready.set(true); + }); } setupListeners() { @@ -236,6 +250,7 @@ class E2E { async loadKeysFromDB() { try { const { public_key, private_key } = await call('e2e.fetchMyKeys'); + this.db_public_key = public_key; this.db_private_key = private_key; } catch (error) { @@ -280,6 +295,12 @@ class E2E { } catch (error) { return console.error('E2E -> Error exporting private key: ', error); } + + this.requestSubscriptionKeys(); + } + + async requestSubscriptionKeys() { + call('e2e.requestSubscriptionKeys'); } createRandomPassword() { diff --git a/packages/rocketchat-e2e/server/index.js b/packages/rocketchat-e2e/server/index.js index 9380368df0e2..c7167c3f6fd3 100644 --- a/packages/rocketchat-e2e/server/index.js +++ b/packages/rocketchat-e2e/server/index.js @@ -9,6 +9,8 @@ import './methods/getUsersOfRoomWithoutKey'; import './methods/updateGroupKey'; import './methods/setRoomKeyID'; import './methods/fetchMyKeys'; +import './methods/resetUserE2EKey'; +import './methods/requestSubscriptionKeys'; RocketChat.callbacks.add('afterJoinRoom', (user, room) => { RocketChat.Notifications.notifyRoom('e2e.keyRequest', room._id, room.e2eKeyId); diff --git a/packages/rocketchat-e2e/server/methods/requestSubscriptionKeys.js b/packages/rocketchat-e2e/server/methods/requestSubscriptionKeys.js new file mode 100644 index 000000000000..8653937d5b72 --- /dev/null +++ b/packages/rocketchat-e2e/server/methods/requestSubscriptionKeys.js @@ -0,0 +1,33 @@ +import { Meteor } from 'meteor/meteor'; +import { RocketChat } from 'meteor/rocketchat:lib'; + +Meteor.methods({ + 'e2e.requestSubscriptionKeys'() { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'requestSubscriptionKeys', + }); + } + + // Get all encrypted rooms that the user is subscribed to + + const subscriptions = RocketChat.models.Subscriptions.findByUserId(Meteor.userId()); + const roomIds = subscriptions.map((subscription) => subscription.rid); + + const query = { + e2eKeyId : { + $exists: true, + }, + _id : { + $in: roomIds, + }, + }; + + const rooms = RocketChat.models.Rooms.find(query); + rooms.forEach((room) => { + RocketChat.Notifications.notifyRoom('e2e.keyRequest', room._id, room.e2eKeyId); + }); + + return true; + }, +}); diff --git a/packages/rocketchat-e2e/server/methods/resetUserE2EKey.js b/packages/rocketchat-e2e/server/methods/resetUserE2EKey.js new file mode 100644 index 000000000000..53599f16d284 --- /dev/null +++ b/packages/rocketchat-e2e/server/methods/resetUserE2EKey.js @@ -0,0 +1,25 @@ +import { Meteor } from 'meteor/meteor'; +import { RocketChat } from 'meteor/rocketchat:lib'; + +Meteor.methods({ + 'e2e.resetUserE2EKey'(userId) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'resetUserE2EKey', + }); + } + + if (RocketChat.authz.hasPermission(Meteor.userId(), 'reset-other-user-e2e-key') !== true) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'resetUserE2EKey', + }); + } + + RocketChat.models.Users.resetE2EKey(userId); + RocketChat.models.Subscriptions.resetUserE2EKey(userId); + + // Force the user to logout, so that the keys can be generated again + RocketChat.models.Users.removeResumeService(userId); + return true; + }, +}); diff --git a/packages/rocketchat-e2e/server/models/Subscriptions.js b/packages/rocketchat-e2e/server/models/Subscriptions.js index 88af4c7d357b..e949c1929917 100644 --- a/packages/rocketchat-e2e/server/models/Subscriptions.js +++ b/packages/rocketchat-e2e/server/models/Subscriptions.js @@ -17,3 +17,13 @@ RocketChat.models.Subscriptions.findByRidWithoutE2EKey = function(rid, options) return this.find(query, options); }; + +RocketChat.models.Subscriptions.resetUserE2EKey = function(userId) { + this.update({ 'u._id': userId }, { + $unset: { + E2EKey: '', + }, + }, { + multi: true, + }); +}; diff --git a/packages/rocketchat-e2e/server/models/Users.js b/packages/rocketchat-e2e/server/models/Users.js index ac072f960d45..3a099808b0d9 100644 --- a/packages/rocketchat-e2e/server/models/Users.js +++ b/packages/rocketchat-e2e/server/models/Users.js @@ -34,3 +34,11 @@ RocketChat.models.Users.findByIdsWithPublicE2EKey = function(ids, options) { return this.find(query, options); }; + +RocketChat.models.Users.resetE2EKey = function(userId) { + this.update({ _id: userId }, { + $unset: { + e2e: '', + }, + }); +}; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 9a1e4b2e476c..0a6cedc7e739 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2211,6 +2211,8 @@ "Require_password_change": "Require password change", "Resend_verification_email": "Resend verification email", "Reset": "Reset", + "Reset_E2E_Key": "Reset E2E Key", + "reset-other-user-e2e-key": "Reset Other User E2E Key", "Reset_password": "Reset password", "Reset_section_settings": "Reset Section Settings", "Reset_Connection": "Reset Connection", @@ -2726,6 +2728,7 @@ "User_and_group_mentions_only": "User and group mentions only", "User_default": "User default", "User_doesnt_exist": "No user exists by the name of `@%s`.", + "User_e2e_key_was_reset": "User E2E key was reset successfully.", "User_has_been_activated": "User has been activated", "User_has_been_deactivated": "User has been deactivated", "User_has_been_deleted": "User has been deleted", diff --git a/packages/rocketchat-lib/server/models/Users.js b/packages/rocketchat-lib/server/models/Users.js index cf968a47c15c..bc9ac2f61927 100644 --- a/packages/rocketchat-lib/server/models/Users.js +++ b/packages/rocketchat-lib/server/models/Users.js @@ -604,6 +604,16 @@ class ModelUsers extends RocketChat.models._Base { return this.update({ _id }, update); } + removeResumeService(_id) { + const update = { + $unset: { + 'services.resume': '', + }, + }; + + return this.update({ _id }, update); + } + // INSERT create(data) { const user = { diff --git a/packages/rocketchat-ui-flextab/client/tabs/userActions.js b/packages/rocketchat-ui-flextab/client/tabs/userActions.js index 15d10c5bca12..bc0cdd5312c2 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/userActions.js +++ b/packages/rocketchat-ui-flextab/client/tabs/userActions.js @@ -481,6 +481,21 @@ export const getActions = function({ user, directActions, hideAdminControls }) { name: t('Activate'), action: prevent(getUser, ({ _id }) => Meteor.call('setUserActiveStatus', _id, true, success(() => toastr.success(t('User_has_been_activated'))))), }; + }, () => { + if (hideAdminControls || !hasPermission('reset-other-user-e2e-key')) { + return; + } + if (!RocketChat.settings.get('E2E_Enable')) { + return; + } + + return { + group: 'admin', + icon: 'key', + id: 'reset-e2e', + name: t('Reset_E2E_Key'), + action: prevent(getUser, ({ _id }) => Meteor.call('e2e.resetUserE2EKey', _id, success(() => toastr.success(t('User_e2e_key_was_reset'))))), + }; }]; return actions; }; diff --git a/packages/rocketchat-ui/client/lib/RoomManager.js b/packages/rocketchat-ui/client/lib/RoomManager.js index 9bfad667ac5a..7b5fe35bf136 100644 --- a/packages/rocketchat-ui/client/lib/RoomManager.js +++ b/packages/rocketchat-ui/client/lib/RoomManager.js @@ -242,9 +242,13 @@ const loadMissedMessages = function(rid) { return; } const subscription = ChatSubscription.findOne({ rid }); - return Meteor.call('loadMissedMessages', rid, lastMessage.ts, (err, result) => - Array.from(result).map((item) => RocketChat.promises.run('onClientMessageReceived', item).then((msg) => upsertMessage({ msg, subscription }))) - ); + return Meteor.call('loadMissedMessages', rid, lastMessage.ts, (err, result) => { + if (result) { + return Array.from(result).map((item) => RocketChat.promises.run('onClientMessageReceived', item).then((msg) => upsertMessage({ msg, subscription }))); + } else { + return []; + } + }); }; let connectionWasOnline = true;