finish explicit group updates

pull/1461/head
Audric Ackermann 4 years ago
parent 0e25ab2874
commit 334da0a169
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -3215,6 +3215,12 @@ async function updateExistingClosedGroupToClosedGroup(instance) {
* @param {*} groupPublicKey string | PubKey * @param {*} groupPublicKey string | PubKey
*/ */
async function getAllEncryptionKeyPairsForGroup(groupPublicKey) { async function getAllEncryptionKeyPairsForGroup(groupPublicKey) {
const rows = await getAllEncryptionKeyPairsForGroupRaw(groupPublicKey);
return map(rows, row => jsonToObject(row.json));
}
async function getAllEncryptionKeyPairsForGroupRaw(groupPublicKey) {
const pubkeyAsString = groupPublicKey.key const pubkeyAsString = groupPublicKey.key
? groupPublicKey.key ? groupPublicKey.key
: groupPublicKey; : groupPublicKey;
@ -3225,7 +3231,7 @@ async function getAllEncryptionKeyPairsForGroup(groupPublicKey) {
} }
); );
return map(rows, row => jsonToObject(row.json)); return rows;
} }
async function getLatestClosedGroupEncryptionKeyPair(groupPublicKey) { async function getLatestClosedGroupEncryptionKeyPair(groupPublicKey) {

@ -22,6 +22,7 @@ import { ECKeyPair } from './keypairs';
import { getOurNumber } from '../session/utils/User'; import { getOurNumber } from '../session/utils/User';
import { UserUtils } from '../session/utils'; import { UserUtils } from '../session/utils';
import { ConversationModel } from '../../js/models/conversations'; import { ConversationModel } from '../../js/models/conversations';
import _ from 'lodash';
export async function handleClosedGroupControlMessage( export async function handleClosedGroupControlMessage(
envelope: EnvelopePlus, envelope: EnvelopePlus,
@ -40,7 +41,13 @@ export async function handleClosedGroupControlMessage(
await handleClosedGroupEncryptionKeyPair(envelope, groupUpdate); await handleClosedGroupEncryptionKeyPair(envelope, groupUpdate);
} else if (type === Type.NEW) { } else if (type === Type.NEW) {
await handleNewClosedGroup(envelope, groupUpdate); await handleNewClosedGroup(envelope, groupUpdate);
} else if (type === Type.NAME_CHANGE || type === Type.MEMBERS_REMOVED || type === Type.MEMBERS_ADDED || type === Type.MEMBER_LEFT || type === Type.UPDATE) { } else if (
type === Type.NAME_CHANGE ||
type === Type.MEMBERS_REMOVED ||
type === Type.MEMBERS_ADDED ||
type === Type.MEMBER_LEFT ||
type === Type.UPDATE
) {
await performIfValid(envelope, groupUpdate); await performIfValid(envelope, groupUpdate);
} else { } else {
window.log.error('Unknown group update type: ', type); window.log.error('Unknown group update type: ', type);
@ -230,7 +237,6 @@ async function handleUpdateClosedGroup(
const curAdmins = convo.get('groupAdmins'); const curAdmins = convo.get('groupAdmins');
// NOTE: admins cannot change with closed groups // NOTE: admins cannot change with closed groups
const members = membersBinary.map(toHex); const members = membersBinary.map(toHex);
const diff = ClosedGroup.buildGroupDiff(convo, { name, members }); const diff = ClosedGroup.buildGroupDiff(convo, { name, members });
@ -291,6 +297,7 @@ async function handleUpdateClosedGroup(
convo.set('members', members); convo.set('members', members);
await convo.commit(); await convo.commit();
convo.updateLastMessage();
await removeFromCache(envelope); await removeFromCache(envelope);
} }
@ -310,6 +317,9 @@ async function handleClosedGroupEncryptionKeyPair(
) { ) {
return; return;
} }
window.log.info(
`Got a group update for group ${envelope.source}, type: ENCRYPTION_KEY_PAIR`
);
const ourNumber = await UserUtils.getOurNumber(); const ourNumber = await UserUtils.getOurNumber();
const groupPublicKey = envelope.source; const groupPublicKey = envelope.source;
const ourKeyPair = await UserUtils.getIdentityKeyPair(); const ourKeyPair = await UserUtils.getIdentityKeyPair();
@ -410,9 +420,10 @@ async function handleClosedGroupEncryptionKeyPair(
await removeFromCache(envelope); await removeFromCache(envelope);
} }
async function performIfValid(
async function performIfValid(envelope: EnvelopePlus, envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage) { groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage
) {
const { Type } = SignalService.DataMessage.ClosedGroupControlMessage; const { Type } = SignalService.DataMessage.ClosedGroupControlMessage;
const groupPublicKey = envelope.source; const groupPublicKey = envelope.source;
@ -470,7 +481,6 @@ async function performIfValid(envelope: EnvelopePlus,
await handleClosedGroupMemberLeft(envelope, groupUpdate, convo); await handleClosedGroupMemberLeft(envelope, groupUpdate, convo);
} }
return true; return true;
} }
@ -481,18 +491,20 @@ async function handleClosedGroupNameChanged(
) { ) {
// Only add update message if we have something to show // Only add update message if we have something to show
const newName = groupUpdate.name; const newName = groupUpdate.name;
if ( window.log.info(
newName !== convo.get(('name')) `Got a group update for group ${envelope.source}, type: NAME_CHANGED`
) { );
if (newName !== convo.get('name')) {
const groupDiff: ClosedGroup.GroupDiff = { const groupDiff: ClosedGroup.GroupDiff = {
newName, newName,
}; };
await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming');
convo.set({ name: newName }); convo.set({ name: newName });
convo.updateLastMessage();
await convo.commit(); await convo.commit();
} }
await removeFromCache(envelope); await removeFromCache(envelope);
} }
@ -504,11 +516,18 @@ async function handleClosedGroupMembersAdded(
const { members: addedMembersBinary } = groupUpdate; const { members: addedMembersBinary } = groupUpdate;
const addedMembers = (addedMembersBinary || []).map(toHex); const addedMembers = (addedMembersBinary || []).map(toHex);
const oldMembers = convo.get('members') || []; const oldMembers = convo.get('members') || [];
const membersNotAlreadyPresent = addedMembers.filter(m => !oldMembers.includes(m)); const membersNotAlreadyPresent = addedMembers.filter(
m => !oldMembers.includes(m)
);
console.warn('membersNotAlreadyPresent', membersNotAlreadyPresent); console.warn('membersNotAlreadyPresent', membersNotAlreadyPresent);
window.log.info(
`Got a group update for group ${envelope.source}, type: MEMBERS_ADDED`
);
if (membersNotAlreadyPresent.length === 0) { if (membersNotAlreadyPresent.length === 0) {
window.log.info('no new members in this group update compared to what we have already. Skipping update'); window.log.info(
'no new members in this group update compared to what we have already. Skipping update'
);
await removeFromCache(envelope); await removeFromCache(envelope);
return; return;
} }
@ -522,6 +541,7 @@ async function handleClosedGroupMembersAdded(
await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming');
convo.set({ members }); convo.set({ members });
convo.updateLastMessage();
await convo.commit(); await convo.commit();
await removeFromCache(envelope); await removeFromCache(envelope);
} }
@ -529,8 +549,77 @@ async function handleClosedGroupMembersAdded(
async function handleClosedGroupMembersRemoved( async function handleClosedGroupMembersRemoved(
envelope: EnvelopePlus, envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
convo: ConversationModel) { 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 = await UserUtils.getOurNumber();
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( async function handleClosedGroupMemberLeft(
@ -545,7 +634,7 @@ async function handleClosedGroupMemberLeft(
// otherwise, we remove the sender from the list of current members in this group // otherwise, we remove the sender from the list of current members in this group
const oldMembers = convo.get('members') || []; const oldMembers = convo.get('members') || [];
const leftMemberWasPresent = oldMembers.includes(sender); const leftMemberWasPresent = oldMembers.includes(sender);
const members = didAdminLeave ? [] : (oldMembers).filter(s => s !== sender); const members = didAdminLeave ? [] : oldMembers.filter(s => s !== sender);
// Guard against self-sends // Guard against self-sends
const ourPubkey = await UserUtils.getCurrentDevicePubKey(); const ourPubkey = await UserUtils.getCurrentDevicePubKey();
if (!ourPubkey) { if (!ourPubkey) {
@ -558,9 +647,13 @@ async function handleClosedGroupMemberLeft(
} }
// Generate and distribute a new encryption key pair if needed // Generate and distribute a new encryption key pair if needed
const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourPubkey) || false; const isCurrentUserAdmin =
convo.get('groupAdmins')?.includes(ourPubkey) || false;
if (isCurrentUserAdmin && !!members.length) { if (isCurrentUserAdmin && !!members.length) {
await ClosedGroup.generateAndSendNewEncryptionKeyPair(groupPublicKey, members); await ClosedGroup.generateAndSendNewEncryptionKeyPair(
groupPublicKey,
members
);
} }
if (didAdminLeave) { if (didAdminLeave) {
@ -571,16 +664,14 @@ async function handleClosedGroupMemberLeft(
convo.set('isKickedFromGroup', true); convo.set('isKickedFromGroup', true);
window.SwarmPolling.removePubkey(groupPublicKey); window.SwarmPolling.removePubkey(groupPublicKey);
} }
// Update the group
// Only add update message if we have something to show // Only add update message if we have something to show
if ( if (leftMemberWasPresent) {
leftMemberWasPresent
) {
const groupDiff: ClosedGroup.GroupDiff = { const groupDiff: ClosedGroup.GroupDiff = {
leavingMembers: didAdminLeave ? oldMembers : [sender], leavingMembers: didAdminLeave ? oldMembers : [sender],
}; };
await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming');
convo.updateLastMessage();
} }
convo.set('members', members); convo.set('members', members);
@ -590,7 +681,6 @@ async function handleClosedGroupMemberLeft(
await removeFromCache(envelope); await removeFromCache(envelope);
} }
export async function createClosedGroup( export async function createClosedGroup(
groupName: string, groupName: string,
members: Array<string> members: Array<string>
@ -644,7 +734,9 @@ export async function createClosedGroup(
// the sending pipeline needs to know from GroupUtils when a message is for a medium group // the sending pipeline needs to know from GroupUtils when a message is for a medium group
await ClosedGroup.updateOrCreateClosedGroup(groupDetails); await ClosedGroup.updateOrCreateClosedGroup(groupDetails);
convo.set('lastJoinedTimestamp', Date.now()); convo.set('lastJoinedTimestamp', Date.now());
convo.set('active_at', Date.now());
await convo.commit(); await convo.commit();
convo.updateLastMessage();
// Send a closed group update message to all members individually // Send a closed group update message to all members individually
const promises = listOfMembers.map(async m => { const promises = listOfMembers.map(async m => {

@ -75,6 +75,9 @@ async function decryptForClosedGroup(
encryptionKeyPair, encryptionKeyPair,
true true
); );
if (decryptedContent?.byteLength) {
break;
}
keyIndex++; keyIndex++;
} catch (e) { } catch (e) {
window.log.info( window.log.info(
@ -83,13 +86,21 @@ async function decryptForClosedGroup(
} }
} while (encryptionKeyPairs.length > 0); } while (encryptionKeyPairs.length > 0);
if (!decryptedContent) { if (!decryptedContent?.byteLength) {
await removeFromCache(envelope); await removeFromCache(envelope);
throw new Error( throw new Error(
`Could not decrypt message for closed group with any of the ${encryptionKeyPairsCount} keypairs.` `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 = await UserUtils.getCurrentDevicePubKey(); const ourDevicePubKey = await UserUtils.getCurrentDevicePubKey();
if ( if (

@ -251,7 +251,10 @@ export async function handleDataMessage(
window.log.info('data message from', getEnvelopeId(envelope)); window.log.info('data message from', getEnvelopeId(envelope));
if (dataMessage.closedGroupControlMessage) { if (dataMessage.closedGroupControlMessage) {
await handleClosedGroupControlMessage(envelope, dataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage); await handleClosedGroupControlMessage(
envelope,
dataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage
);
return; return;
} }

@ -114,7 +114,7 @@ export async function encryptUsingSessionProtocol(
window?.log?.info( window?.log?.info(
'encryptUsingSessionProtocol for ', 'encryptUsingSessionProtocol for ',
recipientHexEncodedX25519PublicKey recipientHexEncodedX25519PublicKey.key
); );
const recipientX25519PublicKey = recipientHexEncodedX25519PublicKey.withoutPrefixToArray(); const recipientX25519PublicKey = recipientHexEncodedX25519PublicKey.withoutPrefixToArray();

@ -340,13 +340,13 @@ export async function leaveClosedGroup(groupId: string) {
received_at: now, received_at: now,
}); });
window.getMessageController().register(dbMessage.id, dbMessage); window.getMessageController().register(dbMessage.id, dbMessage);
const existingExpireTimer = convo.get('expireTimer') || 0;
// Send the update to the group // Send the update to the group
const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ const ourLeavingMessage = new ClosedGroupMemberLeftMessage({
timestamp: Date.now(), timestamp: Date.now(),
groupId, groupId,
identifier: dbMessage.id, identifier: dbMessage.id,
expireTimer: 0, expireTimer: existingExpireTimer,
}); });
window.log.info( window.log.info(

@ -5,11 +5,13 @@ import { describe } from 'mocha';
import { GroupUtils, PromiseUtils, UserUtils } from '../../../../session/utils'; import { GroupUtils, PromiseUtils, UserUtils } from '../../../../session/utils';
import { TestUtils } from '../../../../test/test-utils'; import { TestUtils } from '../../../../test/test-utils';
import { generateEnvelopePlusClosedGroup, generateGroupUpdateNameChange } from '../../../test-utils/utils/envelope'; import {
generateEnvelopePlusClosedGroup,
generateGroupUpdateNameChange,
} from '../../../test-utils/utils/envelope';
import { handleClosedGroupControlMessage } from '../../../../receiver/closedGroups'; import { handleClosedGroupControlMessage } from '../../../../receiver/closedGroups';
import { ConversationController } from '../../../../session/conversations'; import { ConversationController } from '../../../../session/conversations';
// tslint:disable-next-line: no-require-imports no-var-requires no-implicit-dependencies // tslint:disable-next-line: no-require-imports no-var-requires no-implicit-dependencies
const chaiAsPromised = require('chai-as-promised'); const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
@ -18,41 +20,41 @@ const { expect } = chai;
// tslint:disable-next-line: max-func-body-length // tslint:disable-next-line: max-func-body-length
describe('ClosedGroupUpdates', () => { describe('ClosedGroupUpdates', () => {
// Initialize new stubbed cache // Initialize new stubbed cache
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
const ourDevice = TestUtils.generateFakePubKey(); const ourDevice = TestUtils.generateFakePubKey();
const ourNumber = ourDevice.key; const ourNumber = ourDevice.key;
const groupId = TestUtils.generateFakePubKey().key; const groupId = TestUtils.generateFakePubKey().key;
const members = TestUtils.generateFakePubKeys(10); const members = TestUtils.generateFakePubKeys(10);
const sender = members[3].key; const sender = members[3].key;
const getConvo = sandbox.stub(ConversationController.getInstance(), 'get'); const getConvo = sandbox.stub(ConversationController.getInstance(), 'get');
beforeEach(async () => { beforeEach(async () => {
// Utils Stubs // Utils Stubs
sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber); sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber);
}); });
afterEach(() => { afterEach(() => {
TestUtils.restoreStubs(); TestUtils.restoreStubs();
sandbox.restore(); sandbox.restore();
});
describe('handleClosedGroupControlMessage', () => {
describe('performIfValid', () => {
it('does not perform if convo does not exist', async () => {
const envelope = generateEnvelopePlusClosedGroup(groupId, sender);
const groupUpdate = generateGroupUpdateNameChange(groupId);
getConvo.returns(undefined as any);
await handleClosedGroupControlMessage(envelope, groupUpdate);
});
}); });
describe('handleClosedGroupControlMessage', () => { // describe('handleClosedGroupNameChanged', () => {
describe('performIfValid', () => { // it('does not trigger an update of the group if the name is the same', async () => {
it('does not perform if convo does not exist', async () => { // const envelope = generateEnvelopePlusClosedGroup(groupId, sender);
const envelope = generateEnvelopePlusClosedGroup(groupId, sender); // const groupUpdate = generateGroupUpdateNameChange(groupId);
const groupUpdate = generateGroupUpdateNameChange(groupId); // await handleClosedGroupControlMessage(envelope, groupUpdate);
getConvo.returns(undefined as any); // });
await handleClosedGroupControlMessage(envelope, groupUpdate); // });
}); });
});
// describe('handleClosedGroupNameChanged', () => {
// it('does not trigger an update of the group if the name is the same', async () => {
// const envelope = generateEnvelopePlusClosedGroup(groupId, sender);
// const groupUpdate = generateGroupUpdateNameChange(groupId);
// await handleClosedGroupControlMessage(envelope, groupUpdate);
// });
// });
});
}); });

@ -4,38 +4,36 @@ import { SignalService } from '../../../protobuf';
import uuid from 'uuid'; import uuid from 'uuid';
import { fromHexToArray } from '../../../session/utils/String'; import { fromHexToArray } from '../../../session/utils/String';
export function generateEnvelopePlusClosedGroup( export function generateEnvelopePlusClosedGroup(
groupId: string, groupId: string,
sender: string sender: string
): EnvelopePlus { ): EnvelopePlus {
const envelope: EnvelopePlus = { const envelope: EnvelopePlus = {
senderIdentity: sender, senderIdentity: sender,
receivedAt: Date.now(), receivedAt: Date.now(),
timestamp: Date.now() - 2000, timestamp: Date.now() - 2000,
id: uuid(), id: uuid(),
type: SignalService.Envelope.Type.CLOSED_GROUP_CIPHERTEXT, type: SignalService.Envelope.Type.CLOSED_GROUP_CIPHERTEXT,
source: groupId, source: groupId,
content: new Uint8Array(), content: new Uint8Array(),
toJSON: () => ['fake'], toJSON: () => ['fake'],
}; };
return envelope; return envelope;
} }
export function generateGroupUpdateNameChange( export function generateGroupUpdateNameChange(
groupId: string groupId: string
): SignalService.DataMessage.ClosedGroupControlMessage { ): SignalService.DataMessage.ClosedGroupControlMessage {
const update: SignalService.DataMessage.ClosedGroupControlMessage = { const update: SignalService.DataMessage.ClosedGroupControlMessage = {
type: SignalService.DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE, type: SignalService.DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE,
toJSON: () => ['fake'], toJSON: () => ['fake'],
publicKey: fromHexToArray(groupId), publicKey: fromHexToArray(groupId),
name: 'fakeNewName', name: 'fakeNewName',
members: [], members: [],
admins: [], admins: [],
wrappers: [], wrappers: [],
}; };
return update; return update;
} }

Loading…
Cancel
Save