|
|
|
@ -8,12 +8,12 @@ import {
|
|
|
|
|
RatchetState,
|
|
|
|
|
saveSenderKeys,
|
|
|
|
|
saveSenderKeysInner,
|
|
|
|
|
shareSenderKeys,
|
|
|
|
|
} from './senderKeys';
|
|
|
|
|
import { getChainKey } from './ratchet';
|
|
|
|
|
import { MultiDeviceProtocol } from '../protocols';
|
|
|
|
|
import { BufferType } from '../utils/String';
|
|
|
|
|
import { UserUtil } from '../../util';
|
|
|
|
|
import { MediumGroupQuitMessage } from '../messages/outgoing/content/data/mediumgroup/MediumGroupQuitMessage';
|
|
|
|
|
import {
|
|
|
|
|
ClosedGroupChatMessage,
|
|
|
|
|
ClosedGroupMessage,
|
|
|
|
@ -21,7 +21,6 @@ import {
|
|
|
|
|
ExpirationTimerUpdateMessage,
|
|
|
|
|
MediumGroupCreateMessage,
|
|
|
|
|
MediumGroupMessage,
|
|
|
|
|
Message,
|
|
|
|
|
} from '../messages/outgoing';
|
|
|
|
|
import { MessageModel, MessageModelType } from '../../../js/models/messages';
|
|
|
|
|
import { getMessageQueue } from '../../session';
|
|
|
|
@ -35,6 +34,7 @@ export {
|
|
|
|
|
saveSenderKeys,
|
|
|
|
|
saveSenderKeysInner,
|
|
|
|
|
getChainKey,
|
|
|
|
|
shareSenderKeys,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toHex = (d: BufferType) => StringUtils.decode(d, 'hex');
|
|
|
|
@ -118,12 +118,14 @@ export async function createMediumGroup(
|
|
|
|
|
|
|
|
|
|
const dbMessage = await addUpdateMessage(convo, groupDiff, 'outgoing');
|
|
|
|
|
|
|
|
|
|
// be sure to call this before sending the message.
|
|
|
|
|
// the sending pipeline needs to know from GroupUtils when a message is for a medium group
|
|
|
|
|
await updateOrCreateGroup(groupDetails);
|
|
|
|
|
|
|
|
|
|
await sendGroupUpdate(convo, groupDiff, groupDetails, dbMessage.id);
|
|
|
|
|
|
|
|
|
|
// ***** 3. Add update message to the conversation *****
|
|
|
|
|
|
|
|
|
|
await updateOrCreateGroup(groupDetails);
|
|
|
|
|
|
|
|
|
|
convo.updateGroupAdmins(admins);
|
|
|
|
|
|
|
|
|
|
window.owsDesktopApp.appView.openConversation(groupId, {});
|
|
|
|
@ -185,6 +187,7 @@ export async function leaveMediumGroup(groupId: string) {
|
|
|
|
|
|
|
|
|
|
// TODO: need to reset everyone's sender keys
|
|
|
|
|
window.SwarmPolling.removePubkey(groupId);
|
|
|
|
|
// TODO audric: we just left a group, we have to regenerate our senderkey
|
|
|
|
|
|
|
|
|
|
const maybeConvo = await ConversationController.get(groupId);
|
|
|
|
|
|
|
|
|
@ -207,16 +210,23 @@ export async function leaveMediumGroup(groupId: string) {
|
|
|
|
|
sent_at: now,
|
|
|
|
|
received_at: now,
|
|
|
|
|
});
|
|
|
|
|
const ourPrimary = await UserUtil.getPrimary();
|
|
|
|
|
|
|
|
|
|
const updateParams = {
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
identifier: dbMessage.id,
|
|
|
|
|
groupId,
|
|
|
|
|
const members = convo.get('members').filter(m => m !== ourPrimary.key);
|
|
|
|
|
// do not include senderkey as everyone needs to generate new one
|
|
|
|
|
const groupUpdate: GroupInfo = {
|
|
|
|
|
id: convo.get('id'),
|
|
|
|
|
name: convo.get('name'),
|
|
|
|
|
members,
|
|
|
|
|
is_medium_group: true,
|
|
|
|
|
admins: convo.get('groupAdmins'),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const message = new MediumGroupQuitMessage(updateParams);
|
|
|
|
|
|
|
|
|
|
await sendToMembers(groupId, message, dbMessage);
|
|
|
|
|
await sendGroupUpdateForMedium(
|
|
|
|
|
{ leavingMembers: [ourPrimary.key] },
|
|
|
|
|
groupUpdate,
|
|
|
|
|
dbMessage.id
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Just a container to store two named list of keys
|
|
|
|
@ -251,48 +261,22 @@ async function getExistingSenderKeysForGroup(
|
|
|
|
|
return maybeKeys.filter(d => d !== null).map(d => d as RatchetState);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create all sender keys based on the changes in
|
|
|
|
|
// the group's composition
|
|
|
|
|
async function getOrCreateSenderKeysForUpdate(
|
|
|
|
|
groupId: string,
|
|
|
|
|
members: Array<string>,
|
|
|
|
|
changes: MemberChanges
|
|
|
|
|
): Promise<SenderKeysContainer> {
|
|
|
|
|
// 1. Create sender keys for every joining member
|
|
|
|
|
const joining = changes.joiningMembers || [];
|
|
|
|
|
const leaving = changes.leavingMembers || [];
|
|
|
|
|
|
|
|
|
|
let newKeys = await createSenderKeysForMembers(groupId, joining);
|
|
|
|
|
|
|
|
|
|
// 2. Get ratchet states for existing members
|
|
|
|
|
// Get a list of senderKeys we have to send to joining members
|
|
|
|
|
// Basically this is the senderkey of all members who joined concatenated with
|
|
|
|
|
// the one of members currently in the group.
|
|
|
|
|
|
|
|
|
|
const existingMembers = _.difference(members, joining);
|
|
|
|
|
// Also, the list of senderkeys for existing member must be empty if there is any leaving members,
|
|
|
|
|
// as they each member need to regenerate a new senderkey
|
|
|
|
|
async function getOrUpdateSenderKeysForJoiningMembers(
|
|
|
|
|
groupId: string,
|
|
|
|
|
members: Array<string>
|
|
|
|
|
): Promise<Array<RatchetState>> {
|
|
|
|
|
// get all devices for members
|
|
|
|
|
|
|
|
|
|
const allDevices = _.flatten(
|
|
|
|
|
await Promise.all(
|
|
|
|
|
existingMembers.map(m => MultiDeviceProtocol.getAllDevices(m))
|
|
|
|
|
)
|
|
|
|
|
await Promise.all(members.map(m => MultiDeviceProtocol.getAllDevices(m)))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let existingKeys: Array<RatchetState> = [];
|
|
|
|
|
|
|
|
|
|
if (leaving.length > 0) {
|
|
|
|
|
// If we have leaving members, we have to re-generate ratchet
|
|
|
|
|
// keys for existing members
|
|
|
|
|
const otherKeys = await Promise.all(
|
|
|
|
|
allDevices.map(async device => {
|
|
|
|
|
return createSenderKeyForGroup(groupId, PubKey.cast(device));
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
newKeys = _.union(newKeys, otherKeys);
|
|
|
|
|
} else {
|
|
|
|
|
// We can reuse existing keys
|
|
|
|
|
existingKeys = await getExistingSenderKeysForGroup(groupId, allDevices);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { existingKeys, newKeys };
|
|
|
|
|
return getExistingSenderKeysForGroup(groupId, allDevices);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getGroupSecretKey(groupId: string): Promise<Uint8Array> {
|
|
|
|
@ -313,6 +297,9 @@ async function getGroupSecretKey(groupId: string): Promise<Uint8Array> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function syncMediumGroup(group: ConversationModel) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'Medium group syncing must be done once multi device is enabled back'
|
|
|
|
|
);
|
|
|
|
|
const ourPrimary = await UserUtil.getPrimary();
|
|
|
|
|
|
|
|
|
|
const groupId = group.get('id');
|
|
|
|
@ -343,7 +330,6 @@ async function syncMediumGroup(group: ConversationModel) {
|
|
|
|
|
members: group.get('members'),
|
|
|
|
|
is_medium_group: true,
|
|
|
|
|
admins: group.get('groupAdmins'),
|
|
|
|
|
senderKeysContainer,
|
|
|
|
|
secretKey,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@ -409,12 +395,7 @@ export async function initiateGroupUpdate(
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isMediumGroup) {
|
|
|
|
|
// Send sender keys and group secret key
|
|
|
|
|
updateObj.senderKeysContainer = await getOrCreateSenderKeysForUpdate(
|
|
|
|
|
groupId,
|
|
|
|
|
members,
|
|
|
|
|
diff
|
|
|
|
|
);
|
|
|
|
|
// Send group secret key
|
|
|
|
|
const secretKey = await getGroupSecretKey(groupId);
|
|
|
|
|
|
|
|
|
|
updateObj.secretKey = secretKey;
|
|
|
|
@ -522,7 +503,6 @@ interface GroupInfo {
|
|
|
|
|
blocked?: boolean;
|
|
|
|
|
admins?: Array<string>;
|
|
|
|
|
secretKey?: Uint8Array;
|
|
|
|
|
senderKeysContainer?: SenderKeysContainer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface UpdatableGroupState {
|
|
|
|
@ -596,29 +576,28 @@ export function calculateGroupDiff(
|
|
|
|
|
return groupDiff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendGroupUpdateForExistingMembers(
|
|
|
|
|
leavingMembers: Array<string>,
|
|
|
|
|
remainingMembers: Array<string>,
|
|
|
|
|
async function sendGroupUpdateForMedium(
|
|
|
|
|
diff: MemberChanges,
|
|
|
|
|
groupUpdate: GroupInfo,
|
|
|
|
|
messageId?: string
|
|
|
|
|
) {
|
|
|
|
|
const { id: groupId, members, name: groupName } = groupUpdate;
|
|
|
|
|
const ourPrimary = await UserUtil.getPrimary();
|
|
|
|
|
|
|
|
|
|
const leavingMembers = diff.leavingMembers || [];
|
|
|
|
|
const joiningMembers = diff.joiningMembers || [];
|
|
|
|
|
const wasAnyUserRemoved = leavingMembers.length > 0;
|
|
|
|
|
const isUserLeaving = leavingMembers.includes(ourPrimary.key);
|
|
|
|
|
|
|
|
|
|
const membersBin = members.map(
|
|
|
|
|
(pkHex: string) => new Uint8Array(fromHex(pkHex))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const admins = groupUpdate.admins || [];
|
|
|
|
|
|
|
|
|
|
const adminsBin = admins.map(
|
|
|
|
|
(pkHex: string) => new Uint8Array(fromHex(pkHex))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Existing members only receive new sender keys
|
|
|
|
|
const senderKeys = groupUpdate.senderKeysContainer
|
|
|
|
|
? groupUpdate.senderKeysContainer.newKeys
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const params = {
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
identifier: messageId || uuid(),
|
|
|
|
@ -626,106 +605,91 @@ async function sendGroupUpdateForExistingMembers(
|
|
|
|
|
members: membersBin,
|
|
|
|
|
groupName,
|
|
|
|
|
admins: adminsBin,
|
|
|
|
|
senderKeys,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const message = new MediumGroupUpdateMessage(params);
|
|
|
|
|
|
|
|
|
|
remainingMembers.forEach(async member => {
|
|
|
|
|
const memberPubKey = new PubKey(member);
|
|
|
|
|
await getMessageQueue().sendUsingMultiDevice(memberPubKey, message);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Remove sender keys from the params to send to leaving memebers
|
|
|
|
|
params.senderKeys = [];
|
|
|
|
|
const strippedMessage = new MediumGroupUpdateMessage(params);
|
|
|
|
|
leavingMembers.forEach(async member => {
|
|
|
|
|
const memberPubKey = new PubKey(member);
|
|
|
|
|
await getMessageQueue().sendUsingMultiDevice(memberPubKey, strippedMessage);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendGroupUpdateForJoiningMembers(
|
|
|
|
|
recipients: Array<string>,
|
|
|
|
|
groupUpdate: GroupInfo,
|
|
|
|
|
messageId?: string
|
|
|
|
|
) {
|
|
|
|
|
const { id: groupId, name, members } = groupUpdate;
|
|
|
|
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
const { secretKey, senderKeysContainer } = groupUpdate;
|
|
|
|
|
|
|
|
|
|
if (!secretKey) {
|
|
|
|
|
window.log.error('Group secret key not specified, aborting...');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (wasAnyUserRemoved) {
|
|
|
|
|
if (isUserLeaving && leavingMembers.length !== 1) {
|
|
|
|
|
window.log.warn("Can't remove self and others simultaneously.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
|
|
|
|
|
const paramsWithoutSenderKeys = {
|
|
|
|
|
...params,
|
|
|
|
|
senderKeys: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let senderKeys: Array<RatchetState> = [];
|
|
|
|
|
if (!senderKeysContainer) {
|
|
|
|
|
window.log.warn('Sender keys for joining members not found');
|
|
|
|
|
} else {
|
|
|
|
|
// Joining members should receive all known sender keys
|
|
|
|
|
senderKeys = _.union(
|
|
|
|
|
senderKeysContainer.existingKeys,
|
|
|
|
|
senderKeysContainer.newKeys
|
|
|
|
|
const messageStripped = new MediumGroupUpdateMessage(
|
|
|
|
|
paramsWithoutSenderKeys
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const membersBin = members.map(
|
|
|
|
|
(pkHex: string) => new Uint8Array(fromHex(pkHex))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const admins = groupUpdate.admins || [];
|
|
|
|
|
|
|
|
|
|
const adminsBin = admins.map(
|
|
|
|
|
(pkHex: string) => new Uint8Array(fromHex(pkHex))
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const createParams = {
|
|
|
|
|
timestamp: now,
|
|
|
|
|
groupId,
|
|
|
|
|
identifier: messageId || uuid(),
|
|
|
|
|
groupSecretKey: secretKey,
|
|
|
|
|
members: membersBin,
|
|
|
|
|
groupName: name,
|
|
|
|
|
admins: adminsBin,
|
|
|
|
|
senderKeys,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mediumGroupCreateMessage = new MediumGroupCreateMessage(createParams);
|
|
|
|
|
|
|
|
|
|
recipients.forEach(async member => {
|
|
|
|
|
const memberPubKey = new PubKey(member);
|
|
|
|
|
await getMessageQueue().sendUsingMultiDevice(
|
|
|
|
|
memberPubKey,
|
|
|
|
|
mediumGroupCreateMessage
|
|
|
|
|
window.log.warn('Sending to groupUpdateMessage without senderKeys');
|
|
|
|
|
await getMessageQueue().sendToGroup(messageStripped);
|
|
|
|
|
} else {
|
|
|
|
|
let senderKeys: Array<RatchetState>;
|
|
|
|
|
if (joiningMembers.length > 0) {
|
|
|
|
|
// Generate ratchets for any new members
|
|
|
|
|
senderKeys = await createSenderKeysForMembers(groupId, joiningMembers);
|
|
|
|
|
} else {
|
|
|
|
|
// It's not a member change, maybe an name change. So just reuse all senderkeys
|
|
|
|
|
senderKeys = await getOrUpdateSenderKeysForJoiningMembers(
|
|
|
|
|
groupId,
|
|
|
|
|
members
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const paramsWithSenderKeys = {
|
|
|
|
|
...params,
|
|
|
|
|
senderKeys,
|
|
|
|
|
};
|
|
|
|
|
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
|
|
|
|
|
const message = new MediumGroupUpdateMessage(paramsWithSenderKeys);
|
|
|
|
|
window.log.warn(
|
|
|
|
|
'Sending to groupUpdateMessage with joining members senderKeys to groupAddress',
|
|
|
|
|
senderKeys
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendGroupUpdateForMedium(
|
|
|
|
|
diff: MemberChanges,
|
|
|
|
|
groupUpdate: GroupInfo,
|
|
|
|
|
messageId?: string
|
|
|
|
|
) {
|
|
|
|
|
const joining = diff.joiningMembers || [];
|
|
|
|
|
const leaving = diff.leavingMembers || [];
|
|
|
|
|
await getMessageQueue().sendToGroup(message);
|
|
|
|
|
|
|
|
|
|
// 1. create group for all joining members (send timeout timer if necessary)
|
|
|
|
|
if (joining.length) {
|
|
|
|
|
await sendGroupUpdateForJoiningMembers(joining, groupUpdate, messageId);
|
|
|
|
|
}
|
|
|
|
|
// now send a CREATE group message with all senderkeys no matter what to all joining members, using established channels
|
|
|
|
|
if (joiningMembers.length) {
|
|
|
|
|
const { secretKey } = groupUpdate;
|
|
|
|
|
|
|
|
|
|
// 2. send group update to all other members
|
|
|
|
|
const others = _.difference(groupUpdate.members, joining);
|
|
|
|
|
if (others.length) {
|
|
|
|
|
await sendGroupUpdateForExistingMembers(
|
|
|
|
|
leaving,
|
|
|
|
|
others,
|
|
|
|
|
groupUpdate,
|
|
|
|
|
messageId
|
|
|
|
|
);
|
|
|
|
|
if (!secretKey) {
|
|
|
|
|
window.log.error('Group secret key not specified, aborting...');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const allSenderKeys = await getOrUpdateSenderKeysForJoiningMembers(
|
|
|
|
|
groupId,
|
|
|
|
|
members
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const createParams = {
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
identifier: messageId || uuid(),
|
|
|
|
|
groupSecretKey: secretKey,
|
|
|
|
|
groupId,
|
|
|
|
|
members: membersBin,
|
|
|
|
|
groupName,
|
|
|
|
|
admins: adminsBin,
|
|
|
|
|
senderKeys: allSenderKeys,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mediumGroupCreateMessage = new MediumGroupCreateMessage(
|
|
|
|
|
createParams
|
|
|
|
|
);
|
|
|
|
|
// console.warn(
|
|
|
|
|
// 'sending group create to',
|
|
|
|
|
// joiningMembers,
|
|
|
|
|
// ' obj: ',
|
|
|
|
|
// mediumGroupCreateMessage
|
|
|
|
|
// );
|
|
|
|
|
|
|
|
|
|
joiningMembers.forEach(async member => {
|
|
|
|
|
const memberPubKey = new PubKey(member);
|
|
|
|
|
await getMessageQueue().sendUsingMultiDevice(
|
|
|
|
|
memberPubKey,
|
|
|
|
|
mediumGroupCreateMessage
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|