[IMPROVE] E2EE improvements (#4763)

This commit is contained in:
Diego Mello 2023-01-12 10:32:33 -03:00
parent acbd950c7e
commit bbe524ee55
10 changed files with 89 additions and 22 deletions

View File

@ -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;

View File

@ -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 }) => {};
};

View File

@ -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;

View File

@ -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 }]
})
]
}
]
});

View File

@ -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 },

View File

@ -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) => {

View File

@ -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) => {
const roomE2EKey = E2EKey.slice(12);
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 decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey);
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);
// 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(sessionKeyExportedString as string);
const roomKey = b64ToBuffer(k);
return {
sessionKeyExportedString,
roomKey,
keyID
};
} catch (e: any) {
throw new Error(e);
}
};
// Create a key to a room

View File

@ -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

View File

@ -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 });

View File

@ -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