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.
372 lines
10 KiB
TypeScript
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);
|
|
}
|
|
}
|