You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/receiver/mediumGroups.ts

372 lines
10 KiB
TypeScript

import { SignalService } from '../protobuf';
import { removeFromCache } from './cache';
import { EnvelopePlus } from './types';
import { MediumGroupResponseKeysMessage } from '../session/messages/outgoing';
import { getMessageQueue } from '../session';
import { PubKey } from '../session/types';
import _ from 'lodash';
import * as SenderKeyAPI from '../session/medium_group';
import { getChainKey } from '../session/medium_group/ratchet';
import { fromHex, toHex } from '../session/utils/String';
import { UserUtil } from '../util';
import {
createSenderKeyForGroup,
shareSenderKeys,
} from '../session/medium_group/senderKeys';
async function handleSenderKeyRequest(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
) {
const { textsecure, log } = window;
const senderIdentity = envelope.source;
const ourIdentity = await textsecure.storage.user.getNumber();
const { groupPublicKey } = groupUpdate;
const groupId = toHex(groupPublicKey);
log.debug('[sender key] sender key request from:', senderIdentity);
let maybeKey = await getChainKey(groupId, ourIdentity);
if (!maybeKey) {
log.error('Could not find own sender key. Generating new one.');
maybeKey = await SenderKeyAPI.createSenderKeyForGroup(
groupId,
PubKey.cast(ourIdentity)
);
if (!maybeKey) {
log.error('Could not find own sender key after regenerate');
await removeFromCache(envelope);
return;
}
}
// We reuse the same message type for sender keys
const { chainKey, keyIdx } = maybeKey;
// NOTE: we can't use `shareSenderKeys` here because
// we are not using multidevice
const responseParams = {
timestamp: Date.now(),
groupId,
senderKey: {
chainKey: new Uint8Array(chainKey),
keyIdx,
pubKey: new Uint8Array(fromHex(ourIdentity)),
},
};
const keysResponseMessage = new MediumGroupResponseKeysMessage(
responseParams
);
const senderPubKey = new PubKey(senderIdentity);
await getMessageQueue().send(senderPubKey, keysResponseMessage);
await removeFromCache(envelope);
}
async function handleSenderKey(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
) {
const { log } = window;
const { groupPublicKey, senderKeys } = groupUpdate;
const senderIdentity = envelope.source;
const groupId = toHex(groupPublicKey);
log.debug(
'[sender key] got a new sender key from:',
senderIdentity,
'group:',
groupId
);
await saveIncomingRatchetKeys(groupId, senderKeys);
await removeFromCache(envelope);
}
async function saveIncomingRatchetKeys(
groupId: string,
ratchetKeys: Array<SignalService.MediumGroupUpdate.ISenderKey>
) {
await Promise.all(
ratchetKeys.map(async senderKey => {
// Note that keyIndex is a number and 0 is considered a valid value:
if (
senderKey.chainKey &&
senderKey.keyIndex !== undefined &&
senderKey.publicKey
) {
const pubKey = toHex(senderKey.publicKey);
const chainKey = toHex(senderKey.chainKey);
const keyIndex = senderKey.keyIndex as number;
window.log.info('Saving sender keys for:', pubKey);
// TODO: check that we are not overriting sender keys when
// we are not expected to do so
await SenderKeyAPI.saveSenderKeys(
groupId,
PubKey.cast(pubKey),
chainKey,
keyIndex
);
} else {
window.log.error('Received invalid sender key');
}
})
);
}
async function checkOwnSenderKeyPresent(
senderKeys: Array<SignalService.MediumGroupUpdate.ISenderKey>
) {
const ownKey = (await UserUtil.getCurrentDevicePubKey()) as string;
const pubkeys = senderKeys
.filter(key => key.publicKey && key.keyIndex !== undefined && key.chainKey)
.map(key => toHex(key.publicKey as Uint8Array));
if (pubkeys.indexOf(ownKey) === -1) {
window.log.error(
'Could not find sender key inside medium group invitation!'
);
// TODO: we should probably create the key ourselves in this case;
}
}
async function handleNewGroup(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
) {
const { log } = window;
const {
name,
groupPublicKey,
groupPrivateKey,
members: membersBinary,
admins: adminsBinary,
senderKeys,
} = groupUpdate;
const groupId = toHex(groupPublicKey);
const members = membersBinary.map(toHex);
const admins = adminsBinary.map(toHex);
const maybeConvo = window.ConversationController.get(groupId);
const groupExists = !!maybeConvo;
if (groupExists) {
if (maybeConvo && maybeConvo.get('isKickedFromGroup')) {
// TODO: indicate that we've been re-invited
// to the group if that is the case
// Enable typing:
maybeConvo.set('isKickedFromGroup', false);
maybeConvo.set('left', false);
maybeConvo.updateTextInputState();
} else {
log.warn(
'Ignoring a medium group message of type NEW: the conversation already exists'
);
await removeFromCache(envelope);
return;
}
}
const convo =
maybeConvo ||
(await window.ConversationController.getOrCreateAndWait(groupId, 'group'));
await SenderKeyAPI.addUpdateMessage(convo, { newName: name }, 'incoming');
// ***** Creating a new group *****
log.info('Received a new medium group:', groupId);
// TODO: Check that we are even a part of this group
convo.set('name', name);
convo.set('members', members);
convo.set('is_medium_group', true);
convo.set('active_at', Date.now());
// We only set group admins on group creation
convo.set('groupAdmins', admins);
await convo.commit();
const secretKeyHex = toHex(groupPrivateKey);
await window.Signal.Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: secretKeyHex,
});
// Sanity check: we expect to find our own sender key in the list
await checkOwnSenderKeyPresent(senderKeys);
await saveIncomingRatchetKeys(groupId, senderKeys);
window.SwarmPolling.addGroupId(PubKey.cast(groupId));
await removeFromCache(envelope);
}
function sanityCheckMediumGroupUpdate(
primary: PubKey,
diff: SenderKeyAPI.MemberChanges,
groupUpdate: SignalService.MediumGroupUpdate
) {
const joining = diff.joiningMembers || [];
const leaving = diff.leavingMembers || [];
// 1. When there are no member changes, we expect all sender keys
if (!joining.length && !leaving.length) {
if (groupUpdate.senderKeys.length !== groupUpdate.members.length) {
window.log.error('Incorrect number of sender keys in group update');
}
}
// 2. With leaving members, we expect keys for all members
// (ignoring multidevice for now)
if (leaving.length) {
const stillMember = leaving.indexOf(primary.key) === -1;
if (!stillMember) {
// Should not receive any sender keys
if (groupUpdate.senderKeys.length) {
window.log.error('Unexpected sender keys for a leaving member');
}
} else if (groupUpdate.senderKeys.length < groupUpdate.members.length) {
window.log.error('Too few sender keys in group update');
}
}
}
async function handleMediumGroupChange(
envelope: EnvelopePlus,
groupUpdate: SignalService.MediumGroupUpdate
) {
const {
name,
groupPublicKey,
members: membersBinary,
senderKeys,
} = groupUpdate;
const { log } = window;
const groupId = toHex(groupPublicKey);
const convo = window.ConversationController.get(groupId);
if (!convo) {
log.warn(
'Ignoring a medium group update message (INFO) for a non-existing group'
);
await removeFromCache(envelope);
// TODO: In practice we probably need to be able to request the group's
// the NEW message if we somehow missed the initial group invitation
return;
}
// ***** Updating the group *****
const curAdmins = convo.get('groupAdmins') || [];
if (!curAdmins.length) {
log.error('Error: medium group must have at least one admin');
await removeFromCache(envelope);
return;
}
// Check that the sender is admin (make sure it words with multidevice)
const isAdmin = true;
if (!isAdmin) {
log.warn('Rejected attempt to update a group by non-admin');
await removeFromCache(envelope);
return;
}
// NOTE: right now, we don't expect admins to change
// const admins = adminsBinary.map(toHex);
const members = membersBinary.map(toHex);
const diff = SenderKeyAPI.calculateGroupDiff(convo, { name, members });
// Check whether we are still in the group
const primary = await UserUtil.getPrimary();
sanityCheckMediumGroupUpdate(primary, diff, groupUpdate);
// console.log(`Got group update`, groupUpdate);
await saveIncomingRatchetKeys(groupId, senderKeys);
// Only add update message if we have something to show
if (diff.joiningMembers || diff.leavingMembers || diff.newName) {
await SenderKeyAPI.addUpdateMessage(convo, diff, 'incoming');
}
convo.set('name', name);
convo.set('members', members);
const areWeKicked = members.indexOf(primary.key) === -1;
if (areWeKicked) {
convo.set('isKickedFromGroup', true);
// Disable typing:
convo.updateTextInputState();
window.SwarmPolling.removePubkey(groupId);
} else {
if (convo.get('isKickedFromGroup')) {
// Enable typing:
convo.set('isKickedFromGroup', false);
convo.set('left', false);
// Subscribe to this group id
window.SwarmPolling.addGroupId(new PubKey(groupId));
convo.updateTextInputState();
}
}
await convo.commit();
if (diff.leavingMembers && diff.leavingMembers.length > 0 && !areWeKicked) {
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
const userSenderKey = await createSenderKeyForGroup(groupId, primary);
window.log.warn(
'Sharing our new senderKey with remainingMembers via message',
members
);
await shareSenderKeys(groupId, members, userSenderKey);
}
await removeFromCache(envelope);
}
export async function handleMediumGroupUpdate(
envelope: EnvelopePlus,
groupUpdate: any
) {
const { type } = groupUpdate;
const { Type } = SignalService.MediumGroupUpdate;
if (type === Type.SENDER_KEY_REQUEST) {
await handleSenderKeyRequest(envelope, groupUpdate);
} else if (type === Type.SENDER_KEY) {
await handleSenderKey(envelope, groupUpdate);
} else if (type === Type.NEW) {
await handleNewGroup(envelope, groupUpdate);
} else if (type === Type.INFO) {
await handleMediumGroupChange(envelope, groupUpdate);
} else {
window.log.error('Unknown group update type: ', type);
}
}