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.
		
		
		
		
		
			
		
			
				
	
	
		
			925 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			925 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			TypeScript
		
	
import { Data } from '../../ts/data/data';
 | 
						|
import { SignalService } from '../protobuf';
 | 
						|
import { getMessageQueue } from '../session';
 | 
						|
import { getConversationController } from '../session/conversations';
 | 
						|
import * as ClosedGroup from '../session/group/closed-group';
 | 
						|
import { PubKey } from '../session/types';
 | 
						|
import { toHex } from '../session/utils/String';
 | 
						|
import { BlockedNumberController } from '../util';
 | 
						|
import { removeFromCache } from './cache';
 | 
						|
import { decryptWithSessionProtocol } from './contentMessage';
 | 
						|
import { EnvelopePlus } from './types';
 | 
						|
 | 
						|
import _, { isNumber, toNumber } from 'lodash';
 | 
						|
import { ConversationModel } from '../models/conversation';
 | 
						|
import { ConversationTypeEnum } from '../models/conversationAttributes';
 | 
						|
import { getSwarmPollingInstance } from '../session/apis/snode_api';
 | 
						|
import { SnodeNamespaces } from '../session/apis/snode_api/namespaces';
 | 
						|
import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage';
 | 
						|
import { UserUtils } from '../session/utils';
 | 
						|
import { perfEnd, perfStart } from '../session/utils/Performance';
 | 
						|
import { ReleasedFeatures } from '../util/releaseFeature';
 | 
						|
import { Storage } from '../util/storage';
 | 
						|
import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions';
 | 
						|
import { getSettingsKeyFromLibsessionWrapper } from './configMessage';
 | 
						|
import { ECKeyPair, HexKeyPair } from './keypairs';
 | 
						|
import { queueAllCachedFromSource } from './receiver';
 | 
						|
 | 
						|
export const distributingClosedGroupEncryptionKeyPairs = new Map<string, ECKeyPair>();
 | 
						|
 | 
						|
// this is a cache of the keypairs stored in the db.
 | 
						|
const cacheOfClosedGroupKeyPairs: Map<string, Array<HexKeyPair>> = new Map();
 | 
						|
 | 
						|
