From a80f9a5965e63c6afd5eefed17fc6bae08edaed7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 1 Oct 2020 15:12:51 +1000 Subject: [PATCH] fix medium group to match mobile way --- ts/receiver/contentMessage.ts | 42 ++-- ts/receiver/dataMessage.ts | 5 +- ts/receiver/mediumGroups.ts | 10 +- ts/session/medium_group/index.ts | 263 +++++++++----------- ts/session/medium_group/ratchet.ts | 15 +- ts/session/sending/MessageQueue.ts | 7 +- ts/session/sending/MessageQueueInterface.ts | 5 +- ts/session/sending/MessageSender.ts | 1 + ts/session/utils/Messages.ts | 6 +- 9 files changed, 168 insertions(+), 186 deletions(-) diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 7ed9eca8e..b32505438 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -19,6 +19,7 @@ import { BlockedNumberController } from '../util/blockedNumberController'; import { decryptWithSenderKey } from '../session/medium_group/ratchet'; import { StringUtils } from '../session/utils'; import { UserUtil } from '../util'; +import { getMessageQueue } from '../session'; export async function handleContentMessage(envelope: EnvelopePlus) { try { @@ -89,8 +90,7 @@ async function decryptForMediumGroup( groupId, sourceAsStr ); - - return unpad(plaintext); + return plaintext ? unpad(plaintext) : null; } function unpad(paddedData: ArrayBuffer): ArrayBuffer { @@ -288,28 +288,32 @@ async function decrypt( return plaintext; } catch (error) { - if (error && error instanceof textsecure.SenderKeyMissing) { + if ( + error && + (error instanceof textsecure.SenderKeyMissing || + error instanceof DOMException) + ) { const groupId = envelope.source; const { senderIdentity } = error; + if (senderIdentity) { + log.info( + 'Requesting missing key for identity: ', + senderIdentity, + 'groupId: ', + groupId + ); - log.info( - 'Requesting missing key for identity: ', - senderIdentity, - 'groupId: ', - groupId - ); - - const params = { - timestamp: Date.now(), - groupId, - }; + const params = { + timestamp: Date.now(), + groupId, + }; - const requestKeysMessage = new MediumGroupRequestKeysMessage(params); - const sender = new PubKey(senderIdentity); - // tslint:disable-next-line no-floating-promises - libsession.getMessageQueue().send(sender, requestKeysMessage); + const requestKeysMessage = new MediumGroupRequestKeysMessage(params); + const sender = new PubKey(senderIdentity); + void getMessageQueue().send(sender, requestKeysMessage); - return; + return; + } } let errorToThrow = error; diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index ea8049a71..31f3f7cbf 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -279,7 +279,10 @@ export async function handleDataMessage( envelope: EnvelopePlus, dataMessage: SignalService.IDataMessage ): Promise { - window.log.info('data message from', getEnvelopeId(envelope)); + window.log.info( + 'data message from', + getEnvelopeId(envelope) + ); if (dataMessage.mediumGroupUpdate) { await handleMediumGroupUpdate(envelope, dataMessage.mediumGroupUpdate); diff --git a/ts/receiver/mediumGroups.ts b/ts/receiver/mediumGroups.ts index c3312c1df..74eaaebc3 100644 --- a/ts/receiver/mediumGroups.ts +++ b/ts/receiver/mediumGroups.ts @@ -243,10 +243,10 @@ function sanityCheckMediumGroupUpdate( const joining = diff.joiningMembers || []; const leaving = diff.leavingMembers || []; - // 1. When there are no member changes, we don't expect any sender keys + // 1. When there are no member changes, we expect all sender keys if (!joining.length && !leaving.length) { - if (groupUpdate.senderKeys.length) { - window.log.error('Unexpected sender keys in group update'); + if (groupUpdate.senderKeys.length !== groupUpdate.members.length) { + window.log.error('Incorrect number of sender keys in group update'); } } @@ -270,13 +270,10 @@ async function handleMediumGroupChange( envelope: EnvelopePlus, groupUpdate: SignalService.MediumGroupUpdate ) { - const senderIdentity = envelope.source; - const { name, groupPublicKey, members: membersBinary, - admins: adminsBinary, senderKeys, } = groupUpdate; const { log } = window; @@ -342,6 +339,7 @@ async function handleMediumGroupChange( convo.set('isKickedFromGroup', true); // Disable typing: convo.updateTextInputState(); + window.SwarmPolling.removePubkey(groupId); } await convo.commit(); diff --git a/ts/session/medium_group/index.ts b/ts/session/medium_group/index.ts index aeb72d767..51324250f 100644 --- a/ts/session/medium_group/index.ts +++ b/ts/session/medium_group/index.ts @@ -208,19 +208,18 @@ export async function leaveMediumGroup(groupId: string) { sent_at: now, received_at: now, }); + const ourPrimary = await UserUtil.getPrimary(); + const members = convo.get('members').filter(m => m !== ourPrimary.key); // do not include senderkey as everyone needs to generate new one const groupUpdate: GroupInfo = { id: convo.get('id'), name: convo.get('name'), - members: convo.get('members'), + members, is_medium_group: true, admins: convo.get('groupAdmins'), - senderKeysContainer: undefined, }; - const ourPrimary = await UserUtil.getPrimary(); - await sendGroupUpdateForMedium( { leavingMembers: [ourPrimary.key] }, groupUpdate, @@ -260,24 +259,23 @@ async function getExistingSenderKeysForGroup( return maybeKeys.filter(d => d !== null).map(d => d as RatchetState); } -// Create all sender keys based on the changes in -// the group's composition -async function getOrCreateSenderKeysForUpdate( +// Get a list of senderKeys we have to send to joining members +// Basically this is the senderkey of all members who joined concatenated with +// the one of members currently in the group. + +// Also, the list of senderkeys for existing member must be empty if there is any leaving members, +// as they each member need to regenerate a new senderkey +async function getOrUpdateSenderKeysForJoiningMembers( groupId: string, members: Array, - changes: MemberChanges -): Promise { - // 1. Create sender keys for every joining member - const joining = changes.joiningMembers || []; - const leaving = changes.leavingMembers || []; - - let newKeys = await createSenderKeysForMembers(groupId, joining); - - // 2. Get ratchet states for existing members + diff?: GroupDiff, + joiningMembersSenderKeys?: Array +): Promise> { + const leavingMembers = diff?.leavingMembers || []; + const joiningMembers = diff?.joiningMembers || []; - const existingMembers = _.difference(members, joining); + const existingMembers = _.difference(members, joiningMembers); // get all devices for members - const allDevices = _.flatten( await Promise.all( existingMembers.map(m => MultiDeviceProtocol.getAllDevices(m)) @@ -285,23 +283,10 @@ async function getOrCreateSenderKeysForUpdate( ); let existingKeys: Array = []; - - if (leaving.length > 0) { - // If we have leaving members, we have to re-generate ratchet - // keys for existing members - const otherKeys = await Promise.all( - allDevices.map(async device => { - return createSenderKeyForGroup(groupId, PubKey.cast(device)); - }) - ); - - newKeys = _.union(newKeys, otherKeys); - } else { - // We can reuse existing keys + if (leavingMembers.length === 0) { existingKeys = await getExistingSenderKeysForGroup(groupId, allDevices); } - - return { existingKeys, newKeys }; + return _.union(joiningMembersSenderKeys, existingKeys); } async function getGroupSecretKey(groupId: string): Promise { @@ -322,6 +307,9 @@ async function getGroupSecretKey(groupId: string): Promise { } async function syncMediumGroup(group: ConversationModel) { + throw new Error( + 'Medium group syncing must be done once multi device is enabled back' + ); const ourPrimary = await UserUtil.getPrimary(); const groupId = group.get('id'); @@ -352,7 +340,6 @@ async function syncMediumGroup(group: ConversationModel) { members: group.get('members'), is_medium_group: true, admins: group.get('groupAdmins'), - senderKeysContainer, secretKey, }; @@ -418,12 +405,7 @@ export async function initiateGroupUpdate( }; if (isMediumGroup) { - // Send sender keys and group secret key - updateObj.senderKeysContainer = await getOrCreateSenderKeysForUpdate( - groupId, - members, - diff - ); + // Send group secret key const secretKey = await getGroupSecretKey(groupId); updateObj.secretKey = secretKey; @@ -531,7 +513,6 @@ interface GroupInfo { blocked?: boolean; admins?: Array; secretKey?: Uint8Array; - senderKeysContainer?: SenderKeysContainer; } interface UpdatableGroupState { @@ -605,15 +586,16 @@ export function calculateGroupDiff( return groupDiff; } -async function sendGroupUpdateForExistingMembers( - leavingMembers: Array, - remainingMembers: Array, +async function sendGroupUpdateForMedium( + diff: MemberChanges, groupUpdate: GroupInfo, messageId?: string ) { const { id: groupId, members, name: groupName } = groupUpdate; const ourPrimary = await UserUtil.getPrimary(); + const leavingMembers = diff.leavingMembers || []; + const joiningMembers = diff.joiningMembers || []; const wasAnyUserRemoved = leavingMembers.length > 0; const isUserLeaving = leavingMembers.includes(ourPrimary.key); @@ -626,10 +608,7 @@ async function sendGroupUpdateForExistingMembers( (pkHex: string) => new Uint8Array(fromHex(pkHex)) ); - // Existing members only receive new sender keys - const senderKeys = groupUpdate.senderKeysContainer - ? groupUpdate.senderKeysContainer.newKeys - : []; + const remainingMembers = _.difference(groupUpdate.members, joiningMembers); const params = { timestamp: Date.now(), @@ -638,7 +617,6 @@ async function sendGroupUpdateForExistingMembers( members: membersBin, groupName, admins: adminsBin, - senderKeys: senderKeys, }; if (wasAnyUserRemoved) { @@ -652,116 +630,101 @@ async function sendGroupUpdateForExistingMembers( senderKeys: [], }; - const messageStripped = new MediumGroupUpdateMessage(paramsWithoutSenderKeys); + const messageStripped = new MediumGroupUpdateMessage( + paramsWithoutSenderKeys + ); window.log.warn('Sending to groupUpdateMessage without senderKeys'); await getMessageQueue().sendToGroup(messageStripped); - // TODO Delete all ratchets (it's important that this happens * after * sending out the update) - if (isUserLeaving) { - // nothing to do on desktop - } else { - // Send out the user's new ratchet to all members (minus the removed ones) using established channels - const ourPrimaryKey = new Uint8Array(fromHex(ourPrimary.key)) - const ourNewSenderKey = senderKeys.find(s => _.isEqual(s.pubKey, ourPrimaryKey)); - - if (! ourNewSenderKey) { - window.console.warn('We need to share our senderkey with remaining member but our senderKeys was not given.'); - } else { - window.log.warn('Sharing our new senderKey with remainingMembers via message', remainingMembers, ourNewSenderKey); - await shareSenderKeys(groupId, remainingMembers, ourNewSenderKey); + getMessageQueue().events.addListener('success', async message => { + if (message.identifier === params.identifier) { + // console.log('Our first message encrypted with old sk is sent.'); + // TODO Delete all ratchets (it's important that this happens * after * sending out the update) + if (isUserLeaving) { + // nothing to do on desktop + } else { + // Send out the user's new ratchet to all members (minus the removed ones) using established channels + const userSenderKey = await createSenderKeyForGroup( + groupId, + ourPrimary + ); + window.log.warn( + 'Sharing our new senderKey with remainingMembers via message', + remainingMembers, + userSenderKey + ); + + await shareSenderKeys(groupId, remainingMembers, userSenderKey); + } } - - } - } else { - const message = new MediumGroupUpdateMessage(params); - window.log.warn('Sending to groupUpdateMessage with senderKeys to groupAddress', senderKeys); - - await getMessageQueue().sendToGroup(message); - } - -} - -async function sendGroupUpdateForJoiningMembers( - recipients: Array, - groupUpdate: GroupInfo, - messageId?: string -) { - const { id: groupId, name, members } = groupUpdate; - - const now = Date.now(); - - const { secretKey, senderKeysContainer } = groupUpdate; - - if (!secretKey) { - window.log.error('Group secret key not specified, aborting...'); - return; - } - - let senderKeys: Array = []; - if (!senderKeysContainer) { - window.log.warn('Sender keys for joining members not found'); + }); } else { - // Joining members should receive all known sender keys - senderKeys = _.union( - senderKeysContainer.existingKeys, - senderKeysContainer.newKeys - ); - } - - const membersBin = members.map( - (pkHex: string) => new Uint8Array(fromHex(pkHex)) - ); - - const admins = groupUpdate.admins || []; - - const adminsBin = admins.map( - (pkHex: string) => new Uint8Array(fromHex(pkHex)) - ); - - const createParams = { - timestamp: now, - groupId, - identifier: messageId || uuid(), - groupSecretKey: secretKey, - members: membersBin, - groupName: name, - admins: adminsBin, - senderKeys, - }; - - const mediumGroupCreateMessage = new MediumGroupCreateMessage(createParams); - - recipients.forEach(async member => { - const memberPubKey = new PubKey(member); - await getMessageQueue().sendUsingMultiDevice( - memberPubKey, - mediumGroupCreateMessage + let senderKeys: Array; + if (joiningMembers.length > 0) { + // Generate ratchets for any new members + senderKeys = await createSenderKeysForMembers(groupId, joiningMembers); + } else { + // It's not a member change, maybe an name change. So just reuse all senderkeys + senderKeys = await getOrUpdateSenderKeysForJoiningMembers( + groupId, + members + ); + } + const paramsWithSenderKeys = { + ...params, + senderKeys, + }; + // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) + const message = new MediumGroupUpdateMessage(paramsWithSenderKeys); + window.log.warn( + 'Sending to groupUpdateMessage with joining members senderKeys to groupAddress', + senderKeys ); - }); -} -async function sendGroupUpdateForMedium( - diff: MemberChanges, - groupUpdate: GroupInfo, - messageId?: string -) { - const joining = diff.joiningMembers || []; - const leaving = diff.leavingMembers || []; + await getMessageQueue().sendToGroup(message); - // 1. create group for all joining members (send timeout timer if necessary) - if (joining.length) { - await sendGroupUpdateForJoiningMembers(joining, groupUpdate, messageId); - } + // now send a CREATE group message with all senderkeys no matter what to all joining members, using established channels + if (joiningMembers.length) { + const { secretKey } = groupUpdate; - // 2. send group update to all other members - const others = _.difference(groupUpdate.members, joining); - if (others.length) { - await sendGroupUpdateForExistingMembers( - leaving, - others, - groupUpdate, - messageId - ); + if (!secretKey) { + window.log.error('Group secret key not specified, aborting...'); + return; + } + const allSenderKeys = await getOrUpdateSenderKeysForJoiningMembers( + groupId, + members + ); + + const createParams = { + timestamp: Date.now(), + identifier: messageId || uuid(), + groupSecretKey: secretKey, + groupId, + members: membersBin, + groupName, + admins: adminsBin, + senderKeys: allSenderKeys, + }; + + const mediumGroupCreateMessage = new MediumGroupCreateMessage( + createParams + ); + // console.warn( + // 'sending group create to', + // joiningMembers, + // ' obj: ', + // mediumGroupCreateMessage + // ); + + joiningMembers.forEach(async member => { + const memberPubKey = new PubKey(member); + await getMessageQueue().sendUsingMultiDevice( + memberPubKey, + mediumGroupCreateMessage + ); + }); + } } } diff --git a/ts/session/medium_group/ratchet.ts b/ts/session/medium_group/ratchet.ts index 223a16266..e64fd5ffd 100644 --- a/ts/session/medium_group/ratchet.ts +++ b/ts/session/medium_group/ratchet.ts @@ -14,12 +14,15 @@ async function queueJobForNumber(number: string, runJob: any) { const runCurrent = runPrevious.then(runJob, runJob); jobQueue[number] = runCurrent; // tslint:disable-next-line no-floating-promises - runCurrent.then(() => { - if (jobQueue[number] === runCurrent) { - // tslint:disable-next-line no-dynamic-delete - delete jobQueue[number]; - } - }); + runCurrent + .then(() => { + if (jobQueue[number] === runCurrent) { + // tslint:disable-next-line no-dynamic-delete + delete jobQueue[number]; + } + }).catch((e: any) => { + window.log.error('queueJobForNumber() Caught error', e); + }); return runCurrent; } diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 4cddd8e73..ca495f63c 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -7,6 +7,7 @@ import { ClosedGroupMessage, ContentMessage, ExpirationTimerUpdateMessage, + MediumGroupChatMessage, MediumGroupMessage, OpenGroupMessage, SessionRequestMessage, @@ -102,8 +103,10 @@ export class MessageQueue implements MessageQueueInterface { throw new Error('Invalid group message passed in sendToGroup.'); } // if this is a medium group message. We just need to send to the group pubkey - if (message instanceof MediumGroupMessage) { - window.log.warn('sending medium ', message, ' to ', groupId) + if ( + message instanceof MediumGroupMessage || + message instanceof MediumGroupChatMessage + ) { return this.send(PubKey.cast(groupId), message); } diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts index af44e6e7f..596b84bea 100644 --- a/ts/session/sending/MessageQueueInterface.ts +++ b/ts/session/sending/MessageQueueInterface.ts @@ -9,7 +9,10 @@ import { RawMessage } from '../types/RawMessage'; import { TypedEventEmitter } from '../utils'; import { PubKey } from '../types'; -type GroupMessageType = OpenGroupMessage | ClosedGroupMessage | MediumGroupMessage; +type GroupMessageType = + | OpenGroupMessage + | ClosedGroupMessage + | MediumGroupMessage; export interface MessageQueueInterfaceEvents { success: ( diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 6341274d7..374fc2d10 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -45,6 +45,7 @@ export async function send( timestamp, cipherText ); + // console.warn('sending', envelope, ' to ', device.key); const data = wrapEnvelope(envelope); return pRetry( diff --git a/ts/session/utils/Messages.ts b/ts/session/utils/Messages.ts index 5169afdd7..3d84b0c59 100644 --- a/ts/session/utils/Messages.ts +++ b/ts/session/utils/Messages.ts @@ -6,6 +6,7 @@ import { } from '../messages/outgoing'; import { EncryptionType, PubKey } from '../types'; import { SessionProtocol } from '../protocols'; +import { MediumGroupUpdateMessage } from '../messages/outgoing/content/data/mediumgroup/MediumGroupUpdateMessage'; export async function toRawMessage( device: PubKey, @@ -16,7 +17,10 @@ export async function toRawMessage( const plainTextBuffer = message.plainTextBuffer(); let encryption: EncryptionType; - if (message instanceof MediumGroupChatMessage) { + if ( + message instanceof MediumGroupChatMessage || + message instanceof MediumGroupUpdateMessage + ) { encryption = EncryptionType.MediumGroup; } else if (message instanceof SessionRequestMessage) { encryption = EncryptionType.Fallback;