Skip to content

Commit

Permalink
Merge pull request #18642 from RocketChat/reset-other-e2ekey
Browse files Browse the repository at this point in the history
[NEW] Admin option to reset other users’ E2E encryption key
  • Loading branch information
sampaiodiego authored Aug 22, 2020
2 parents 448fd33 + 011ef5d commit dbd0e3e
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 15 deletions.
1 change: 1 addition & 0 deletions app/2fa/server/startup/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ settings.addGroup('Accounts', function() {
// TODO: Remove this setting for version 4.0
this.add('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback', true, {
type: 'boolean',
public: true,
});
});
});
31 changes: 31 additions & 0 deletions app/api/server/v1/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { API } from '../api';
import { setStatusText } from '../../../lib/server';
import { findUsersToAutocomplete } from '../lib/users';
import { getUserForCheck, emailCheck } from '../../../2fa/server/code';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';

API.v1.addRoute('users.create', { authRequired: true }, {
post() {
Expand Down Expand Up @@ -786,3 +787,33 @@ API.v1.addRoute('users.removeOtherTokens', { authRequired: true }, {
API.v1.success(Meteor.call('removeOtherTokens'));
},
});

API.v1.addRoute('users.resetE2EKey', { authRequired: true, twoFactorRequired: true }, {
post() {
// reset own keys
if (this.isUserFromParams()) {
resetUserE2EEncriptionKey(this.userId);
return API.v1.success();
}

// reset other user keys
const user = this.getUserFromParams();
if (!user) {
throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
}

if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

if (!hasPermission(Meteor.userId(), 'edit-other-user-e2ee')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

if (!resetUserE2EEncriptionKey(user._id)) {
return API.v1.failure();
}

return API.v1.success();
},
});
1 change: 1 addition & 0 deletions app/authorization/server/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Meteor.startup(function() {
{ _id: 'edit-other-user-info', roles: ['admin'] },
{ _id: 'edit-other-user-password', roles: ['admin'] },
{ _id: 'edit-other-user-avatar', roles: ['admin'] },
{ _id: 'edit-other-user-e2ee', roles: ['admin'] },
{ _id: 'edit-privileged-setting', roles: ['admin'] },
{ _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] },
{ _id: 'edit-room-avatar', roles: ['admin', 'owner', 'moderator'] },
Expand Down
10 changes: 4 additions & 6 deletions app/e2e/server/methods/resetOwnE2EKey.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';

import { Users, Subscriptions } from '../../../models';
import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';

Meteor.methods({
'e2e.resetOwnE2EKey': twoFactorRequired(function() {
Expand All @@ -13,11 +13,9 @@ Meteor.methods({
});
}

Users.resetE2EKey(userId);
Subscriptions.resetUserE2EKey(userId);

// Force the user to logout, so that the keys can be generated again
Users.removeResumeService(userId);
if (!resetUserE2EEncriptionKey(userId)) {
return false;
}
return true;
}),
});
43 changes: 34 additions & 9 deletions client/admin/users/UserInfoActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useSetting } from '../../contexts/SettingsContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useTranslation } from '../../contexts/TranslationContext';

const DeleteWarningModal = ({ onDelete, onCancel, erasureType, ...props }) => {
const ConfirmWarningModal = ({ onConfirm, onCancel, confirmText, text, ...props }) => {
const t = useTranslation();

return <Modal {...props}>
Expand All @@ -22,27 +22,27 @@ const DeleteWarningModal = ({ onDelete, onCancel, erasureType, ...props }) => {
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t(`Delete_User_Warning_${ erasureType }`)}
{text}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
<Button primary danger onClick={onConfirm}>{confirmText}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};

const SuccessModal = ({ onClose, ...props }) => {
const SuccessModal = ({ onClose, title, text, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Title>{title}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('User_has_been_deleted')}
{text}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
Expand All @@ -63,9 +63,12 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
const canDirectMessage = usePermission('create-d');
const canEditOtherUserInfo = usePermission('edit-other-user-info');
const canAssignAdminRole = usePermission('assign-admin-role');
const canResetE2EEKey = usePermission('edit-other-user-e2ee');
const canEditOtherUserActiveStatus = usePermission('edit-other-user-active-status');
const canDeleteUser = usePermission('delete-user');

const enforcePassword = useSetting('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback');

const confirmOwnerChanges = (action, modalProps = {}) => async () => {
try {
return await action();
Expand Down Expand Up @@ -100,7 +103,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })

const result = await deleteUserEndpoint(deleteUserQuery);
if (result.success) {
setModal(<SuccessModal onClose={() => { setModal(); onChange(); }}/>);
setModal(<SuccessModal title={t('Deleted')} text={t('User_has_been_deleted')} onClose={() => { setModal(); onChange(); }}/>);
} else {
setModal();
}
Expand All @@ -110,8 +113,8 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
});

const confirmDeleteUser = useCallback(() => {
setModal(<DeleteWarningModal onDelete={deleteUser} onCancel={() => setModal()} erasureType={erasureType}/>);
}, [deleteUser, erasureType, setModal]);
setModal(<ConfirmWarningModal onConfirm={deleteUser} onCancel={() => setModal()} text={t(`Delete_User_Warning_${ erasureType }`)} confirmText={t('Delete')} />);
}, [deleteUser, erasureType, setModal, t]);

const setAdminStatus = useMethod('setAdminStatus');
const changeAdminStatus = useCallback(() => {
Expand All @@ -125,6 +128,20 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
}
}, [_id, dispatchToastMessage, isAdmin, onChange, setAdminStatus, t]);

