diff --git a/preload.js b/preload.js index d7f99256c..eab5aa735 100644 --- a/preload.js +++ b/preload.js @@ -445,6 +445,7 @@ window.lokiFeatureFlags = { useFileOnionRequests: true, useFileOnionRequestsV2: true, // more compact encoding of files in response onionRequestHops: 3, + useRequestEncryptionKeyPair: false, }; // eslint-disable-next-line no-extend-native,func-names @@ -478,6 +479,7 @@ if (config.environment.includes('test-integration')) { useOnionRequests: false, useFileOnionRequests: false, useOnionRequestsV2: false, + useRequestEncryptionKeyPair: false, }; /* eslint-disable global-require, import/no-extraneous-dependencies */ window.sinon = require('sinon'); diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 33325a563..eb48040c5 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -33,6 +33,11 @@ import { MessageController } from '../session/messages'; import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/content/data/group'; import { queueAllCachedFromSource } from './receiver'; +export const distributingClosedGroupEncryptionKeyPairs = new Map< + string, + ECKeyPair +>(); + export async function handleClosedGroupControlMessage( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage @@ -456,6 +461,9 @@ async function handleClosedGroupEncryptionKeyPair( ); if (isKeyPairAlreadyHere) { + const existingKeyPairs = await getAllEncryptionKeyPairsForGroup( + groupPublicKey + ); window.log.info('Dropping already saved keypair for group', groupPublicKey); await removeFromCache(envelope); return; @@ -532,11 +540,18 @@ async function performIfValid( } else if (groupUpdate.type === Type.MEMBER_LEFT) { await handleClosedGroupMemberLeft(envelope, groupUpdate, convo); } else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) { - await handleClosedGroupEncryptionKeyPairRequest( - envelope, - groupUpdate, - convo - ); + if (window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + await handleClosedGroupEncryptionKeyPairRequest( + envelope, + groupUpdate, + convo + ); + } else { + window.log.warn( + 'Received ENCRYPTION_KEY_PAIR_REQUEST message but it is not enabled for now.' + ); + await removeFromCache(envelope); + } // if you add a case here, remember to add it where performIfValid is called too. } @@ -590,6 +605,15 @@ async function handleClosedGroupMembersAdded( return; } + if (await areWeAdmin(convo)) { + await sendLatestKeyPairToUsers( + envelope, + convo, + convo.id, + membersNotAlreadyPresent + ); + } + const members = [...oldMembers, ...membersNotAlreadyPresent]; // Only add update message if we have something to show @@ -604,6 +628,16 @@ async function handleClosedGroupMembersAdded( await removeFromCache(envelope); } +async function areWeAdmin(groupConvo: ConversationModel) { + if (!groupConvo) { + throw new Error('areWeAdmin needs a convo'); + } + + const groupAdmins = groupConvo.get('groupAdmins'); + const ourNumber = (await UserUtils.getCurrentDevicePubKey()) as string; + return groupAdmins?.includes(ourNumber) || false; +} + async function handleClosedGroupMembersRemoved( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, @@ -650,8 +684,7 @@ async function handleClosedGroupMembersRemoved( window.SwarmPolling.removePubkey(groupPubKey); } // Generate and distribute a new encryption key pair if needed - const isCurrentUserAdmin = firstAdmin === ourPubKey.key; - if (isCurrentUserAdmin) { + if (await areWeAdmin(convo)) { try { await ClosedGroup.generateAndSendNewEncryptionKeyPair( groupPubKey, @@ -733,58 +766,84 @@ async function handleClosedGroupMemberLeft( await removeFromCache(envelope); } -async function handleClosedGroupEncryptionKeyPairRequest( +async function sendLatestKeyPairToUsers( envelope: EnvelopePlus, - groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, - convo: ConversationModel + groupConvo: ConversationModel, + groupPubKey: string, + targetUsers: Array ) { - const sender = envelope.senderIdentity; - const groupPublicKey = envelope.source; - // Guard against self-sends - if (await UserUtils.isUs(sender)) { - window.log.info( - 'Dropping self send message of type ENCRYPTION_KEYPAIR_REQUEST' - ); - await removeFromCache(envelope); - return; - } + // use the inMemory keypair if found + + const inMemoryKeyPair = distributingClosedGroupEncryptionKeyPairs.get( + groupPubKey + ); + // Get the latest encryption key pair const latestKeyPair = await getLatestClosedGroupEncryptionKeyPair( - groupPublicKey + groupPubKey ); - if (!latestKeyPair) { + if (!inMemoryKeyPair && !latestKeyPair) { window.log.info( 'We do not have the keypair ourself, so dropping this message.' ); - await removeFromCache(envelope); return; } - window.log.info( - `Responding to closed group encryption key pair request from: ${sender}` - ); - await ConversationController.getInstance().getOrCreateAndWait( - sender, - 'private' - ); + const expireTimer = groupConvo.get('expireTimer') || 0; - const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( - [sender], - ECKeyPair.fromHexKeyPair(latestKeyPair) - ); - const expireTimer = convo.get('expireTimer') || 0; + await Promise.all( + targetUsers.map(async member => { + window.log.info( + `Sending latest closed group encryption key pair to: ${member}` + ); + await ConversationController.getInstance().getOrCreateAndWait( + member, + 'private' + ); - const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ - groupId: groupPublicKey, - timestamp: Date.now(), - encryptedKeyPairs: wrappers, - expireTimer, - }); + const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( + [member], + inMemoryKeyPair || ECKeyPair.fromHexKeyPair(latestKeyPair) + ); - // the encryption keypair is sent using established channels - await getMessageQueue().sendToPubKey(PubKey.cast(sender), keypairsMessage); + const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ + groupId: groupPubKey, + timestamp: Date.now(), + encryptedKeyPairs: wrappers, + expireTimer, + }); + + // the encryption keypair is sent using established channels + await getMessageQueue().sendToPubKey( + PubKey.cast(member), + keypairsMessage + ); + }) + ); +} - await removeFromCache(envelope); +async function handleClosedGroupEncryptionKeyPairRequest( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + groupConvo: ConversationModel +) { + if (!window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + throw new Error('useRequestEncryptionKeyPair is disabled'); + } + const sender = envelope.senderIdentity; + const groupPublicKey = envelope.source; + // Guard against self-sends + if (await UserUtils.isUs(sender)) { + window.log.info( + 'Dropping self send message of type ENCRYPTION_KEYPAIR_REQUEST' + ); + await removeFromCache(envelope); + return; + } + await sendLatestKeyPairToUsers(envelope, groupConvo, groupPublicKey, [ + sender, + ]); + return removeFromCache(envelope); } export async function createClosedGroup( diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 9175c8cd6..7b7427c32 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -125,18 +125,23 @@ async function decryptForClosedGroup( 'decryptWithSessionProtocol for medium group message throw:', e ); - const keypairRequestManager = KeyPairRequestManager.getInstance(); const groupPubKey = PubKey.cast(envelope.source); - if (keypairRequestManager.canTriggerRequestWith(groupPubKey)) { - keypairRequestManager.markRequestSendFor(groupPubKey, Date.now()); - await requestEncryptionKeyPair(groupPubKey); + + // To enable back if we decide to enable encryption key pair request work again + if (window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + const keypairRequestManager = KeyPairRequestManager.getInstance(); + if (keypairRequestManager.canTriggerRequestWith(groupPubKey)) { + keypairRequestManager.markRequestSendFor(groupPubKey, Date.now()); + await requestEncryptionKeyPair(groupPubKey); + } } + // IMPORTANT do not remove the message from the cache just yet. + // We will try to decrypt it once we get the encryption keypair. + // for that to work, we need to throw an error just like here. throw new Error( `Waiting for an encryption keypair to be received for group ${groupPubKey.key}` ); - // do not remove it from the cache yet. We will try to decrypt it once we get the encryption keypair - // TODO drop it if after some time we still don't get to decrypt it - // await removeFromCache(envelope); + return null; } } diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index acd980883..d5cc8de88 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -35,6 +35,7 @@ import { ClosedGroupUpdateMessage, } from '../messages/outgoing/content/data/group'; import { MessageController } from '../messages'; +import { distributingClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups'; export interface GroupInfo { id: string; @@ -572,11 +573,15 @@ export async function generateAndSendNewEncryptionKeyPair( expireTimer, }); + distributingClosedGroupEncryptionKeyPairs.set(toHex(groupId), newKeyPair); + const messageSentCallback = async () => { window.log.info( `KeyPairMessage for ClosedGroup ${groupPublicKey} is sent. Saving the new encryptionKeyPair.` ); + distributingClosedGroupEncryptionKeyPairs.delete(toHex(groupId)); + await addClosedGroupEncryptionKeyPair( toHex(groupId), newKeyPair.toHexKeyPair() @@ -626,6 +631,10 @@ export async function buildEncryptionKeyPairWrappers( export async function requestEncryptionKeyPair( groupPublicKey: string | PubKey ) { + if (!window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + throw new Error('useRequestEncryptionKeyPair is disabled'); + } + const groupConvo = ConversationController.getInstance().get( PubKey.cast(groupPublicKey).key ); diff --git a/ts/window.d.ts b/ts/window.d.ts index 6bd056219..2373c9c3b 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -64,6 +64,7 @@ declare global { useFileOnionRequests: boolean; useFileOnionRequestsV2: boolean; onionRequestHops: number; + useRequestEncryptionKeyPair: boolean; }; lokiFileServerAPI: LokiFileServerInstance; lokiMessageAPI: LokiMessageInterface;