// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { differenceWith, omit, partition } from 'lodash'; import PQueue from 'p-queue'; import { groupEncrypt, ProtocolAddress, sealedSenderMultiRecipientEncrypt, SenderCertificate, UnidentifiedSenderMessageContent, } from '@signalapp/signal-client'; import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto'; import * as Bytes from '../Bytes'; import { senderCertificateService } from '../services/senderCertificate'; import { padMessage, SenderCertificateMode, SendLogCallbackType, } from '../textsecure/OutgoingMessage'; import { isOlderThan } from './timestamp'; import { GroupSendOptionsType, SendOptionsType, } from '../textsecure/SendMessage'; import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores'; import { ConversationModel } from '../models/conversations'; import { DeviceType, CallbackResultType } from '../textsecure/Types.d'; import { getKeysForIdentifier } from '../textsecure/getKeysForIdentifier'; import { ConversationAttributesType } from '../model-types.d'; import { handleMessageSend, SEALED_SENDER, SendTypesType, shouldSaveProto, } from './handleMessageSend'; import { parseIntOrThrow } from './parseIntOrThrow'; import { multiRecipient200ResponseSchema, multiRecipient409ResponseSchema, multiRecipient410ResponseSchema, } from '../textsecure/WebAPI'; import { SignalService as Proto } from '../protobuf'; import * as RemoteConfig from '../RemoteConfig'; import { strictAssert } from './assert'; import { isGroupV2 } from './whatTypeOfConversation'; const ERROR_EXPIRED_OR_MISSING_DEVICES = 409; const ERROR_STALE_DEVICES = 410; const HOUR = 60 * 60 * 1000; const DAY = 24 * HOUR; const MAX_CONCURRENCY = 5; // sendWithSenderKey is recursive, but we don't want to loop back too many times. const MAX_RECURSION = 10; const ACCESS_KEY_LENGTH = 16; const ZERO_ACCESS_KEY = Bytes.toBase64(new Uint8Array(ACCESS_KEY_LENGTH)); // TODO: remove once we move away from ArrayBuffers const FIXMEU8 = Uint8Array; // Public API: export async function sendToGroup({ contentHint, conversation, groupSendOptions, messageId, isPartialSend, sendOptions, sendType, }: { contentHint: number; conversation: ConversationModel; groupSendOptions: GroupSendOptionsType; isPartialSend?: boolean; messageId: string | undefined; sendOptions?: SendOptionsType; sendType: SendTypesType; }): Promise { strictAssert( window.textsecure.messaging, 'sendToGroup: textsecure.messaging not available!' ); const { timestamp } = groupSendOptions; const recipients = getRecipients(groupSendOptions); // First, do the attachment upload and prepare the proto we'll be sending const protoAttributes = window.textsecure.messaging.getAttrsFromGroupOptions( groupSendOptions ); const contentMessage = await window.textsecure.messaging.getContentMessage( protoAttributes ); return sendContentMessageToGroup({ contentHint, contentMessage, conversation, isPartialSend, messageId, recipients, sendOptions, sendType, timestamp, }); } export async function sendContentMessageToGroup({ contentHint, contentMessage, conversation, isPartialSend, messageId, online, recipients, sendOptions, sendType, timestamp, }: { contentHint: number; contentMessage: Proto.Content; conversation: ConversationModel; isPartialSend?: boolean; messageId: string | undefined; online?: boolean; recipients: Array; sendOptions?: SendOptionsType; sendType: SendTypesType; timestamp: number; }): Promise { const logId = conversation.idForLogging(); strictAssert( window.textsecure.messaging, 'sendContentMessageToGroup: textsecure.messaging not available!' ); const ourConversationId = window.ConversationController.getOurConversationIdOrThrow(); const ourConversation = window.ConversationController.get(ourConversationId); if ( ourConversation?.get('capabilities')?.senderKey && RemoteConfig.isEnabled('desktop.senderKey.send') && isGroupV2(conversation.attributes) ) { try { return await sendToGroupViaSenderKey({ contentHint, contentMessage, conversation, isPartialSend, messageId, online, recipients, recursionCount: 0, sendOptions, sendType, timestamp, }); } catch (error) { window.log.error( `sendToGroup/${logId}: Sender Key send failed, logging, proceeding to normal send`, error && error.stack ? error.stack : error ); } } const sendLogCallback = window.textsecure.messaging.makeSendLogCallback({ contentHint, messageId, proto: Buffer.from(Proto.Content.encode(contentMessage).finish()), sendType, timestamp, }); const groupId = isGroupV2(conversation.attributes) ? conversation.get('groupId') : undefined; return window.textsecure.messaging.sendGroupProto({ contentHint, groupId, options: { ...sendOptions, online }, proto: contentMessage, recipients, sendLogCallback, timestamp, }); } // The Primary Sender Key workflow export async function sendToGroupViaSenderKey(options: { contentHint: number; contentMessage: Proto.Content; conversation: ConversationModel; isPartialSend?: boolean; messageId: string | undefined; online?: boolean; recipients: Array; recursionCount: number; sendOptions?: SendOptionsType; sendType: SendTypesType; timestamp: number; }): Promise { const { contentHint, contentMessage, conversation, isPartialSend, messageId, online, recursionCount, recipients, sendOptions, sendType, timestamp, } = options; const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const logId = conversation.idForLogging(); window.log.info( `sendToGroupViaSenderKey/${logId}: Starting ${timestamp}, recursion count ${recursionCount}...` ); if (recursionCount > MAX_RECURSION) { throw new Error( `sendToGroupViaSenderKey/${logId}: Too much recursion! Count is at ${recursionCount}` ); } const groupId = conversation.get('groupId'); if (!groupId || !isGroupV2(conversation.attributes)) { throw new Error( `sendToGroupViaSenderKey/${logId}: Missing groupId or group is not GV2` ); } if ( contentHint !== ContentHint.DEFAULT && contentHint !== ContentHint.RESENDABLE && contentHint !== ContentHint.IMPLICIT ) { throw new Error( `sendToGroupViaSenderKey/${logId}: Invalid contentHint ${contentHint}` ); } strictAssert( window.textsecure.messaging, 'sendToGroupViaSenderKey: textsecure.messaging not available!' ); const { attributes, }: { attributes: ConversationAttributesType } = conversation; // 1. Add sender key info if we have none, or clear out if it's too old const THIRTY_DAYS = 30 * DAY; if (!attributes.senderKeyInfo) { window.log.info( `sendToGroupViaSenderKey/${logId}: Adding initial sender key info` ); conversation.set({ senderKeyInfo: { createdAtDate: Date.now(), distributionId: window.getGuid(), memberDevices: [], }, }); await window.Signal.Data.updateConversation(attributes); } else if (isOlderThan(attributes.senderKeyInfo.createdAtDate, THIRTY_DAYS)) { const { createdAtDate } = attributes.senderKeyInfo; window.log.info( `sendToGroupViaSenderKey/${logId}: Resetting sender key; ${createdAtDate} is too old` ); await resetSenderKey(conversation); } // 2. Fetch all devices we believe we'll be sending to const { devices: currentDevices, emptyIdentifiers, } = await window.textsecure.storage.protocol.getOpenDevices(recipients); // 3. If we have no open sessions with people we believe we are sending to, and we // believe that any have signal accounts, fetch their prekey bundle and start // sessions with them. if ( emptyIdentifiers.length > 0 && emptyIdentifiers.some(isIdentifierRegistered) ) { await fetchKeysForIdentifiers(emptyIdentifiers); // Restart here to capture devices for accounts we just started sessions with return sendToGroupViaSenderKey({ ...options, recursionCount: recursionCount + 1, }); } strictAssert( attributes.senderKeyInfo, `sendToGroupViaSenderKey/${logId}: expect senderKeyInfo` ); // Note: From here on, we will need to recurse if we change senderKeyInfo const { memberDevices, distributionId, createdAtDate, } = attributes.senderKeyInfo; // 4. Partition devices into sender key and non-sender key groups const [devicesForSenderKey, devicesForNormalSend] = partition( currentDevices, device => isValidSenderKeyRecipient(conversation, device.identifier) ); const senderKeyRecipients = getUuidsFromDevices(devicesForSenderKey); const normalSendRecipients = getUuidsFromDevices(devicesForNormalSend); window.log.info( `sendToGroupViaSenderKey/${logId}:` + ` ${senderKeyRecipients.length} accounts for sender key (${devicesForSenderKey.length} devices),` + ` ${normalSendRecipients.length} accounts for normal send (${devicesForNormalSend.length} devices)` ); // 5. Ensure we have enough recipients if (senderKeyRecipients.length < 2) { throw new Error( `sendToGroupViaSenderKey/${logId}: Not enough recipients for Sender Key message. Failing over.` ); } // 6. Analyze target devices for sender key, determine which have been added or removed const { newToMemberDevices, newToMemberUuids, removedFromMemberDevices, removedFromMemberUuids, } = _analyzeSenderKeyDevices( memberDevices, devicesForSenderKey, isPartialSend ); // 7. If members have been removed from the group, we need to reset our sender key, then // start over to get a fresh set of target devices. const keyNeedsReset = Array.from(removedFromMemberUuids).some( uuid => !conversation.hasMember(uuid) ); if (keyNeedsReset) { await resetSenderKey(conversation); // Restart here to start over; empty memberDevices means we'll send distribution // message to everyone. return sendToGroupViaSenderKey({ ...options, recursionCount: recursionCount + 1, }); } // 8. If there are new members or new devices in the group, we need to ensure that they // have our sender key before we send sender key messages to them. if (newToMemberUuids.length > 0) { window.log.info( `sendToGroupViaSenderKey/${logId}: Sending sender key to ${ newToMemberUuids.length } members: ${JSON.stringify(newToMemberUuids)}` ); await handleMessageSend( window.textsecure.messaging.sendSenderKeyDistributionMessage( { contentHint: ContentHint.RESENDABLE, distributionId, groupId, identifiers: newToMemberUuids, }, sendOptions ? { ...sendOptions, online: false } : undefined ), { messageIds: [], sendType: 'senderKeyDistributionMessage' } ); // Update memberDevices with new devices const updatedMemberDevices = [...memberDevices, ...newToMemberDevices]; conversation.set({ senderKeyInfo: { createdAtDate, distributionId, memberDevices: updatedMemberDevices, }, }); await window.Signal.Data.updateConversation(conversation.attributes); // Restart here because we might have discovered new or dropped devices as part of // distributing our sender key. return sendToGroupViaSenderKey({ ...options, recursionCount: recursionCount + 1, }); } // 9. Update memberDevices with removals which didn't require a reset. if (removedFromMemberDevices.length > 0) { const updatedMemberDevices = [ ...differenceWith( memberDevices, removedFromMemberDevices, deviceComparator ), ]; conversation.set({ senderKeyInfo: { createdAtDate, distributionId, memberDevices: updatedMemberDevices, }, }); await window.Signal.Data.updateConversation(conversation.attributes); } // 10. Send the Sender Key message! let sendLogId: number; let senderKeyRecipientsWithDevices: Record> = {}; devicesForSenderKey.forEach(item => { const { id, identifier } = item; senderKeyRecipientsWithDevices[identifier] ||= []; senderKeyRecipientsWithDevices[identifier].push(id); }); try { const messageBuffer = await encryptForSenderKey({ contentHint, devices: devicesForSenderKey, distributionId, contentMessage: toArrayBuffer( Proto.Content.encode(contentMessage).finish() ), groupId, }); const accessKeys = getXorOfAccessKeys(devicesForSenderKey); const result = await window.textsecure.messaging.sendWithSenderKey( toArrayBuffer(messageBuffer), toArrayBuffer(accessKeys), timestamp, online ); const parsed = multiRecipient200ResponseSchema.safeParse(result); if (parsed.success) { const { uuids404 } = parsed.data; if (uuids404 && uuids404.length > 0) { await _waitForAll({ tasks: uuids404.map(uuid => async () => markIdentifierUnregistered(uuid) ), }); } senderKeyRecipientsWithDevices = omit( senderKeyRecipientsWithDevices, uuids404 || [] ); } else { window.log.error( `sendToGroupViaSenderKey/${logId}: Server returned unexpected 200 response ${JSON.stringify( parsed.error.flatten() )}` ); } if (shouldSaveProto(sendType)) { sendLogId = await window.Signal.Data.insertSentProto( { contentHint, proto: Buffer.from(Proto.Content.encode(contentMessage).finish()), timestamp, }, { recipients: senderKeyRecipientsWithDevices, messageIds: messageId ? [messageId] : [], } ); } } catch (error) { if (error.code === ERROR_EXPIRED_OR_MISSING_DEVICES) { await handle409Response(logId, error); // Restart here to capture the right set of devices for our next send. return sendToGroupViaSenderKey({ ...options, recursionCount: recursionCount + 1, }); } if (error.code === ERROR_STALE_DEVICES) { await handle410Response(conversation, error); // Restart here to use the right registrationIds for devices we already knew about, // as well as send our sender key to these re-registered or re-linked devices. return sendToGroupViaSenderKey({ ...options, recursionCount: recursionCount + 1, }); } throw new Error( `sendToGroupViaSenderKey/${logId}: Returned unexpected error ${ error.code }. Failing over. ${error.stack || error}` ); } // 11. Return early if there are no normal send recipients if (normalSendRecipients.length === 0) { return { dataMessage: contentMessage.dataMessage ? toArrayBuffer( Proto.DataMessage.encode(contentMessage.dataMessage).finish() ) : undefined, successfulIdentifiers: senderKeyRecipients, unidentifiedDeliveries: senderKeyRecipients, contentHint, timestamp, contentProto: Buffer.from(Proto.Content.encode(contentMessage).finish()), recipients: senderKeyRecipientsWithDevices, }; } // 12. Send normal message to the leftover normal recipients. Then combine normal send // result with result from sender key send for final return value. // We don't want to use a normal send log callback here, because the proto has already // been saved as part of the Sender Key send. We're just adding recipients here. const sendLogCallback: SendLogCallbackType = async ({ identifier, deviceIds, }: { identifier: string; deviceIds: Array; }) => { if (!shouldSaveProto(sendType)) { return; } const sentToConversation = window.ConversationController.get(identifier); if (!sentToConversation) { window.log.warn( `sendToGroupViaSenderKey/callback: Unable to find conversation for identifier ${identifier}` ); return; } const recipientUuid = sentToConversation.get('uuid'); if (!recipientUuid) { window.log.warn( `sendToGroupViaSenderKey/callback: Conversation ${conversation.idForLogging()} had no UUID` ); return; } await window.Signal.Data.insertProtoRecipients({ id: sendLogId, recipientUuid, deviceIds, }); }; const normalSendResult = await window.textsecure.messaging.sendGroupProto({ contentHint, groupId, options: { ...sendOptions, online }, proto: contentMessage, recipients: normalSendRecipients, sendLogCallback, timestamp, }); return { dataMessage: contentMessage.dataMessage ? toArrayBuffer( Proto.DataMessage.encode(contentMessage.dataMessage).finish() ) : undefined, errors: normalSendResult.errors, failoverIdentifiers: normalSendResult.failoverIdentifiers, successfulIdentifiers: [ ...(normalSendResult.successfulIdentifiers || []), ...senderKeyRecipients, ], unidentifiedDeliveries: [ ...(normalSendResult.unidentifiedDeliveries || []), ...senderKeyRecipients, ], contentHint, timestamp, contentProto: Buffer.from(Proto.Content.encode(contentMessage).finish()), recipients: { ...normalSendResult.recipients, ...senderKeyRecipientsWithDevices, }, }; } // Utility Methods export async function _waitForAll({ tasks, maxConcurrency = MAX_CONCURRENCY, }: { tasks: Array<() => Promise>; maxConcurrency?: number; }): Promise> { const queue = new PQueue({ concurrency: maxConcurrency, timeout: 2 * 60 * 1000, }); return queue.addAll(tasks); } function getRecipients(options: GroupSendOptionsType): Array { if (options.groupV2) { return options.groupV2.members; } if (options.groupV1) { return options.groupV1.members; } throw new Error('getRecipients: Unable to extract recipients!'); } async function markIdentifierUnregistered(identifier: string) { const conversation = window.ConversationController.getOrCreate( identifier, 'private' ); conversation.setUnregistered(); await window.Signal.Data.updateConversation(conversation.attributes); await window.textsecure.storage.protocol.archiveAllSessions(identifier); } function isIdentifierRegistered(identifier: string) { const conversation = window.ConversationController.getOrCreate( identifier, 'private' ); const isUnregistered = conversation.isUnregistered(); return !isUnregistered; } async function handle409Response(logId: string, error: Error) { const parsed = multiRecipient409ResponseSchema.safeParse(error.response); if (parsed.success) { await _waitForAll({ tasks: parsed.data.map(item => async () => { const { uuid, devices } = item; // Start new sessions with devices we didn't know about before if (devices.missingDevices && devices.missingDevices.length > 0) { await fetchKeysForIdentifier(uuid, devices.missingDevices); } // Archive sessions with devices that have been removed if (devices.extraDevices && devices.extraDevices.length > 0) { await _waitForAll({ tasks: devices.extraDevices.map(deviceId => async () => { const address = `${uuid}.${deviceId}`; await window.textsecure.storage.protocol.archiveSession(address); }), }); } }), maxConcurrency: 2, }); } else { window.log.error( `handle409Response/${logId}: Server returned unexpected 409 response ${JSON.stringify( parsed.error.flatten() )}` ); throw error; } } async function handle410Response( conversation: ConversationModel, error: Error ) { const logId = conversation.idForLogging(); const parsed = multiRecipient410ResponseSchema.safeParse(error.response); if (parsed.success) { await _waitForAll({ tasks: parsed.data.map(item => async () => { const { uuid, devices } = item; if (devices.staleDevices && devices.staleDevices.length > 0) { // First, archive our existing sessions with these devices await _waitForAll({ tasks: devices.staleDevices.map(deviceId => async () => { const address = `${uuid}.${deviceId}`; await window.textsecure.storage.protocol.archiveSession(address); }), }); // Start new sessions with these devices await fetchKeysForIdentifier(uuid, devices.staleDevices); // Forget that we've sent our sender key to these devices, since they've // been re-registered or re-linked. const senderKeyInfo = conversation.get('senderKeyInfo'); if (senderKeyInfo) { const devicesToRemove: Array = devices.staleDevices.map( id => ({ id, identifier: uuid }) ); conversation.set({ senderKeyInfo: { ...senderKeyInfo, memberDevices: differenceWith( senderKeyInfo.memberDevices, devicesToRemove, partialDeviceComparator ), }, }); await window.Signal.Data.updateConversation( conversation.attributes ); } } }), maxConcurrency: 2, }); } else { window.log.error( `handle410Response/${logId}: Server returned unexpected 410 response ${JSON.stringify( parsed.error.flatten() )}` ); throw error; } } function getXorOfAccessKeys(devices: Array): Buffer { const uuids = getUuidsFromDevices(devices); const result = Buffer.alloc(ACCESS_KEY_LENGTH); strictAssert( result.length === ACCESS_KEY_LENGTH, 'getXorOfAccessKeys starting value' ); uuids.forEach(uuid => { const conversation = window.ConversationController.get(uuid); if (!conversation) { throw new Error( `getXorOfAccessKeys: Unable to fetch conversation for UUID ${uuid}` ); } const accessKey = getAccessKey(conversation.attributes); if (!accessKey) { throw new Error(`getXorOfAccessKeys: No accessKey for UUID ${uuid}`); } const accessKeyBuffer = Buffer.from(accessKey, 'base64'); if (accessKeyBuffer.length !== ACCESS_KEY_LENGTH) { throw new Error( `getXorOfAccessKeys: Access key for ${uuid} had length ${accessKeyBuffer.length}` ); } for (let i = 0; i < ACCESS_KEY_LENGTH; i += 1) { // eslint-disable-next-line no-bitwise result[i] ^= accessKeyBuffer[i]; } }); return result; } async function encryptForSenderKey({ contentHint, contentMessage, devices, distributionId, groupId, }: { contentHint: number; contentMessage: ArrayBuffer; devices: Array; distributionId: string; groupId: string; }): Promise { const ourUuid = window.textsecure.storage.user.getUuid(); const ourDeviceId = window.textsecure.storage.user.getDeviceId(); if (!ourUuid || !ourDeviceId) { throw new Error( 'encryptForSenderKey: Unable to fetch our uuid or deviceId' ); } const sender = ProtocolAddress.new( ourUuid, parseIntOrThrow(ourDeviceId, 'encryptForSenderKey, ourDeviceId') ); const ourAddress = getOurAddress(); const senderKeyStore = new SenderKeys(); const message = Buffer.from(padMessage(new FIXMEU8(contentMessage))); const ciphertextMessage = await window.textsecure.storage.protocol.enqueueSenderKeyJob( ourAddress, () => groupEncrypt(sender, distributionId, senderKeyStore, message) ); const groupIdBuffer = Buffer.from(groupId, 'base64'); const senderCertificateObject = await senderCertificateService.get( SenderCertificateMode.WithoutE164 ); if (!senderCertificateObject) { throw new Error('encryptForSenderKey: Unable to fetch sender certifiate!'); } const senderCertificate = SenderCertificate.deserialize( Buffer.from(senderCertificateObject.serialized) ); const content = UnidentifiedSenderMessageContent.new( ciphertextMessage, senderCertificate, contentHint, groupIdBuffer ); const recipients = devices.map(device => ProtocolAddress.new(device.identifier, device.id) ); const identityKeyStore = new IdentityKeys(); const sessionStore = new Sessions(); return sealedSenderMultiRecipientEncrypt( content, recipients, identityKeyStore, sessionStore ); } function isValidSenderKeyRecipient( conversation: ConversationModel, uuid: string ): boolean { if (!conversation.hasMember(uuid)) { window.log.info( `isValidSenderKeyRecipient: Sending to ${uuid}, not a group member` ); return false; } const memberConversation = window.ConversationController.get(uuid); if (!memberConversation) { window.log.warn( `isValidSenderKeyRecipient: Missing conversation model for member ${uuid}` ); return false; } const capabilities = memberConversation.get('capabilities'); if (!capabilities?.senderKey) { return false; } if (!getAccessKey(memberConversation.attributes)) { return false; } if (memberConversation.isUnregistered()) { window.log.warn( `isValidSenderKeyRecipient: Member ${uuid} is unregistered` ); return false; } return true; } function deviceComparator(left?: DeviceType, right?: DeviceType): boolean { return Boolean( left && right && left.id === right.id && left.identifier === right.identifier && left.registrationId === right.registrationId ); } type PartialDeviceType = Omit; function partialDeviceComparator( left?: PartialDeviceType, right?: PartialDeviceType ): boolean { return Boolean( left && right && left.id === right.id && left.identifier === right.identifier ); } function getUuidsFromDevices(devices: Array): Array { const uuids = new Set(); devices.forEach(device => { uuids.add(device.identifier); }); return Array.from(uuids); } export function _analyzeSenderKeyDevices( memberDevices: Array, devicesForSend: Array, isPartialSend?: boolean ): { newToMemberDevices: Array; newToMemberUuids: Array; removedFromMemberDevices: Array; removedFromMemberUuids: Array; } { const newToMemberDevices = differenceWith( devicesForSend, memberDevices, deviceComparator ); const newToMemberUuids = getUuidsFromDevices(newToMemberDevices); // If this is a partial send, we won't do anything with device removals if (isPartialSend) { return { newToMemberDevices, newToMemberUuids, removedFromMemberDevices: [], removedFromMemberUuids: [], }; } const removedFromMemberDevices = differenceWith( memberDevices, devicesForSend, deviceComparator ); const removedFromMemberUuids = getUuidsFromDevices(removedFromMemberDevices); return { newToMemberDevices, newToMemberUuids, removedFromMemberDevices, removedFromMemberUuids, }; } function getOurAddress(): string { const ourUuid = window.textsecure.storage.user.getUuid(); const ourDeviceId = window.textsecure.storage.user.getDeviceId(); if (!ourUuid || !ourDeviceId) { throw new Error('getOurAddress: Unable to fetch our uuid or deviceId'); } return `${ourUuid}.${ourDeviceId}`; } async function resetSenderKey(conversation: ConversationModel): Promise { const logId = conversation.idForLogging(); window.log.info( `resetSenderKey/${logId}: Sender key needs reset. Clearing data...` ); const { attributes, }: { attributes: ConversationAttributesType } = conversation; const { senderKeyInfo } = attributes; if (!senderKeyInfo) { window.log.warn(`resetSenderKey/${logId}: No sender key info`); return; } const { distributionId } = senderKeyInfo; const address = getOurAddress(); await window.textsecure.storage.protocol.removeSenderKey( address, distributionId ); // Note: We preserve existing distributionId to minimize space for sender key storage conversation.set({ senderKeyInfo: { createdAtDate: Date.now(), distributionId, memberDevices: [], }, }); await window.Signal.Data.updateConversation(conversation.attributes); } function getAccessKey( attributes: ConversationAttributesType ): string | undefined { const { sealedSender, accessKey } = attributes; if (sealedSender === SEALED_SENDER.ENABLED) { return accessKey || undefined; } if ( sealedSender === SEALED_SENDER.UNKNOWN || sealedSender === SEALED_SENDER.UNRESTRICTED ) { return ZERO_ACCESS_KEY; } return undefined; } async function fetchKeysForIdentifiers( identifiers: Array ): Promise { window.log.info( `fetchKeysForIdentifiers: Fetching keys for ${identifiers.length} identifiers` ); try { await _waitForAll({ tasks: identifiers.map(identifier => async () => fetchKeysForIdentifier(identifier) ), }); } catch (error) { window.log.error( 'fetchKeysForIdentifiers: Failed to fetch keys:', error && error.stack ? error.stack : error ); } } async function fetchKeysForIdentifier( identifier: string, devices?: Array ): Promise { window.log.info( `fetchKeysForIdentifier: Fetching ${ devices || 'all' } devices for ${identifier}` ); if (!window.textsecure?.messaging?.server) { throw new Error('fetchKeysForIdentifier: No server available!'); } const emptyConversation = window.ConversationController.getOrCreate( identifier, 'private' ); try { const { accessKeyFailed } = await getKeysForIdentifier( identifier, window.textsecure?.messaging?.server, devices, getAccessKey(emptyConversation.attributes) ); if (accessKeyFailed) { window.log.info( `fetchKeysForIdentifiers: Setting sealedSender to DISABLED for conversation ${emptyConversation.idForLogging()}` ); emptyConversation.set({ sealedSender: SEALED_SENDER.DISABLED, }); await window.Signal.Data.updateConversation(emptyConversation.attributes); } } catch (error) { if (error.name === 'UnregisteredUserError') { await markIdentifierUnregistered(identifier); return; } throw error; } }