diff --git a/app/sql.js b/app/sql.js index cfbabe14a..b9a1128ce 100644 --- a/app/sql.js +++ b/app/sql.js @@ -3215,6 +3215,12 @@ async function updateExistingClosedGroupToClosedGroup(instance) { * @param {*} groupPublicKey string | PubKey */ async function getAllEncryptionKeyPairsForGroup(groupPublicKey) { + const rows = await getAllEncryptionKeyPairsForGroupRaw(groupPublicKey); + + return map(rows, row => jsonToObject(row.json)); +} + +async function getAllEncryptionKeyPairsForGroupRaw(groupPublicKey) { const pubkeyAsString = groupPublicKey.key ? groupPublicKey.key : groupPublicKey; @@ -3225,7 +3231,7 @@ async function getAllEncryptionKeyPairsForGroup(groupPublicKey) { } ); - return map(rows, row => jsonToObject(row.json)); + return rows; } async function getLatestClosedGroupEncryptionKeyPair(groupPublicKey) { diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index a594676e9..4f5d12dd9 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -21,6 +21,7 @@ import { import { ECKeyPair } from './keypairs'; import { UserUtils } from '../session/utils'; import { ConversationModel } from '../models/conversation'; +import _ from 'lodash'; export async function handleClosedGroupControlMessage( envelope: EnvelopePlus, @@ -295,6 +296,7 @@ async function handleUpdateClosedGroup( convo.set('members', members); await convo.commit(); + convo.updateLastMessage(); await removeFromCache(envelope); } @@ -315,6 +317,9 @@ async function handleClosedGroupEncryptionKeyPair( return; } const ourNumber = UserUtils.getOurPubKeyFromCache(); + window.log.info( + `Got a group update for group ${envelope.source}, type: ENCRYPTION_KEY_PAIR` + ); const groupPublicKey = envelope.source; const ourKeyPair = await UserUtils.getIdentityKeyPair(); @@ -485,12 +490,17 @@ async function handleClosedGroupNameChanged( ) { // Only add update message if we have something to show const newName = groupUpdate.name; + window.log.info( + `Got a group update for group ${envelope.source}, type: NAME_CHANGED` + ); + if (newName !== convo.get('name')) { const groupDiff: ClosedGroup.GroupDiff = { newName, }; await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); convo.set({ name: newName }); + convo.updateLastMessage(); await convo.commit(); } @@ -508,6 +518,10 @@ async function handleClosedGroupMembersAdded( const membersNotAlreadyPresent = addedMembers.filter( m => !oldMembers.includes(m) ); + console.warn('membersNotAlreadyPresent', membersNotAlreadyPresent); + window.log.info( + `Got a group update for group ${envelope.source}, type: MEMBERS_ADDED` + ); if (membersNotAlreadyPresent.length === 0) { window.log.info( @@ -526,6 +540,7 @@ async function handleClosedGroupMembersAdded( await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); convo.set({ members }); + convo.updateLastMessage(); await convo.commit(); await removeFromCache(envelope); } @@ -534,7 +549,77 @@ async function handleClosedGroupMembersRemoved( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, convo: ConversationModel -) { } +) { + // Check that the admin wasn't removed + const currentMembers = convo.get('members'); + // removedMembers are all members in the diff + const removedMembers = groupUpdate.members.map(toHex); + // effectivelyRemovedMembers are the members which where effectively on this group before the update + // and is used for the group update message only + const effectivelyRemovedMembers = removedMembers.filter(m => + currentMembers.includes(m) + ); + const groupPubKey = envelope.source; + window.log.info( + `Got a group update for group ${envelope.source}, type: MEMBERS_REMOVED` + ); + + const membersAfterUpdate = _.difference(currentMembers, removedMembers); + const groupAdmins = convo.get('groupAdmins'); + if (!groupAdmins?.length) { + throw new Error('No admins found for closed group member removed update.'); + } + const firstAdmin = groupAdmins[0]; + + if (removedMembers.includes(firstAdmin)) { + window.log.warn( + 'Ignoring invalid closed group update: trying to remove the admin.' + ); + await removeFromCache(envelope); + return; + } + + // If the current user was removed: + // • Stop polling for the group + // • Remove the key pairs associated with the group + const ourPubKey = UserUtils.getOurPubKeyFromCache(); + const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key); + if (wasCurrentUserRemoved) { + await window.Signal.Data.removeAllClosedGroupEncryptionKeyPairs( + groupPubKey + ); + // Disable typing: + convo.set('isKickedFromGroup', true); + window.SwarmPolling.removePubkey(groupPubKey); + } + // Generate and distribute a new encryption key pair if needed + const isCurrentUserAdmin = firstAdmin === ourPubKey.key; + if (isCurrentUserAdmin) { + try { + await ClosedGroup.generateAndSendNewEncryptionKeyPair( + groupPubKey, + membersAfterUpdate + ); + } catch (e) { + window.log.warn('Could not distribute new encryption keypair.'); + } + } + + // Only add update message if we have something to show + if (membersAfterUpdate.length !== currentMembers.length) { + const groupDiff: ClosedGroup.GroupDiff = { + leavingMembers: effectivelyRemovedMembers, + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); + convo.updateLastMessage(); + } + + // Update the group + convo.set({ members: membersAfterUpdate }); + + await convo.commit(); + await removeFromCache(envelope); +} async function handleClosedGroupMemberLeft( envelope: EnvelopePlus, @@ -578,7 +663,6 @@ async function handleClosedGroupMemberLeft( convo.set('isKickedFromGroup', true); window.SwarmPolling.removePubkey(groupPublicKey); } - // Update the group // Only add update message if we have something to show if (leftMemberWasPresent) { @@ -586,6 +670,7 @@ async function handleClosedGroupMemberLeft( leavingMembers: didAdminLeave ? oldMembers : [sender], }; await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); + convo.updateLastMessage(); } convo.set('members', members); @@ -648,7 +733,9 @@ export async function createClosedGroup( // the sending pipeline needs to know from GroupUtils when a message is for a medium group await ClosedGroup.updateOrCreateClosedGroup(groupDetails); convo.set('lastJoinedTimestamp', Date.now()); + convo.set('active_at', Date.now()); await convo.commit(); + convo.updateLastMessage(); // Send a closed group update message to all members individually const promises = listOfMembers.map(async m => { diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 047366ec3..5146390ed 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -1,6 +1,5 @@ import { EnvelopePlus } from './types'; import { handleDataMessage } from './dataMessage'; -import { getEnvelopeId } from './common'; import { removeFromCache, updateCache } from './cache'; import { SignalService } from '../protobuf'; @@ -75,6 +74,9 @@ async function decryptForClosedGroup( encryptionKeyPair, true ); + if (decryptedContent?.byteLength) { + break; + } keyIndex++; } catch (e) { window.log.info( @@ -83,13 +85,21 @@ async function decryptForClosedGroup( } } while (encryptionKeyPairs.length > 0); - if (!decryptedContent) { + if (!decryptedContent?.byteLength) { await removeFromCache(envelope); throw new Error( `Could not decrypt message for closed group with any of the ${encryptionKeyPairsCount} keypairs.` ); } - window.log.info('ClosedGroup Message decrypted successfully.'); + if (keyIndex !== 0) { + window.log.warn( + 'Decrypted a closed group message with not the latest encryptionkeypair we have' + ); + } + window.log.info( + 'ClosedGroup Message decrypted successfully with keyIndex:', + keyIndex + ); const ourDevicePubKey = UserUtils.getOurPubKeyStrFromCache(); if ( @@ -483,7 +493,7 @@ async function handleTypingMessage( const started = action === SignalService.TypingMessage.Action.STARTED; if (conversation) { - conversation.notifyTyping({ + await conversation.notifyTyping({ isTyping: started, sender: source, }); diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 6cc04e6de..28e32ff5b 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -114,7 +114,7 @@ export async function encryptUsingSessionProtocol( window?.log?.info( 'encryptUsingSessionProtocol for ', - recipientHexEncodedX25519PublicKey + recipientHexEncodedX25519PublicKey.key ); const recipientX25519PublicKey = recipientHexEncodedX25519PublicKey.withoutPrefixToArray(); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index b7f15cb05..088fb4a24 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -341,13 +341,13 @@ export async function leaveClosedGroup(groupId: string) { expireTimer: 0, }); window.getMessageController().register(dbMessage.id, dbMessage); - + const existingExpireTimer = convo.get('expireTimer') || 0; // Send the update to the group const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ timestamp: Date.now(), groupId, identifier: dbMessage.id, - expireTimer: 0, + expireTimer: existingExpireTimer, }); window.log.info(