const resetE2EEKeyRequest = useEndpoint('POST', 'users.resetE2EKey');
const resetE2EEKey = useCallback(async () => {
setModal();
const result = await resetE2EEKeyRequest({ userId: _id });

if (result) {
setModal(<SuccessModal title={t('Success')} text={t('Users_key_has_been_reset')} onClose={() => { setModal(); onChange(); }}/>);
}
}, [resetE2EEKeyRequest, onChange, setModal, t, _id]);

const confirmResetE2EEKey = useCallback(() => {
setModal(<ConfirmWarningModal onConfirm={resetE2EEKey} onCancel={() => setModal()} text={t('E2E_Reset_Other_Key_Warning')} confirmText={t('Reset')} />);
}, [resetE2EEKey, t, setModal]);

const activeStatusQuery = useMemo(() => ({
userId: _id,
activeStatus: !isActive,
Expand Down Expand Up @@ -175,6 +192,11 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
label: isAdmin ? t('Remove_Admin') : t('Make_Admin'),
action: changeAdminStatus,
} },
...canResetE2EEKey && enforcePassword && { resetE2EEKey: {
icon: 'key',
label: t('Reset_E2E_Key'),
action: confirmResetE2EEKey,
} },
...canDeleteUser && { delete: {
icon: 'trash',
label: t('Delete'),
Expand All @@ -199,6 +221,9 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
canEditOtherUserActiveStatus,
isActive,
changeActiveStatus,
enforcePassword,
canResetE2EEKey,
confirmResetE2EEKey,
]);

const { actions: actionsDefinition, menu: menuOptions } = useUserInfoActionsSpread(options);
Expand Down
4 changes: 4 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,7 @@
"E2E_password_reveal_text": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.<br/><br/>This is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store this password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on. <a href=\"https://rocket.chat/docs/user-guides/end-to-end-encryption/\" target=\"_blank\">Learn more here!</a><br/><br/>Your password is: <span style=\"font-weight: bold;\">%s</span><br/><br/>This is an auto generated password, you can setup a new password for your encryption key any time from any browser you have entered the existing password.<br/>This password is only stored on this browser until you store the password and dismiss this message.",
"E2E_password_request_text": "To access your encrypted private groups and direct messages, enter your encryption password. <br/>You need to enter this password to encode/decode your messages on every client you use, since the key is not stored on the server.",
"E2E_Reset_Key_Explanation": "This option will remove your current E2E key and log you out. <BR/>When you login again, Rocket.Chat will generate you a new key and restore your access to any encrypted room that has one or more members online.<BR/>Due to the nature of the E2E encryption, Rocket.Chat will not be able to restore access to any encrypted room that has no member online.",
"E2E_Reset_Other_Key_Warning": "Reset the current E2E key will log out the user. When the user login again, Rocket.Chat will generate a new key and restore the user access to any encrypted room that has one or more members online. Due to the nature of the E2E encryption, Rocket.Chat will not be able to restore access to any encrypted room that has no member online.",
"Edit": "Edit",
"Edit_User": "Edit User",
"Edit_Invite": "Edit Invite",
Expand All @@ -1331,6 +1332,8 @@
"edit-other-user-info_description": "Permission to change other user's name, username or email address.",
"edit-other-user-password": "Edit Other User Password",
"edit-other-user-password_description": "Permission to modify other user's passwords. Requires edit-other-user-info permission.",
"edit-other-user-e2ee": "Edit Other User E2E Encryption",
"edit-other-user-e2ee_description": "Permission to modify other user's E2E Encryption.",
"edit-privileged-setting": "Edit privileged Setting",
"edit-privileged-setting_description": "Permission to edit settings",
"edit-room": "Edit Room",
Expand Down Expand Up @@ -3788,6 +3791,7 @@
"Users_by_time_of_day": "Users by time of day",
"Users_in_role": "Users in role",
"Users must use Two Factor Authentication": "Users must use Two Factor Authentication",
"Users_key_has_been_reset": "User's key has been reset",
"Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Leave the description field blank if you don't want to show the role",
"Uses": "Uses",
"Uses_left": "Uses left",
Expand Down
11 changes: 11 additions & 0 deletions server/lib/resetUserE2EKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Users, Subscriptions } from '../../app/models/server';

export function resetUserE2EEncriptionKey(uid: string): boolean {
Users.resetE2EKey(uid);
Subscriptions.resetUserE2EKey(uid);

// Force the user to logout, so that the keys can be generated again
Users.removeResumeService(uid);

return true;
}

0 comments on commit dbd0e3e

Please sign in to comment.