finish explicit group updates

This commit is contained in:
Audric Ackermann 2021-02-01 11:35:06 +11:00
parent 0e25ab2874
commit 334da0a169
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
8 changed files with 202 additions and 90 deletions

View file

@ -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) {

View file

@ -22,6 +22,7 @@ import { ECKeyPair } from './keypairs';
import { getOurNumber } from '../session/utils/User';
import { UserUtils } from '../session/utils';
import { ConversationModel } from '../../js/models/conversations';
import _ from 'lodash';
export async function handleClosedGroupControlMessage(
envelope: EnvelopePlus,
@ -40,7 +41,13 @@ export async function handleClosedGroupControlMessage(
await handleClosedGroupEncryptionKeyPair(envelope, groupUpdate);
} else if (type === Type.NEW) {
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);
} else {
window.log.error('Unknown group update type: ', type);
@ -230,7 +237,6 @@ async function handleUpdateClosedGroup(
const curAdmins = convo.get('groupAdmins');
// NOTE: admins cannot change with closed groups
const members = membersBinary.map(toHex);
const diff = ClosedGroup.buildGroupDiff(convo, { name, members });
@ -291,6 +297,7 @@ async function handleUpdateClosedGroup(
convo.set('members', members);
await convo.commit();
convo.updateLastMessage();
await removeFromCache(envelope);
}
@ -310,6 +317,9 @@ async function handleClosedGroupEncryptionKeyPair(
) {
return;
}
window.log.info(
`Got a group update for group ${envelope.source}, type: ENCRYPTION_KEY_PAIR`
);
const ourNumber = await UserUtils.getOurNumber();
const groupPublicKey = envelope.source;
const ourKeyPair = await UserUtils.getIdentityKeyPair();
@ -410,9 +420,10 @@ async function handleClosedGroupEncryptionKeyPair(
await removeFromCache(envelope);
}
async function performIfValid(envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage) {
async function performIfValid(
envelope: EnvelopePlus,
groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage
) {
const { Type } = SignalService.DataMessage.ClosedGroupControlMessage;
const groupPublicKey = envelope.source;
@ -470,7 +481,6 @@ async function performIfValid(envelope: EnvelopePlus,
await handleClosedGroupMemberLeft(envelope, groupUpdate, convo);
}
return true;
}
@ -481,18 +491,20 @@ async function handleClosedGroupNameChanged(
) {
// Only add update message if we have something to show
const newName = groupUpdate.name;
if (
newName !== convo.get(('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();
}
await removeFromCache(envelope);
}
@ -504,11 +516,18 @@ async function handleClosedGroupMembersAdded(
const { members: addedMembersBinary } = groupUpdate;
const addedMembers = (addedMembersBinary || []).map(toHex);
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);
window.log.info(
`Got a group update for group ${envelope.source}, type: MEMBERS_ADDED`
);
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);
return;
}
@ -522,6 +541,7 @@ async function handleClosedGroupMembersAdded(
await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming');
convo.set({ members });
convo.updateLastMessage();
await convo.commit();
await removeFromCache(envelope);
}
@ -529,8 +549,77 @@ async function handleClosedGroupMembersAdded(
async function handleClosedGroupMembersRemoved(
envelope: EnvelopePlus,
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(
@ -545,7 +634,7 @@ async function handleClosedGroupMemberLeft(
// otherwise, we remove the sender from the list of current members in this group
const oldMembers = convo.get('members') || [];
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
const ourPubkey = await UserUtils.getCurrentDevicePubKey();
if (!ourPubkey) {
@ -558,9 +647,13 @@ async function handleClosedGroupMemberLeft(
}
// 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) {
await ClosedGroup.generateAndSendNewEncryptionKeyPair(groupPublicKey, members);
await ClosedGroup.generateAndSendNewEncryptionKeyPair(
groupPublicKey,
members
);
}
if (didAdminLeave) {
@ -571,16 +664,14 @@ 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
) {
if (leftMemberWasPresent) {
const groupDiff: ClosedGroup.GroupDiff = {
leavingMembers: didAdminLeave ? oldMembers : [sender],
};
await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming');
convo.updateLastMessage();
}
convo.set('members', members);
@ -590,7 +681,6 @@ async function handleClosedGroupMemberLeft(
await removeFromCache(envelope);
}
export async function createClosedGroup(
groupName: 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
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 => {

View file

@ -75,6 +75,9 @@ async function decryptForClosedGroup(
encryptionKeyPair,
true
);
if (decryptedContent?.byteLength) {
break;
}
keyIndex++;
} catch (e) {
window.log.info(
@ -83,13 +86,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 = await UserUtils.getCurrentDevicePubKey();
if (

View file

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

View file

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

View file

@ -340,13 +340,13 @@ export async function leaveClosedGroup(groupId: string) {
received_at: now,
});
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(

View file

@ -5,11 +5,13 @@ import { describe } from 'mocha';
import { GroupUtils, PromiseUtils, UserUtils } from '../../../../session/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 { ConversationController } from '../../../../session/conversations';
// tslint:disable-next-line: no-require-imports no-var-requires no-implicit-dependencies
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
@ -18,41 +20,41 @@ const { expect } = chai;
// tslint:disable-next-line: max-func-body-length
describe('ClosedGroupUpdates', () => {
// Initialize new stubbed cache
const sandbox = sinon.createSandbox();
const ourDevice = TestUtils.generateFakePubKey();
const ourNumber = ourDevice.key;
const groupId = TestUtils.generateFakePubKey().key;
const members = TestUtils.generateFakePubKeys(10);
const sender = members[3].key;
const getConvo = sandbox.stub(ConversationController.getInstance(), 'get');
// Initialize new stubbed cache
const sandbox = sinon.createSandbox();
const ourDevice = TestUtils.generateFakePubKey();
const ourNumber = ourDevice.key;
const groupId = TestUtils.generateFakePubKey().key;
const members = TestUtils.generateFakePubKeys(10);
const sender = members[3].key;
const getConvo = sandbox.stub(ConversationController.getInstance(), 'get');
beforeEach(async () => {
// Utils Stubs
sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber);
beforeEach(async () => {
// Utils Stubs
sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber);
});
afterEach(() => {
TestUtils.restoreStubs();
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);
});
});
afterEach(() => {
TestUtils.restoreStubs();
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('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);
// });
// });
});
// 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);
// });
// });
});
});

View file

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