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