Skip to content
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
68 changes: 68 additions & 0 deletions app/2fa/server/functions/resetTOTP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';

import { settings } from '../../../settings/server';
import * as Mailer from '../../../mailer';
import { Users } from '../../../models/server/raw/index';
import { IUser } from '../../../../definition/IUser';

const sendResetNotification = async function(uid: string): Promise<void> {
const user: IUser = await Users.findOneById(uid, { projection: { language: 1, emails: 1 } });
if (!user) {
throw new Meteor.Error('invalid-user');
}

const language = user.language || settings.get('Language') || 'en';
const addresses = user.emails?.filter(({ verified }: { verified: boolean}) => verified).map((e) => e.address);
if (!addresses?.length) {
return;
}

const t = (s: string): string => TAPi18n.__(s, { lng: language });
const text = `
${ t('Your_TOTP_has_been_reset') }

${ t('TOTP_Reset_Other_Key_Warning') }
`;
const html = `
<p>${ t('Your_TOTP_has_been_reset') }</p>
<p>${ t('TOTP_Reset_Other_Key_Warning') }</p>
`;

const from = settings.get('From_Email');
const subject = t('TOTP_reset_email');

for (const address of addresses) {
Meteor.defer(() => {
try {
Mailer.send({
to: address,
from,
subject,
text,
html,
} as any);
} catch (error) {
throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${ error.message }`, {
function: 'resetUserTOTP',
message: error.message,
});
}
});
}
};

export async function resetTOTP(userId: string, notifyUser = false): Promise<boolean> {
if (notifyUser) {
await sendResetNotification(userId);
}

const result = await Users.resetTOTPById(userId);

if (result?.modifiedCount === 1) {
await Users.removeResumeService(userId);
return true;
}

return false;
}
31 changes: 30 additions & 1 deletion app/api/server/v1/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { findUsersToAutocomplete } from '../lib/users';
import { getUserForCheck, emailCheck } from '../../../2fa/server/code';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers';
import { resetTOTP } from '../../../2fa/server/functions/resetTOTP';

API.v1.addRoute('users.create', { authRequired: true }, {
post() {
Expand Down Expand Up @@ -797,7 +798,7 @@ API.v1.addRoute('users.removeOtherTokens', { authRequired: true }, {
},
});

API.v1.addRoute('users.resetE2EKey', { authRequired: true, twoFactorRequired: true }, {
API.v1.addRoute('users.resetE2EKey', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, {
post() {
// reset own keys
if (this.isUserFromParams()) {
Expand Down Expand Up @@ -826,3 +827,31 @@ API.v1.addRoute('users.resetE2EKey', { authRequired: true, twoFactorRequired: tr
return API.v1.success();
},
});

API.v1.addRoute('users.resetTOTP', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, {
post() {
// reset own keys
if (this.isUserFromParams()) {
Promise.await(resetTOTP(this.userId, false));
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-totp')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

Promise.await(resetTOTP(user._id, true));

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 @@ -43,6 +43,7 @@ Meteor.startup(function() {
{ _id: 'edit-other-user-password', roles: ['admin'] },
{ _id: 'edit-other-user-avatar', roles: ['admin'] },
{ _id: 'edit-other-user-e2ee', roles: ['admin'] },
{ _id: 'edit-other-user-totp', 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
20 changes: 20 additions & 0 deletions app/models/server/raw/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,4 +517,24 @@ export class UsersRaw extends BaseRaw {

return this.update(query, update, { multi: true });
}

resetTOTPById(userId) {
return this.col.updateOne({
_id: userId,
}, {
$unset: {
'services.totp': 1,
},
});
}

removeResumeService(userId) {
return this.col.updateOne({
_id: userId,
}, {
$unset: {
'services.resume': 1,
},
});
}
}
30 changes: 30 additions & 0 deletions client/admin/users/UserInfoActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
const canEditOtherUserInfo = usePermission('edit-other-user-info');
const canAssignAdminRole = usePermission('assign-admin-role');
const canResetE2EEKey = usePermission('edit-other-user-e2ee');
const canResetTOTP = usePermission('edit-other-user-totp');
const canEditOtherUserActiveStatus = usePermission('edit-other-user-active-status');
const canDeleteUser = usePermission('delete-user');

Expand Down Expand Up @@ -98,6 +99,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
}, [_id, dispatchToastMessage, isAdmin, onChange, setAdminStatus, t]);

const resetE2EEKeyRequest = useEndpoint('POST', 'users.resetE2EKey');
const resetTOTPRequest = useEndpoint('POST', 'users.resetTOTP');
const resetE2EEKey = useCallback(async () => {
setModal();
const result = await resetE2EEKeyRequest({ userId: _id });
Expand All @@ -110,6 +112,18 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
}
}, [resetE2EEKeyRequest, onChange, setModal, t, _id]);

const resetTOTP = useCallback(async () => {
setModal();
const result = await resetTOTPRequest({ userId: _id });

if (result) {
setModal(<DeleteSuccessModal
children={t('Users_TOTP_has_been_reset')}
onClose={() => { setModal(); onChange(); }}
/>);
}
}, [resetTOTPRequest, onChange, setModal, t, _id]);

const confirmResetE2EEKey = useCallback(() => {
setModal(<DeleteWarningModal
children={t('E2E_Reset_Other_Key_Warning')}
Expand All @@ -119,6 +133,15 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
/>);
}, [resetE2EEKey, t, setModal]);

const confirmResetTOTP = useCallback(() => {
setModal(<DeleteWarningModal
children={t('TOTP_Reset_Other_Key_Warning')}
deleteText={t('Reset')}
onCancel={() => setModal()}
onDelete={resetTOTP}
/>);
}, [resetTOTP, t, setModal]);

const activeStatusQuery = useMemo(() => ({
userId: _id,
activeStatus: !isActive,
Expand Down Expand Up @@ -174,6 +197,11 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
label: t('Reset_E2E_Key'),
action: confirmResetE2EEKey,
} },
...canResetTOTP && enforcePassword && { resetTOTP: {
icon: 'key',
label: t('Reset_TOTP'),
action: confirmResetTOTP,
} },
...canDeleteUser && { delete: {
icon: 'trash',
label: t('Delete'),
Expand All @@ -200,7 +228,9 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange })
changeActiveStatus,
enforcePassword,
canResetE2EEKey,
canResetTOTP,
confirmResetE2EEKey,
confirmResetTOTP,
username,
]);

Expand Down
6 changes: 6 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1323,10 +1323,12 @@
"E2E_Encryption_Password_Change": "Change Encryption Password",
"E2E_Encryption_Password_Explanation": "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 your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.",
"E2E_key_reset_email": "E2E Key Reset Notification",
"TOTP_reset_email": "Two Factor TOTP Reset Notification",
"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.",
"TOTP_Reset_Other_Key_Warning": "Reset the current Two Factor TOTP will log out the user. The user will be able to set the Two Factor again later.",
"E2E_Reset_Email_Content": "You've been automatically logged out. When you login again, Rocket.Chat will generate a new key and restore your 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",
Expand All @@ -1343,6 +1345,7 @@
"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-totp": "Edit Other User Two Factor TOTP",
"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",
Expand Down Expand Up @@ -3006,6 +3009,7 @@
"Resend_verification_email": "Resend verification email",
"Reset": "Reset",
"Reset_E2E_Key": "Reset E2E Key",
"Reset_TOTP": "Reset TOTP",
"Reset_password": "Reset password",
"Reset_section_settings": "Reset Section to Default",
"Reset_Connection": "Reset Connection",
Expand Down Expand Up @@ -3830,6 +3834,7 @@
"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",
"Users_TOTP_has_been_reset": "User's TOTP 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 Expand Up @@ -4002,6 +4007,7 @@
"You_will_not_be_able_to_recover_file": "You will not be able to recover this file!",
"You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "You won't receive email notifications because you have not verified your email.",
"Your_e2e_key_has_been_reset": "Your e2e key has been reset.",
"Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.",
"Your_email_address_has_changed": "Your email address has been changed.",
"Your_email_has_been_queued_for_sending": "Your email has been queued for sending",
"Your_entry_has_been_deleted": "Your entry has been deleted.",
Expand Down