[IMPROVE] E2EE improvements (#4763)
This commit is contained in:
parent
acbd950c7e
commit
bbe524ee55
|
@ -91,6 +91,7 @@ export interface ISubscription {
|
|||
livechatData?: any;
|
||||
tags?: string[];
|
||||
E2EKey?: string;
|
||||
E2ESuggestedKey?: string;
|
||||
encrypted?: boolean;
|
||||
e2eKeyId?: string;
|
||||
avatarETag?: string;
|
||||
|
@ -145,6 +146,7 @@ export interface IServerSubscription extends IRocketChatRecord {
|
|||
onHold?: boolean;
|
||||
encrypted?: boolean;
|
||||
E2EKey?: string;
|
||||
E2ESuggestedKey?: string;
|
||||
unreadAlert?: 'default' | 'all' | 'mentions' | 'nothing';
|
||||
|
||||
fname?: unknown;
|
||||
|
|
|
@ -12,6 +12,12 @@ export type E2eEndpoints = {
|
|||
'e2e.updateGroupKey': {
|
||||
POST: (params: { uid: string; rid: string; key: string }) => {};
|
||||
};
|
||||
'e2e.acceptSuggestedGroupKey': {
|
||||
POST: (params: { rid: string }) => {};
|
||||
};
|
||||
'e2e.rejectSuggestedGroupKey': {
|
||||
POST: (params: { rid: string }) => {};
|
||||
};
|
||||
'e2e.setRoomKeyID': {
|
||||
POST: (params: { rid: string; keyID: string }) => {};
|
||||
};
|
||||
|
|
|
@ -123,6 +123,8 @@ export default class Subscription extends Model {
|
|||
|
||||
@field('e2e_key') E2EKey;
|
||||
|
||||
@field('e2e_suggested_key') E2ESuggestedKey;
|
||||
|
||||
@field('encrypted') encrypted;
|
||||
|
||||
@field('e2e_key_id') e2eKeyId;
|
||||
|
|
|
@ -248,6 +248,15 @@ export default schemaMigrations({
|
|||
columns: [{ name: 'tmid', type: 'string', isOptional: true }]
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
toVersion: 20,
|
||||
steps: [
|
||||
addColumns({
|
||||
table: 'subscriptions',
|
||||
columns: [{ name: 'e2e_suggested_key', type: 'string', isOptional: true }]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { appSchema, tableSchema } from '@nozbe/watermelondb';
|
||||
|
||||
export default appSchema({
|
||||
version: 19,
|
||||
version: 20,
|
||||
tables: [
|
||||
tableSchema({
|
||||
name: 'subscriptions',
|
||||
|
@ -55,6 +55,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: 'e2e_suggested_key', type: 'string', isOptional: true },
|
||||
{ name: 'encrypted', type: 'boolean', isOptional: true },
|
||||
{ name: 'e2e_key_id', type: 'string', isOptional: true },
|
||||
{ name: 'avatar_etag', type: 'string', isOptional: true },
|
||||
|
|
|
@ -34,6 +34,7 @@ class Encryption {
|
|||
handshake: Function;
|
||||
decrypt: Function;
|
||||
encrypt: Function;
|
||||
importRoomKey: Function;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -97,6 +98,10 @@ class Encryption {
|
|||
});
|
||||
};
|
||||
|
||||
stopRoom = (rid: string) => {
|
||||
delete this.roomInstances[rid];
|
||||
};
|
||||
|
||||
// When a new participant join and request a new room encryption key
|
||||
provideRoomKeyToUser = async (keyId: string, rid: string) => {
|
||||
// If the client is not ready
|
||||
|
@ -220,6 +225,19 @@ class Encryption {
|
|||
return roomE2E;
|
||||
};
|
||||
|
||||
evaluateSuggestedKey = async (rid: string, E2ESuggestedKey: string) => {
|
||||
try {
|
||||
if (this.privateKey) {
|
||||
const roomE2E = await this.getRoomInstance(rid);
|
||||
await roomE2E.importRoomKey(E2ESuggestedKey, this.privateKey);
|
||||
delete this.roomInstances[rid];
|
||||
await Services.e2eAcceptSuggestedGroupKey(rid);
|
||||
}
|
||||
} catch (e) {
|
||||
await Services.e2eRejectSuggestedGroupKey(rid);
|
||||
}
|
||||
};
|
||||
|
||||
// Logic to decrypt all pending messages/threads/threadMessages
|
||||
// after initialize the encryption client
|
||||
decryptPendingMessages = async (roomId?: string) => {
|
||||
|
|
|
@ -74,7 +74,10 @@ export default class EncryptionRoom {
|
|||
if (E2EKey && Encryption.privateKey) {
|
||||
// We're establishing a new room encryption client
|
||||
this.establishing = true;
|
||||
await this.importRoomKey(E2EKey, Encryption.privateKey);
|
||||
const { keyID, roomKey, sessionKeyExportedString } = await this.importRoomKey(E2EKey, Encryption.privateKey);
|
||||
this.keyID = keyID;
|
||||
this.roomKey = roomKey;
|
||||
this.sessionKeyExportedString = sessionKeyExportedString;
|
||||
this.readyPromise.resolve();
|
||||
return;
|
||||
}
|
||||
|
@ -96,20 +99,33 @@ export default class EncryptionRoom {
|
|||
};
|
||||
|
||||
// Import roomKey as an AES Decrypt key
|
||||
importRoomKey = async (E2EKey: string, privateKey: string) => {
|
||||
importRoomKey = async (
|
||||
E2EKey: string,
|
||||
privateKey: string
|
||||
): Promise<{ sessionKeyExportedString: string | ByteBuffer; roomKey: ArrayBuffer; keyID: string }> => {
|
||||
try {
|
||||
const roomE2EKey = E2EKey.slice(12);
|
||||
|
||||
const decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey);
|
||||
this.sessionKeyExportedString = toString(decryptedKey);
|
||||
const sessionKeyExportedString = toString(decryptedKey);
|
||||
|
||||
this.keyID = Base64.encode(this.sessionKeyExportedString as string).slice(0, 12);
|
||||
const keyID = Base64.encode(sessionKeyExportedString as string).slice(0, 12);
|
||||
|
||||
// Extract K from Web Crypto Secret Key
|
||||
// K is a base64URL encoded array of bytes
|
||||
// Web Crypto API uses this as a private key to decrypt/encrypt things
|
||||
// Reference: https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/jwk/OctetSequenceKey.html
|
||||
const { k } = EJSON.parse(this.sessionKeyExportedString as string);
|
||||
this.roomKey = b64ToBuffer(k);
|
||||
const { k } = EJSON.parse(sessionKeyExportedString as string);
|
||||
const roomKey = b64ToBuffer(k);
|
||||
|
||||
return {
|
||||
sessionKeyExportedString,
|
||||
roomKey,
|
||||
keyID
|
||||
};
|
||||
} catch (e: any) {
|
||||
throw new Error(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Create a key to a room
|
||||
|
|
|
@ -102,6 +102,7 @@ const createOrUpdateSubscription = async (subscription: ISubscription, room: ISe
|
|||
encrypted: s.encrypted,
|
||||
e2eKeyId: s.e2eKeyId,
|
||||
E2EKey: s.E2EKey,
|
||||
E2ESuggestedKey: s.E2ESuggestedKey,
|
||||
avatarETag: s.avatarETag,
|
||||
onHold: s.onHold,
|
||||
hideMentionStatus: s.hideMentionStatus
|
||||
|
@ -165,6 +166,8 @@ const createOrUpdateSubscription = async (subscription: ISubscription, room: ISe
|
|||
tmp = (await Encryption.decryptSubscription(tmp)) as ISubscription;
|
||||
// Decrypt all pending messages of this room in parallel
|
||||
Encryption.decryptPendingMessages(tmp.rid);
|
||||
} else if (sub && subscription.E2ESuggestedKey) {
|
||||
await Encryption.evaluateSuggestedKey(sub.rid, subscription.E2ESuggestedKey);
|
||||
}
|
||||
|
||||
const batch: Model[] = [];
|
||||
|
@ -320,6 +323,8 @@ export default function subscribeRooms() {
|
|||
await db.batch(sub.prepareDestroyPermanently(), ...messagesToDelete, ...threadsToDelete, ...threadMessagesToDelete);
|
||||
});
|
||||
|
||||
Encryption.stopRoom(data.rid);
|
||||
|
||||
const roomState = store.getState().room;
|
||||
// Delete and remove events come from this stream
|
||||
// Here we identify which one was triggered
|
||||
|
|
|
@ -74,6 +74,14 @@ export const e2eRequestRoomKey = (rid: string, e2eKeyId: string): Promise<{ mess
|
|||
// RC 0.70.0
|
||||
sdk.methodCallWrapper('stream-notify-room-users', `${rid}/e2ekeyRequest`, rid, e2eKeyId);
|
||||
|
||||
export const e2eAcceptSuggestedGroupKey = (rid: string): Promise<{ success: boolean }> =>
|
||||
// RC 5.5
|
||||
sdk.post('e2e.acceptSuggestedGroupKey', { rid });
|
||||
|
||||
export const e2eRejectSuggestedGroupKey = (rid: string): Promise<{ success: boolean }> =>
|
||||
// RC 5.5
|
||||
sdk.post('e2e.rejectSuggestedGroupKey', { rid });
|
||||
|
||||
export const updateJitsiTimeout = (roomId: string) =>
|
||||
// RC 0.74.0
|
||||
sdk.post('video-conference/jitsi.update-timeout', { roomId });
|
||||
|
|
|
@ -51,13 +51,6 @@ const handleEncryptionInit = function* handleEncryptionInit() {
|
|||
return;
|
||||
}
|
||||
|
||||
// If the user has a private key stored, but never entered the password
|
||||
const storedRandomPassword = UserPreferences.getString(`${server}-${E2E_RANDOM_PASSWORD_KEY}`);
|
||||
|
||||
if (storedRandomPassword) {
|
||||
yield put(encryptionSet(true, E2E_BANNER_TYPE.SAVE_PASSWORD));
|
||||
}
|
||||
|
||||
// Fetch stored public e2e key for this server
|
||||
let storedPublicKey = UserPreferences.getString(`${server}-${E2E_PUBLIC_KEY}`);
|
||||
|
||||
|
@ -66,14 +59,21 @@ const handleEncryptionInit = function* handleEncryptionInit() {
|
|||
storedPublicKey = EJSON.parse(storedPublicKey);
|
||||
}
|
||||
|
||||
if (storedPublicKey && storedPrivateKey && !storedRandomPassword) {
|
||||
if (storedPublicKey && storedPrivateKey) {
|
||||
// Persist these keys
|
||||
yield Encryption.persistKeys(server, storedPublicKey, storedPrivateKey);
|
||||
yield put(encryptionSet(true));
|
||||
} else {
|
||||
// Create new keys since the user doesn't have any
|
||||
yield Encryption.createKeys(user.id, server);
|
||||
}
|
||||
|
||||
// If the user has a private key stored, but never entered the password
|
||||
const storedRandomPassword = UserPreferences.getString(`${server}-${E2E_RANDOM_PASSWORD_KEY}`);
|
||||
|
||||
if (storedRandomPassword) {
|
||||
yield put(encryptionSet(true, E2E_BANNER_TYPE.SAVE_PASSWORD));
|
||||
} else {
|
||||
yield put(encryptionSet(true));
|
||||
}
|
||||
|
||||
// Decrypt all pending messages/subscriptions
|
||||
|
|
Loading…
Reference in New Issue