export async function getAllCachedECKeyPair(groupPubKey: string) {
 | 
						|
  let keyPairsFound = cacheOfClosedGroupKeyPairs.get(groupPubKey);
 | 
						|
 | 
						|
  if (!keyPairsFound || keyPairsFound.length === 0) {
 | 
						|
    keyPairsFound = (await Data.getAllEncryptionKeyPairsForGroup(groupPubKey)) || [];
 | 
						|
    cacheOfClosedGroupKeyPairs.set(groupPubKey, keyPairsFound);
 | 
						|
  }
 | 
						|
 | 
						|
  return keyPairsFound.slice();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 *
 | 
						|
 * @returns true if this keypair was not already saved for this publickey
 | 
						|
 */
 | 
						|
export async function addKeyPairToCacheAndDBIfNeeded(
 | 
						|
  groupPubKey: string,
 | 
						|
  keyPair: HexKeyPair
 | 
						|
): Promise<boolean> {
 | 
						|
  const existingKeyPairs = await getAllCachedECKeyPair(groupPubKey);
 | 
						|
 | 
						|
  const alreadySaved = existingKeyPairs.some(k => {
 | 
						|
    return k.privateHex === keyPair.privateHex && k.publicHex === keyPair.publicHex;
 | 
						|
  });
 | 
						|
 | 
						|
  if (alreadySaved) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  await Data.addClosedGroupEncryptionKeyPair(groupPubKey, keyPair);
 | 
						|
 | 
						|
  if (!cacheOfClosedGroupKeyPairs.has(groupPubKey)) {
 | 
						|
    cacheOfClosedGroupKeyPairs.set(groupPubKey, []);
 | 
						|
  }
 | 
						|
  cacheOfClosedGroupKeyPairs.get(groupPubKey)?.push(keyPair);
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 | 
						|
export async function removeAllClosedGroupEncryptionKeyPairs(groupPubKey: string) {
 | 
						|
  cacheOfClosedGroupKeyPairs.set(groupPubKey, []);
 | 
						|
  await Data.removeAllClosedGroupEncryptionKeyPairs(groupPubKey);
 | 
						|
}
 | 
						|
 | 
						|
export async function handleClosedGroupControlMessage(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage
 | 
						|
) {
 | 
						|
  const { type } = groupUpdate;
 | 
						|
  const { Type } = SignalService.DataMessage.ClosedGroupControlMessage;
 | 
						|
  window?.log?.info(
 | 
						|
    ` handle closed group update from ${envelope.senderIdentity || envelope.source} about group ${
 | 
						|
      envelope.source
 | 
						|
    }`
 | 
						|
  );
 | 
						|
 | 
						|
  if (PubKey.isClosedGroupV3(envelope.source)) {
 | 
						|
    window?.log?.warn(
 | 
						|
      'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage '
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (BlockedNumberController.isBlocked(PubKey.cast(envelope.source))) {
 | 
						|
    window?.log?.warn('Message ignored; destined for blocked group');
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // We drop New closed group message from our other devices, as they will come as ConfigurationMessage instead
 | 
						|
  if (type === Type.ENCRYPTION_KEY_PAIR) {
 | 
						|
    const isComingFromGroupPubkey =
 | 
						|
      envelope.type === SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE;
 | 
						|
    await handleClosedGroupEncryptionKeyPair(envelope, groupUpdate, isComingFromGroupPubkey);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  if (type === Type.NEW) {
 | 
						|
    if (
 | 
						|
      !getConversationController()
 | 
						|
        .get(envelope.senderIdentity || envelope.source)
 | 
						|
        ?.isApproved()
 | 
						|
    ) {
 | 
						|
      window?.log?.info(
 | 
						|
        'Received new closed group message from an unapproved sender -- dropping message.'
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    await handleNewClosedGroup(envelope, groupUpdate, false);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (
 | 
						|
    type === Type.NAME_CHANGE ||
 | 
						|
    type === Type.MEMBERS_REMOVED ||
 | 
						|
    type === Type.MEMBERS_ADDED ||
 | 
						|
    type === Type.MEMBER_LEFT ||
 | 
						|
    type === Type.ENCRYPTION_KEY_PAIR_REQUEST
 | 
						|
  ) {
 | 
						|
    await performIfValid(envelope, groupUpdate);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  window?.log?.error('Unknown group update type: ', type);
 | 
						|
  await removeFromCache(envelope);
 | 
						|
}
 | 
						|
 | 
						|
function sanityCheckNewGroup(
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage
 | 
						|
): boolean {
 | 
						|
  // for a new group message, we need everything to be set
 | 
						|
  const { name, publicKey, members, admins, encryptionKeyPair } = groupUpdate;
 | 
						|
 | 
						|
  if (!name?.length) {
 | 
						|
    window?.log?.warn('groupUpdate: name is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!name?.length) {
 | 
						|
    window?.log?.warn('groupUpdate: name is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!publicKey?.length) {
 | 
						|
    window?.log?.warn('groupUpdate: publicKey is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  const hexGroupPublicKey = toHex(publicKey);
 | 
						|
  if (!PubKey.from(hexGroupPublicKey)) {
 | 
						|
    window?.log?.warn(
 | 
						|
      'groupUpdate: publicKey is not recognized as a valid pubkey',
 | 
						|
      hexGroupPublicKey
 | 
						|
    );
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (PubKey.isClosedGroupV3(hexGroupPublicKey)) {
 | 
						|
    window?.log?.warn('sanityCheckNewGroup: got a v3 new group as a ClosedGroupControlMessage. ');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!members?.length) {
 | 
						|
    window?.log?.warn('groupUpdate: members is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (members.some(m => m.length === 0)) {
 | 
						|
    window?.log?.warn('groupUpdate: one of the member pubkey is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!admins?.length) {
 | 
						|
    window?.log?.warn('groupUpdate: admins is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (admins.some(a => a.length === 0)) {
 | 
						|
    window?.log?.warn('groupUpdate: one of the admins pubkey is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!encryptionKeyPair?.publicKey?.length) {
 | 
						|
    window?.log?.warn('groupUpdate: keypair publicKey is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!encryptionKeyPair?.privateKey?.length) {
 | 
						|
    window?.log?.warn('groupUpdate: keypair privateKey is empty');
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * If we merged a more recent wrapper, we must not apply the changes from some incoming messages as it would override a change already set in the wrapper.
 | 
						|
 *
 | 
						|
 * This is mostly to take care of the link a device logic, where we apply the changes from a wrapper, and then start polling from our swarm namespace 0.
 | 
						|
 * Some messages on our swarm might unhide a contact which was marked hidden after that message was already received on another device. Same for groups left/joined etc.
 | 
						|
 *
 | 
						|
 * @returns true if the user config release is live AND the latest processed corresponding wrapper is supposed to have already included the changes this message did.
 | 
						|
 * So if that message should not make any changes to the ata tracked in the wrappers (just add messages if needed, but don't set members, unhide contact etc).
 | 
						|
 */
 | 
						|
export async function sentAtMoreRecentThanWrapper(
 | 
						|
  envelopeSentAtMs: number,
 | 
						|
  variant: ConfigWrapperObjectTypes
 | 
						|
): Promise<'unknown' | 'wrapper_more_recent' | 'envelope_more_recent'> {
 | 
						|
  const userConfigReleased = await ReleasedFeatures.checkIsUserConfigFeatureReleased();
 | 
						|
  if (!userConfigReleased) {
 | 
						|
    return 'unknown';
 | 
						|
  }
 | 
						|
 | 
						|
  const settingsKey = getSettingsKeyFromLibsessionWrapper(variant);
 | 
						|
  if (!settingsKey) {
 | 
						|
    return 'unknown';
 | 
						|
  }
 | 
						|
  const latestProcessedEnvelope = Storage.get(settingsKey);
 | 
						|
  if (!isNumber(latestProcessedEnvelope) || !latestProcessedEnvelope) {
 | 
						|
    // We want to process the message if we do not have valid data in the db.
 | 
						|
    // Also, we DO want to process a message if we DO NOT have a latest processed timestamp for that wrapper yet
 | 
						|
    return 'envelope_more_recent';
 | 
						|
  }
 | 
						|
 | 
						|
  // this must return true if the message we are considering should have already been handled based on our `latestProcessedEnvelope`.
 | 
						|
  // so if that message was sent before `latestProcessedEnvelope - 2 mins`, we must return true;
 | 
						|
  const latestProcessedEnvelopeLess2Mins = latestProcessedEnvelope - 2 * 60 * 1000;
 | 
						|
 | 
						|
  return envelopeSentAtMs > latestProcessedEnvelopeLess2Mins
 | 
						|
    ? 'envelope_more_recent'
 | 
						|
    : 'wrapper_more_recent';
 | 
						|
}
 | 
						|
 | 
						|
export async function handleNewClosedGroup(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
 | 
						|
  fromLegacyConfig: boolean
 | 
						|
) {
 | 
						|
  if (groupUpdate.type !== SignalService.DataMessage.ClosedGroupControlMessage.Type.NEW) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  if (!sanityCheckNewGroup(groupUpdate)) {
 | 
						|
    window?.log?.warn('Sanity check for newGroup failed, dropping the message...');
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  const ourNumber = UserUtils.getOurPubKeyFromCache();
 | 
						|
 | 
						|
  if (envelope.senderIdentity === ourNumber.key) {
 | 
						|
    window?.log?.warn('Dropping new closed group updatemessage from our other device.');
 | 
						|
    return removeFromCache(envelope);
 | 
						|
  }
 | 
						|
 | 
						|
  const {
 | 
						|
    name,
 | 
						|
    publicKey,
 | 
						|
    members: membersAsData,
 | 
						|
    admins: adminsAsData,
 | 
						|
    encryptionKeyPair,
 | 
						|
  } = groupUpdate;
 | 
						|
 | 
						|
  const groupId = toHex(publicKey);
 | 
						|
  const members = membersAsData.map(toHex);
 | 
						|
  const admins = adminsAsData.map(toHex);
 | 
						|
  const envelopeTimestamp = toNumber(envelope.timestamp);
 | 
						|
  // a type new is sent and received on one to one so do not use envelope.senderIdentity here
 | 
						|
  const sender = envelope.source;
 | 
						|
 | 
						|
  if (
 | 
						|
    !fromLegacyConfig &&
 | 
						|
    (await sentAtMoreRecentThanWrapper(envelopeTimestamp, 'UserGroupsConfig')) ===
 | 
						|
      'wrapper_more_recent'
 | 
						|
  ) {
 | 
						|
    // not from legacy config, so this is a new closed group deposited on our swarm by a user.
 | 
						|
    // we do not want to process it if our wrapper is more recent that that invite to group envelope.
 | 
						|
    window.log.info('dropping invite to legacy group because our wrapper is more recent');
 | 
						|
    return removeFromCache(envelope);
 | 
						|
  }
 | 
						|
 | 
						|
  if (!members.includes(ourNumber.key)) {
 | 
						|
    window?.log?.info(
 | 
						|
      'Got a new group message but apparently we are not a member of it. Dropping it.'
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  const groupConvo = getConversationController().get(groupId);
 | 
						|
  const expireTimer = groupUpdate.expireTimer;
 | 
						|
 | 
						|
  if (groupConvo) {
 | 
						|
    // if we did not left this group, just add the keypair we got if not already there
 | 
						|
    if (!groupConvo.get('isKickedFromGroup') && !groupConvo.get('left')) {
 | 
						|
      const ecKeyPairAlreadyExistingConvo = new ECKeyPair(
 | 
						|
        // tslint:disable: no-non-null-assertion
 | 
						|
        encryptionKeyPair!.publicKey,
 | 
						|
        encryptionKeyPair!.privateKey
 | 
						|
      );
 | 
						|
      const isKeyPairAlreadyHere = await addKeyPairToCacheAndDBIfNeeded(
 | 
						|
        groupId,
 | 
						|
        ecKeyPairAlreadyExistingConvo.toHexKeyPair()
 | 
						|
      );
 | 
						|
 | 
						|
      await groupConvo.updateExpireTimer(expireTimer, sender, Date.now());
 | 
						|
 | 
						|
      if (isKeyPairAlreadyHere) {
 | 
						|
        window.log.info('Dropping already saved keypair for group', groupId);
 | 
						|
        await removeFromCache(envelope);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      window.log.info(`Received the encryptionKeyPair for new group ${groupId}`);
 | 
						|
      await removeFromCache(envelope);
 | 
						|
      window.log.warn(
 | 
						|
        'Closed group message of type NEW: the conversation already exists, but we saved the new encryption keypair'
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // convo exists and we left or got kicked, enable typing and continue processing
 | 
						|
    // Enable typing:
 | 
						|
    groupConvo.set({
 | 
						|
      left: false,
 | 
						|
      isKickedFromGroup: false,
 | 
						|
      lastJoinedTimestamp: toNumber(envelope.timestamp),
 | 
						|
      // we just got readded. Consider the zombie list to have been cleared
 | 
						|
 | 
						|
      zombies: [],
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  const convo =
 | 
						|
    groupConvo ||
 | 
						|
    (await getConversationController().getOrCreateAndWait(groupId, ConversationTypeEnum.GROUP));
 | 
						|
  // ***** Creating a new group *****
 | 
						|
  window?.log?.info('Received a new ClosedGroup of id:', groupId);
 | 
						|
 | 
						|
  // we don't want the initial "AAA,BBB and You joined the group"
 | 
						|
 | 
						|
  // We only set group admins on group creation
 | 
						|
  const groupDetails: ClosedGroup.GroupInfo = {
 | 
						|
    id: groupId,
 | 
						|
    name: name,
 | 
						|
    members: members,
 | 
						|
    admins,
 | 
						|
    activeAt: envelopeTimestamp,
 | 
						|
  };
 | 
						|
 | 
						|
  // 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 ClosedGroup.updateOrCreateClosedGroup(groupDetails);
 | 
						|
 | 
						|
  // ClosedGroup.updateOrCreateClosedGroup will mark the activeAt to Date.now if it's active
 | 
						|
  // But we need to override this value with the sent timestamp of the message creating this group for us.
 | 
						|
  // Having that timestamp set will allow us to pickup incoming group update which were sent between
 | 
						|
  // envelope.timestamp and Date.now(). And we need to listen to those (some might even remove us)
 | 
						|
  convo.set('lastJoinedTimestamp', envelopeTimestamp);
 | 
						|
  await convo.updateExpireTimer(expireTimer, sender, envelopeTimestamp);
 | 
						|
  convo.updateLastMessage();
 | 
						|
 | 
						|
  await convo.commit();
 | 
						|
  // tslint:disable: no-non-null-assertion
 | 
						|
  const ecKeyPair = new ECKeyPair(encryptionKeyPair!.publicKey, encryptionKeyPair!.privateKey);
 | 
						|
  window?.log?.info(`Received the encryptionKeyPair for new group ${groupId}`);
 | 
						|
 | 
						|
  await addKeyPairToCacheAndDBIfNeeded(groupId, ecKeyPair.toHexKeyPair());
 | 
						|
 | 
						|
  // start polling for this new group
 | 
						|
  getSwarmPollingInstance().addGroupId(PubKey.cast(groupId));
 | 
						|
 | 
						|
  await removeFromCache(envelope);
 | 
						|
  // trigger decrypting of all this group messages we did not decrypt successfully yet.
 | 
						|
  await queueAllCachedFromSource(groupId);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This function is called when we get a message with the new encryption keypair for a closed group.
 | 
						|
 * In this message, we have n-times the same keypair encoded with n being the number of current members.
 | 
						|
 * One of that encoded keypair is the one for us. We need to find it, decode it, and save it for use with this group.
 | 
						|
 */
 | 
						|
async function handleClosedGroupEncryptionKeyPair(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
 | 
						|
  isComingFromGroupPubkey: boolean
 | 
						|
) {
 | 
						|
  if (
 | 
						|
    groupUpdate.type !==
 | 
						|
    SignalService.DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
 | 
						|
  ) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  const ourNumber = UserUtils.getOurPubKeyFromCache();
 | 
						|
  // groupUpdate.publicKey might be set. This is used to give an explicitGroupPublicKey for this update.
 | 
						|
  const groupPublicKey = toHex(groupUpdate.publicKey) || envelope.source;
 | 
						|
 | 
						|
  // in the case of an encryption key pair coming as a reply to a request we made
 | 
						|
  // senderIdentity will be unset as the message is not encoded for medium groups
 | 
						|
  const sender = isComingFromGroupPubkey ? envelope.senderIdentity : envelope.source;
 | 
						|
  window?.log?.info(`Got a group update for group ${groupPublicKey}, type: ENCRYPTION_KEY_PAIR`);
 | 
						|
  const ourKeyPair = await UserUtils.getIdentityKeyPair();
 | 
						|
 | 
						|
  if (!ourKeyPair) {
 | 
						|
    window?.log?.warn("Couldn't find user X25519 key pair.");
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  const groupConvo = getConversationController().get(groupPublicKey);
 | 
						|
  if (!groupConvo) {
 | 
						|
    window?.log?.warn(
 | 
						|
      `Ignoring closed group encryption key pair for nonexistent group. ${groupPublicKey}`
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  if (!groupConvo.isClosedGroup()) {
 | 
						|
    window?.log?.warn(
 | 
						|
      `Ignoring closed group encryption key pair for nonexistent medium group. ${groupPublicKey}`
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  if (!groupConvo.get('groupAdmins')?.includes(sender)) {
 | 
						|
    window?.log?.warn(
 | 
						|
      `Ignoring closed group encryption key pair from non-admin. ${groupPublicKey}`
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // Find our wrapper and decrypt it if possible
 | 
						|
  const ourWrapper = groupUpdate.wrappers.find(w => toHex(w.publicKey) === ourNumber.key);
 | 
						|
  if (!ourWrapper) {
 | 
						|
    window?.log?.warn(
 | 
						|
      `Couldn\'t find our wrapper in the encryption keypairs wrappers for group ${groupPublicKey}`
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  let plaintext: Uint8Array;
 | 
						|
  try {
 | 
						|
    perfStart(`encryptionKeyPair-${envelope.id}`);
 | 
						|
 | 
						|
    const buffer = await decryptWithSessionProtocol(
 | 
						|
      envelope,
 | 
						|
      ourWrapper.encryptedKeyPair,
 | 
						|
      ECKeyPair.fromKeyPair(ourKeyPair)
 | 
						|
    );
 | 
						|
    perfEnd(`encryptionKeyPair-${envelope.id}`, 'encryptionKeyPair');
 | 
						|
 | 
						|
    if (!buffer || buffer.byteLength === 0) {
 | 
						|
      throw new Error();
 | 
						|
    }
 | 
						|
    plaintext = new Uint8Array(buffer);
 | 
						|
  } catch (e) {
 | 
						|
    window?.log?.warn("Couldn't decrypt closed group encryption key pair.", e);
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // Parse it
 | 
						|
  let proto: SignalService.KeyPair;
 | 
						|
  try {
 | 
						|
    proto = SignalService.KeyPair.decode(plaintext);
 | 
						|
    if (!proto || proto.privateKey.length === 0 || proto.publicKey.length === 0) {
 | 
						|
      throw new Error();
 | 
						|
    }
 | 
						|
  } catch (e) {
 | 
						|
    window?.log?.warn("Couldn't parse closed group encryption key pair.");
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  let keyPair: ECKeyPair;
 | 
						|
  try {
 | 
						|
    keyPair = new ECKeyPair(proto.publicKey, proto.privateKey);
 | 
						|
  } catch (e) {
 | 
						|
    window?.log?.warn("Couldn't parse closed group encryption key pair.");
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  window?.log?.info(`Received a new encryptionKeyPair for group ${groupPublicKey}`);
 | 
						|
 | 
						|
  // Store it if needed
 | 
						|
  const newKeyPairInHex = keyPair.toHexKeyPair();
 | 
						|
 | 
						|
  const isKeyPairAlreadyHere = await addKeyPairToCacheAndDBIfNeeded(
 | 
						|
    groupPublicKey,
 | 
						|
    newKeyPairInHex
 | 
						|
  );
 | 
						|
 | 
						|
  if (isKeyPairAlreadyHere) {
 | 
						|
    window?.log?.info('Dropping already saved keypair for group', groupPublicKey);
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  window?.log?.info('Got a new encryption keypair for group', groupPublicKey);
 | 
						|
  await removeFromCache(envelope);
 | 
						|
  // trigger decrypting of all this group messages we did not decrypt successfully yet.
 | 
						|
  await queueAllCachedFromSource(groupPublicKey);
 | 
						|
}
 | 
						|
 | 
						|
async function performIfValid(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage
 | 
						|
) {
 | 
						|
  const { Type } = SignalService.DataMessage.ClosedGroupControlMessage;
 | 
						|
 | 
						|
  const groupPublicKey = envelope.source;
 | 
						|
  const sender = envelope.senderIdentity;
 | 
						|
 | 
						|
  if (PubKey.isClosedGroupV3(groupPublicKey)) {
 | 
						|
    window?.log?.warn(
 | 
						|
      'Message ignored; closed group v3 updates cannot come from SignalService.DataMessage.ClosedGroupControlMessage '
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  const convo = getConversationController().get(groupPublicKey);
 | 
						|
  if (!convo) {
 | 
						|
    window?.log?.warn('dropping message for nonexistent group');
 | 
						|
    return removeFromCache(envelope);
 | 
						|
  }
 | 
						|
 | 
						|
  if (!convo) {
 | 
						|
    window?.log?.warn('Ignoring a closed group update message (INFO) for a non-existing group');
 | 
						|
    return removeFromCache(envelope);
 | 
						|
  }
 | 
						|
 | 
						|
  // Check that the message isn't from before the group was created
 | 
						|
  let lastJoinedTimestamp = convo.get('lastJoinedTimestamp');
 | 
						|
  // might happen for existing groups
 | 
						|
  if (!lastJoinedTimestamp) {
 | 
						|
    const aYearAgo = Date.now() - 1000 * 60 * 24 * 365;
 | 
						|
    convo.set({
 | 
						|
      lastJoinedTimestamp: aYearAgo,
 | 
						|
    });
 | 
						|
    lastJoinedTimestamp = aYearAgo;
 | 
						|
  }
 | 
						|
 | 
						|
  const envelopeTimestamp = toNumber(envelope.timestamp);
 | 
						|
  if (envelopeTimestamp <= lastJoinedTimestamp) {
 | 
						|
    window?.log?.warn(
 | 
						|
      'Got a group update with an older timestamp than when we joined this group last time. Dropping it.'
 | 
						|
    );
 | 
						|
    return removeFromCache(envelope);
 | 
						|
  }
 | 
						|
 | 
						|
  // Check that the sender is a member of the group (before the update)
 | 
						|
  const oldMembers = convo.get('members') || [];
 | 
						|
  if (!oldMembers.includes(sender)) {
 | 
						|
    window?.log?.error(
 | 
						|
      `Error: closed group: ignoring closed group update message from non-member. ${sender} is not a current member.`
 | 
						|
    );
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  // make sure the conversation with this user exist (even if it's just hidden)
 | 
						|
  await getConversationController().getOrCreateAndWait(sender, ConversationTypeEnum.PRIVATE);
 | 
						|
 | 
						|
  const moreRecentOrNah = await sentAtMoreRecentThanWrapper(envelopeTimestamp, 'UserGroupsConfig');
 | 
						|
  const shouldNotApplyGroupChange = moreRecentOrNah === 'wrapper_more_recent';
 | 
						|
 | 
						|
  if (groupUpdate.type === Type.NAME_CHANGE) {
 | 
						|
    await handleClosedGroupNameChanged(envelope, groupUpdate, convo, shouldNotApplyGroupChange);
 | 
						|
  } else if (groupUpdate.type === Type.MEMBERS_ADDED) {
 | 
						|
    await handleClosedGroupMembersAdded(envelope, groupUpdate, convo, shouldNotApplyGroupChange);
 | 
						|
  } else if (groupUpdate.type === Type.MEMBERS_REMOVED) {
 | 
						|
    await handleClosedGroupMembersRemoved(envelope, groupUpdate, convo, shouldNotApplyGroupChange);
 | 
						|
  } else if (groupUpdate.type === Type.MEMBER_LEFT) {
 | 
						|
    await handleClosedGroupMemberLeft(envelope, convo, shouldNotApplyGroupChange);
 | 
						|
  } else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) {
 | 
						|
    await removeFromCache(envelope);
 | 
						|
  }
 | 
						|
  // if you add a case here, remember to add it where performIfValid is called too.
 | 
						|
}
 | 
						|
 | 
						|
async function handleClosedGroupNameChanged(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
 | 
						|
  convo: ConversationModel,
 | 
						|
  shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
 | 
						|
) {
 | 
						|
  // Only add update message if we have something to show
 | 
						|
  const newName = groupUpdate.name;
 | 
						|
  window?.log?.info(`Got a group update for group ${envelope.source}, type: NAME_CHANGED`);
 | 
						|
 | 
						|
  if (newName !== convo.get('displayNameInProfile')) {
 | 
						|
    const groupDiff: ClosedGroup.GroupDiff = {
 | 
						|
      newName,
 | 
						|
    };
 | 
						|
    await ClosedGroup.addUpdateMessage(
 | 
						|
      convo,
 | 
						|
      groupDiff,
 | 
						|
      envelope.senderIdentity,
 | 
						|
      toNumber(envelope.timestamp)
 | 
						|
    );
 | 
						|
    if (!shouldOnlyAddUpdateMessage) {
 | 
						|
      convo.set({ displayNameInProfile: newName });
 | 
						|
    }
 | 
						|
    convo.updateLastMessage();
 | 
						|
    await convo.commit();
 | 
						|
  }
 | 
						|
 | 
						|
  await removeFromCache(envelope);
 | 
						|
}
 | 
						|
 | 
						|
async function handleClosedGroupMembersAdded(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
 | 
						|
  convo: ConversationModel,
 | 
						|
  shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
 | 
						|
) {
 | 
						|
  const { members: addedMembersBinary } = groupUpdate;
 | 
						|
  const addedMembers = (addedMembersBinary || []).map(toHex);
 | 
						|
  const oldMembers = convo.get('members') || [];
 | 
						|
  const membersNotAlreadyPresent = addedMembers.filter(m => !oldMembers.includes(m));
 | 
						|
  window?.log?.info(`Got a group update for group ${envelope.source}, type: MEMBERS_ADDED`);
 | 
						|
 | 
						|
  // make sure those members are not on our zombie list
 | 
						|
  addedMembers.forEach(added => removeMemberFromZombies(envelope, PubKey.cast(added), convo));
 | 
						|
 | 
						|
  if (membersNotAlreadyPresent.length === 0) {
 | 
						|
    window?.log?.info(
 | 
						|
      'no new members in this group update compared to what we have already. Skipping update'
 | 
						|
    );
 | 
						|
    // this is just to make sure that the zombie list got written to the db.
 | 
						|
    // if a member adds a member we have as a zombie, we consider that this member is not a zombie anymore
 | 
						|
    await convo.commit();
 | 
						|
    await removeFromCache(envelope);
 | 
						|
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // this is to avoid a race condition where a user gets removed and added back while the admin is offline
 | 
						|
  if (await areWeAdmin(convo)) {
 | 
						|
    await sendLatestKeyPairToUsers(convo, convo.id, membersNotAlreadyPresent);
 | 
						|
  }
 | 
						|
 | 
						|
  const members = [...oldMembers, ...membersNotAlreadyPresent];
 | 
						|
  // make sure the conversation with those members (even if it's just hidden)
 | 
						|
  await Promise.all(
 | 
						|
    members.map(async m =>
 | 
						|
      getConversationController().getOrCreateAndWait(m, ConversationTypeEnum.PRIVATE)
 | 
						|
    )
 | 
						|
  );
 | 
						|
 | 
						|
  const groupDiff: ClosedGroup.GroupDiff = {
 | 
						|
    joiningMembers: membersNotAlreadyPresent,
 | 
						|
  };
 | 
						|
  await ClosedGroup.addUpdateMessage(
 | 
						|
    convo,
 | 
						|
    groupDiff,
 | 
						|
    envelope.senderIdentity,
 | 
						|
    toNumber(envelope.timestamp)
 | 
						|
  );
 | 
						|
 | 
						|
  if (!shouldOnlyAddUpdateMessage) {
 | 
						|
    convo.set({ members });
 | 
						|
  }
 | 
						|
 | 
						|
  convo.updateLastMessage();
 | 
						|
  await convo.commit();
 | 
						|
  await removeFromCache(envelope);
 | 
						|
}
 | 
						|
 | 
						|
async function areWeAdmin(groupConvo: ConversationModel) {
 | 
						|
  if (!groupConvo) {
 | 
						|
    throw new Error('areWeAdmin needs a convo');
 | 
						|
  }
 | 
						|
 | 
						|
  const groupAdmins = groupConvo.get('groupAdmins');
 | 
						|
  const ourNumber = UserUtils.getOurPubKeyStrFromCache();
 | 
						|
  return groupAdmins?.includes(ourNumber) || false;
 | 
						|
}
 | 
						|
 | 
						|
async function handleClosedGroupMembersRemoved(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage,
 | 
						|
  convo: ConversationModel,
 | 
						|
  shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
 | 
						|
) {
 | 
						|
  // 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);
 | 
						|
    throw new Error('Admins cannot be removed. They can only leave');
 | 
						|
  }
 | 
						|
 | 
						|
  // The MEMBERS_REMOVED message type can only come from an admin.
 | 
						|
  if (!groupAdmins.includes(envelope.senderIdentity)) {
 | 
						|
    window?.log?.warn('Ignoring invalid closed group update. Only admins can remove members.');
 | 
						|
    await removeFromCache(envelope);
 | 
						|
    throw new Error('Only admins can remove members.');
 | 
						|
  }
 | 
						|
 | 
						|
  // If the current user was removed:
 | 
						|
  // • Stop polling for the group
 | 
						|
  // • Remove the key pairs associated with the group
 | 
						|
  const ourPubKey = UserUtils.getOurPubKeyFromCache();
 | 
						|
  const wasCurrentUserKicked = !membersAfterUpdate.includes(ourPubKey.key);
 | 
						|
  if (wasCurrentUserKicked) {
 | 
						|
    // we now want to remove everything related to a group when we get kicked from it.
 | 
						|
    await getConversationController().deleteClosedGroup(groupPubKey, {
 | 
						|
      fromSyncMessage: false,
 | 
						|
      sendLeaveMessage: false,
 | 
						|
    });
 | 
						|
  } else {
 | 
						|
    // Note: we don't want to send a new encryption keypair when we get a member removed.
 | 
						|
    // this is only happening when the admin gets a MEMBER_LEFT message
 | 
						|
 | 
						|
    // Only add update message if we have something to show
 | 
						|
    if (membersAfterUpdate.length !== currentMembers.length) {
 | 
						|
      const groupDiff: ClosedGroup.GroupDiff = {
 | 
						|
        kickedMembers: effectivelyRemovedMembers,
 | 
						|
      };
 | 
						|
      await ClosedGroup.addUpdateMessage(
 | 
						|
        convo,
 | 
						|
        groupDiff,
 | 
						|
        envelope.senderIdentity,
 | 
						|
        toNumber(envelope.timestamp)
 | 
						|
      );
 | 
						|
      convo.updateLastMessage();
 | 
						|
    }
 | 
						|
 | 
						|
    // Update the group
 | 
						|
    const zombies = convo.get('zombies').filter(z => membersAfterUpdate.includes(z));
 | 
						|
 | 
						|
    if (!shouldOnlyAddUpdateMessage) {
 | 
						|
      convo.set({ members: membersAfterUpdate });
 | 
						|
      convo.set({ zombies });
 | 
						|
    }
 | 
						|
    await convo.commit();
 | 
						|
  }
 | 
						|
  await removeFromCache(envelope);
 | 
						|
}
 | 
						|
 | 
						|
function isUserAZombie(convo: ConversationModel, user: PubKey) {
 | 
						|
  return convo.get('zombies').includes(user.key);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns true if the user was not a zombie and so was added to the zombies.
 | 
						|
 * No commit() are called
 | 
						|
 */
 | 
						|
function addMemberToZombies(
 | 
						|
  _envelope: EnvelopePlus,
 | 
						|
  userToAdd: PubKey,
 | 
						|
  convo: ConversationModel
 | 
						|
): boolean {
 | 
						|
  const zombies = convo.get('zombies');
 | 
						|
  const isAlreadyZombie = isUserAZombie(convo, userToAdd);
 | 
						|
 | 
						|
  if (isAlreadyZombie) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  convo.set('zombies', [...zombies, userToAdd.key]);
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 *
 | 
						|
 * Returns true if the user was not a zombie and so was not removed from the zombies.
 | 
						|
 * Note: no commit() are made
 | 
						|
 */
 | 
						|
function removeMemberFromZombies(
 | 
						|
  _envelope: EnvelopePlus,
 | 
						|
  userToAdd: PubKey,
 | 
						|
  convo: ConversationModel
 | 
						|
): boolean {
 | 
						|
  const zombies = convo.get('zombies');
 | 
						|
  const isAlreadyAZombie = isUserAZombie(convo, userToAdd);
 | 
						|
 | 
						|
  if (!isAlreadyAZombie) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  convo.set(
 | 
						|
    'zombies',
 | 
						|
    zombies.filter(z => z !== userToAdd.key)
 | 
						|
  );
 | 
						|
  return true;
 | 
						|
}
 | 
						|
 | 
						|
async function handleClosedGroupAdminMemberLeft(groupPublicKey: string, envelope: EnvelopePlus) {
 | 
						|
  // if the admin was remove and we are the admin, it can only be voluntary
 | 
						|
  await getConversationController().deleteClosedGroup(groupPublicKey, {
 | 
						|
    fromSyncMessage: false,
 | 
						|
    sendLeaveMessage: false,
 | 
						|
  });
 | 
						|
  await removeFromCache(envelope);
 | 
						|
}
 | 
						|
 | 
						|
async function handleClosedGroupLeftOurself(groupId: string, envelope: EnvelopePlus) {
 | 
						|
  // if we ourself left. It can only mean that another of our device left the group and we just synced that message through the swarm
 | 
						|
  await getConversationController().deleteClosedGroup(groupId, {
 | 
						|
    fromSyncMessage: false,
 | 
						|
    sendLeaveMessage: false,
 | 
						|
  });
 | 
						|
  await removeFromCache(envelope);
 | 
						|
}
 | 
						|
 | 
						|
async function handleClosedGroupMemberLeft(
 | 
						|
  envelope: EnvelopePlus,
 | 
						|
  convo: ConversationModel,
 | 
						|
  shouldOnlyAddUpdateMessage: boolean // set this to true to not apply the change to the convo itself, just add the update in the conversation
 | 
						|
) {
 | 
						|
  const sender = envelope.senderIdentity;
 | 
						|
  const groupPublicKey = envelope.source;
 | 
						|
  const didAdminLeave = convo.get('groupAdmins')?.includes(sender) || false;
 | 
						|
  // If the admin leaves the group is disbanded
 | 
						|
  // otherwise, we remove the sender from the list of current members in this group
 | 
						|
  const oldMembers = convo.get('members') || [];
 | 
						|
  const newMembers = oldMembers.filter(s => s !== sender);
 | 
						|
  window?.log?.info(`Got a group update for group ${envelope.source}, type: MEMBER_LEFT`);
 | 
						|
 | 
						|
  // Show log if we sent this message ourself (from another device or not)
 | 
						|
  if (UserUtils.isUsFromCache(sender)) {
 | 
						|
    window?.log?.info('Got self-sent group update member left...');
 | 
						|
  }
 | 
						|
  const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
 | 
						|
 | 
						|
  // if the admin leaves, the group is disabled for everyone
 | 
						|
  if (didAdminLeave) {
 | 
						|
    await handleClosedGroupAdminMemberLeft(groupPublicKey, envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // if we are no longer a member, we LEFT from another device
 | 
						|
  if (!newMembers.includes(ourPubkey)) {
 | 
						|
    // stop polling, remove everything from this closed group and the corresponding conversation
 | 
						|
    await handleClosedGroupLeftOurself(groupPublicKey, envelope);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // Another member left, not us, not the admin, just another member.
 | 
						|
  // But this member was in the list of members (as performIfValid checks for that)
 | 
						|
  const groupDiff: ClosedGroup.GroupDiff = {
 | 
						|
    leavingMembers: [sender],
 | 
						|
  };
 | 
						|
 | 
						|
  await ClosedGroup.addUpdateMessage(
 | 
						|
    convo,
 | 
						|
    groupDiff,
 | 
						|
    envelope.senderIdentity,
 | 
						|
    toNumber(envelope.timestamp)
 | 
						|
  );
 | 
						|
  convo.updateLastMessage();
 | 
						|
  // if a user just left and we are the admin, we remove him right away for everyone by sending a MEMBERS_REMOVED message so no need to add him as a zombie
 | 
						|
  if (oldMembers.includes(sender)) {
 | 
						|
    addMemberToZombies(envelope, PubKey.cast(sender), convo);
 | 
						|
  }
 | 
						|
  if (!shouldOnlyAddUpdateMessage) {
 | 
						|
    convo.set('members', newMembers);
 | 
						|
  }
 | 
						|
 | 
						|
  await convo.commit();
 | 
						|
 | 
						|
  await removeFromCache(envelope);
 | 
						|
}
 | 
						|
 | 
						|
async function sendLatestKeyPairToUsers(
 | 
						|
  _groupConvo: ConversationModel,
 | 
						|
  groupPubKey: string,
 | 
						|
  targetUsers: Array<string>
 | 
						|
) {
 | 
						|
  // use the inMemory keypair if found
 | 
						|
  const inMemoryKeyPair = distributingClosedGroupEncryptionKeyPairs.get(groupPubKey);
 | 
						|
 | 
						|
  // Get the latest encryption key pair
 | 
						|
  const latestKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair(groupPubKey);
 | 
						|
  if (!inMemoryKeyPair && !latestKeyPair) {
 | 
						|
    window?.log?.info('We do not have the keypair ourself, so dropping this message.');
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  const keyPairToUse = inMemoryKeyPair || ECKeyPair.fromHexKeyPair(latestKeyPair as HexKeyPair);
 | 
						|
 | 
						|
  await Promise.all(
 | 
						|
    targetUsers.map(async member => {
 | 
						|
      window?.log?.info(`Sending latest closed group encryption key pair to: ${member}`);
 | 
						|
      await getConversationController().getOrCreateAndWait(member, ConversationTypeEnum.PRIVATE);
 | 
						|
 | 
						|
      const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers([member], keyPairToUse);
 | 
						|
 | 
						|
      const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({
 | 
						|
        groupId: groupPubKey,
 | 
						|
        timestamp: Date.now(),
 | 
						|
        encryptedKeyPairs: wrappers,
 | 
						|
      });
 | 
						|
 | 
						|
      // the encryption keypair is sent using established channels
 | 
						|
      await getMessageQueue().sendToPubKey(
 | 
						|
        PubKey.cast(member),
 | 
						|
        keypairsMessage,
 | 
						|
        SnodeNamespaces.UserMessages
 | 
						|
      );
 | 
						|
    })
 | 
						|
  );
 | 
						|
}
 |