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

[NEW] Admin option to reset other users’ E2E encryption key #18642

Merged
merged 4 commits into from
Aug 22, 2020
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
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;
}