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

feat: Reset room key #6030

Merged
merged 23 commits into from
Dec 11, 2024
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/containers/List/ListInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const styles = StyleSheet.create({
},
text: {
fontSize: 14,
lineHeight: 20,
...sharedStyles.textRegular
}
});
Expand Down
4 changes: 4 additions & 0 deletions app/definitions/ISubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export enum ERoomTypes {

type RelationModified<T extends Model> = { fetch(): Promise<T[]> } & Relation<T>;

type OldKey = { e2eKeyId: string; ts: Date; E2EKey: string };

export interface ISubscription {
_id: string;
id: string;
Expand Down Expand Up @@ -93,6 +95,7 @@ export interface ISubscription {
livechatData?: any;
tags?: string[];
E2EKey?: string;
oldRoomKeys?: OldKey[];
E2ESuggestedKey?: string | null;
encrypted?: boolean;
e2eKeyId?: string;
Expand Down Expand Up @@ -154,6 +157,7 @@ export interface IServerSubscription extends IRocketChatRecord {
onHold?: boolean;
encrypted?: boolean;
E2EKey?: string;
oldRoomKeys?: OldKey[];
E2ESuggestedKey?: string | null;
usersWaitingForE2EKeys?: TUserWaitingForE2EKeys[];
unreadAlert?: 'default' | 'all' | 'mentions' | 'nothing';
Expand Down
3 changes: 3 additions & 0 deletions app/definitions/rest/v1/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ export type E2eEndpoints = {
'e2e.fetchMyKeys': {
GET: () => { public_key: string; private_key: string };
};
'e2e.resetRoomKey': {
POST: (params: { rid: string; e2eKey: string; e2eKeyId: string }) => void;
};
};
13 changes: 11 additions & 2 deletions app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
"Dont_activate": "Don't activate now",
"Dont_Have_An_Account": "Don't you have an account?",
"Downloaded_file": "Downloaded file",
"E2E_Encryption": "E2E encryption",
"E2E_Encryption": "End-to-end encryption",
"E2E_encryption_change_password_confirmation": "Yes, change it",
"E2E_encryption_change_password_description": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted. \nThis is end to end encryption so the key to encode/decode your messages will not be saved on the workspace. 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_encryption_change_password_error": "Error while changing E2E key password!",
Expand Down Expand Up @@ -866,5 +866,14 @@
"Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Your invite link will expire on {{date}} or after {{usesLeft}} uses.",
"Your_invite_link_will_never_expire": "Your invite link will never expire.",
"Your_password_is": "Your password is",
"Your_push_was_sent_to_s_devices": "Your push was sent to {{s}} devices"
"Your_push_was_sent_to_s_devices": "Your push was sent to {{s}} devices",
"Reset_room_key_title": "Reset encryption key",
"Reset_room_key_message": "All members may lose access to previously encrypted content.",
"Encryption_keys_reset": "Encryption keys reset",
"Encryption_keys_reset_failed": "Encryption keys reset failed",
"Reset": "Reset",
"Encrypt__room_type__": "Encrypt {{room_type}}",
"Encrypt__room_type__info__room_name__": "Ensure only intended recipients can access messages and files in {{room_name}}.",
"Reset_encryption_keys": "Reset encryption keys",
"Reset_encryption_keys_info__room_type__": "Resetting E2EE keys is only recommend if no {{room_type}} member has a valid key to regain access to the previously encrypted content."
}
3 changes: 3 additions & 0 deletions app/lib/database/model/Subscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export default class Subscription extends Model {

@field('e2e_key') E2EKey;

@json('old_room_keys', sanitizer) oldRoomKeys;

@field('e2e_suggested_key') E2ESuggestedKey;

@field('encrypted') encrypted;
Expand Down Expand Up @@ -203,6 +205,7 @@ export default class Subscription extends Model {
livechatData: this.livechatData,
tags: this.tags,
E2EKey: this.E2EKey,
oldKeys: this.oldKeys,
E2ESuggestedKey: this.E2ESuggestedKey,
encrypted: this.encrypted,
e2eKeyId: this.e2eKeyId,
Expand Down
5 changes: 4 additions & 1 deletion app/lib/database/model/migrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,10 @@ export default schemaMigrations({
steps: [
addColumns({
table: 'subscriptions',
columns: [{ name: 'users_waiting_for_e2e_keys', type: 'string', isOptional: true }]
columns: [
{ name: 'users_waiting_for_e2e_keys', type: 'string', isOptional: true },
{ name: 'old_room_keys', type: 'string', isOptional: true }
]
})
]
}
Expand Down
1 change: 1 addition & 0 deletions app/lib/database/schema/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default appSchema({
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true },
{ name: 'e2e_key', type: 'string', isOptional: true },
{ name: 'old_room_keys', type: 'string', isOptional: true },
{ name: 'e2e_suggested_key', type: 'string', isOptional: true },
{ name: 'encrypted', type: 'boolean', isOptional: true },
{ name: 'e2e_key_id', type: 'string', isOptional: true },
Expand Down
7 changes: 6 additions & 1 deletion app/lib/encryption/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const ROOM_KEY_EXCHANGE_SIZE = 10;
class Encryption {
ready: boolean;
privateKey: string | null;
publicKey: string | null;
readyPromise: Deferred;
userId: string | null;
roomInstances: {
Expand All @@ -55,6 +56,7 @@ class Encryption {
encryptFile: TEncryptFile;
encryptUpload: Function;
importRoomKey: Function;
resetRoomKey: Function;
hasSessionKey: () => boolean;
encryptGroupKeyForParticipantsWaitingForTheKeys: (params: any) => Promise<any>;
};
Expand All @@ -67,6 +69,7 @@ class Encryption {
this.userId = '';
this.ready = false;
this.privateKey = null;
this.publicKey = null;
this.roomInstances = {};
this.readyPromise = new Deferred();
this.readyPromise
Expand Down Expand Up @@ -112,6 +115,7 @@ class Encryption {
stop = () => {
this.userId = null;
this.privateKey = null;
this.publicKey = null;
this.roomInstances = {};
// Cancel ongoing encryption/decryption requests
this.readyPromise.reject();
Expand Down Expand Up @@ -155,7 +159,8 @@ class Encryption {
// Persist keys on UserPreferences
persistKeys = async (server: string, publicKey: string, privateKey: string) => {
this.privateKey = await SimpleCrypto.RSA.importKey(EJSON.parse(privateKey));
UserPreferences.setString(`${server}-${E2E_PUBLIC_KEY}`, EJSON.stringify(publicKey));
this.publicKey = EJSON.stringify(publicKey);
UserPreferences.setString(`${server}-${E2E_PUBLIC_KEY}`, this.publicKey);
UserPreferences.setString(`${server}-${E2E_PRIVATE_KEY}`, privateKey);
};

Expand Down
52 changes: 52 additions & 0 deletions app/lib/encryption/helpers/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { compareServerVersion } from '../../methods/helpers';
import { useAppSelector } from '../../hooks';
import { TSubscriptionModel } from '../../../definitions';

const isMissingRoomE2EEKey = ({
encryptionEnabled,
roomEncrypted,
E2EKey
}: {
encryptionEnabled: boolean;
roomEncrypted: TSubscriptionModel['encrypted'];
E2EKey: TSubscriptionModel['E2EKey'];
}) => (encryptionEnabled && roomEncrypted && !E2EKey) ?? false;

const isE2EEDisabledEncryptedRoom = ({
encryptionEnabled,
roomEncrypted
}: {
encryptionEnabled: boolean;
roomEncrypted: TSubscriptionModel['encrypted'];
}) => (!encryptionEnabled && roomEncrypted) ?? false;

export const useIsMissingRoomE2EEKey = (roomEncrypted: TSubscriptionModel['encrypted'], E2EKey: TSubscriptionModel['E2EKey']) => {
const serverVersion = useAppSelector(state => state.server.version);
const e2eeEnabled = useAppSelector(state => state.settings.E2E_Enable);
const encryptionEnabled = useAppSelector(state => state.encryption.enabled);
if (!e2eeEnabled) {
return false;
}
if (compareServerVersion(serverVersion, 'lowerThan', '6.10.0')) {
return false;
}

return isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted, E2EKey });
};

export const useHasE2EEWarning = (roomEncrypted: TSubscriptionModel['encrypted'], E2EKey: TSubscriptionModel['E2EKey']) => {
const serverVersion = useAppSelector(state => state.server.version);
const e2eeEnabled = useAppSelector(state => state.settings.E2E_Enable);
const encryptionEnabled = useAppSelector(state => state.encryption.enabled);
if (!e2eeEnabled) {
return false;
}
if (compareServerVersion(serverVersion, 'lowerThan', '6.10.0')) {
return false;
}

return (
isMissingRoomE2EEKey({ encryptionEnabled, roomEncrypted, E2EKey }) ||
isE2EEDisabledEncryptedRoom({ encryptionEnabled, roomEncrypted })
);
};
48 changes: 29 additions & 19 deletions app/lib/encryption/helpers/toggleRoomE2EE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ import database from '../../database';
import { getSubscriptionByRoomId } from '../../database/services/Subscription';
import log from '../../methods/helpers/log';
import I18n from '../../../i18n';
import { TSubscriptionModel } from '../../../definitions';

const optimisticUpdate = async (room: TSubscriptionModel, value: TSubscriptionModel['encrypted']) => {
try {
const db = database.active;

// Instantly feedback to the user
await db.write(async () => {
await room.update(r => {
r.encrypted = value;
});
});
} catch {
// do nothing
}
};

export const toggleRoomE2EE = async (rid: string): Promise<void> => {
const room = await getSubscriptionByRoomId(rid);
Expand All @@ -17,34 +33,32 @@ export const toggleRoomE2EE = async (rid: string): Promise<void> => {
const message = I18n.t(isEncrypted ? 'Disable_encryption_description' : 'Enable_encryption_description');
const confirmationText = I18n.t(isEncrypted ? 'Disable' : 'Enable');

// Toggle encrypted value
const newValue = !room.encrypted;

// Instantly feedback to the user
await optimisticUpdate(room, newValue);

Alert.alert(
title,
message,
[
{
text: I18n.t('Cancel'),
style: 'cancel'
style: 'cancel',
onPress: async () => {
// Revert to original value
await optimisticUpdate(room, !newValue);
}
},
{
text: confirmationText,
style: isEncrypted ? 'destructive' : 'default',
onPress: async () => {
try {
const db = database.active;

// Toggle encrypted value
const encrypted = !room.encrypted;

// Instantly feedback to the user
await db.write(async () => {
await room.update(r => {
r.encrypted = encrypted;
});
});

try {
// Send new room setting value to server
const { result } = await Services.saveRoomSettings(rid, { encrypted });
const { result } = await Services.saveRoomSettings(rid, { encrypted: newValue });
// If it was saved successfully
if (result) {
return;
Expand All @@ -54,11 +68,7 @@ export const toggleRoomE2EE = async (rid: string): Promise<void> => {
}

// If something goes wrong we go back to the previous value
await db.write(async () => {
await room.update(r => {
r.encrypted = room.encrypted;
});
});
await optimisticUpdate(room, !newValue);
} catch (e) {
log(e);
}
Expand Down
Loading