2023-01-03 19:55:46 +00:00
// Copyright 2021 Signal Messenger, LLC
2021-05-25 22:40:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2024-09-06 17:52:19 +00:00
import { differenceWith , omit } from 'lodash' ;
2023-08-10 16:43:33 +00:00
import { v4 as generateUuid } from 'uuid' ;
2021-05-25 22:40:04 +00:00
import {
2021-09-02 20:44:34 +00:00
ErrorCode ,
2022-11-22 18:43:43 +00:00
LibSignalErrorBase ,
2021-05-25 22:40:04 +00:00
groupEncrypt ,
ProtocolAddress ,
sealedSenderMultiRecipientEncrypt ,
SenderCertificate ,
UnidentifiedSenderMessageContent ,
2022-03-24 21:47:21 +00:00
} from '@signalapp/libsignal-client' ;
2021-08-04 01:02:35 +00:00
import * as Bytes from '../Bytes' ;
2021-05-25 22:40:04 +00:00
import { senderCertificateService } from '../services/senderCertificate' ;
2021-10-26 19:15:33 +00:00
import type { SendLogCallbackType } from '../textsecure/OutgoingMessage' ;
2021-05-25 22:40:04 +00:00
import {
padMessage ,
SenderCertificateMode ,
} from '../textsecure/OutgoingMessage' ;
2021-09-10 02:38:11 +00:00
import { Address } from '../types/Address' ;
import { QualifiedAddress } from '../types/QualifiedAddress' ;
2022-11-22 18:43:43 +00:00
import * as Errors from '../types/errors' ;
2024-09-06 17:52:19 +00:00
import { DataWriter , DataReader } from '../sql/Client' ;
2023-12-07 23:59:54 +00:00
import { getValue } from '../RemoteConfig' ;
2023-08-10 16:43:33 +00:00
import type { ServiceIdString } from '../types/ServiceId' ;
import { ServiceIdKind } from '../types/ServiceId' ;
2022-01-31 21:51:24 +00:00
import { isRecord } from './isRecord' ;
2021-05-25 22:40:04 +00:00
import { isOlderThan } from './timestamp' ;
2021-10-26 19:15:33 +00:00
import type {
2021-05-25 22:40:04 +00:00
GroupSendOptionsType ,
SendOptionsType ,
} from '../textsecure/SendMessage' ;
2022-01-31 21:51:24 +00:00
import {
ConnectTimeoutError ,
2023-01-21 00:50:34 +00:00
IncorrectSenderKeyAuthError ,
2022-01-31 21:51:24 +00:00
OutgoingIdentityKeyError ,
SendMessageProtoError ,
2023-01-13 22:01:47 +00:00
UnknownRecipientError ,
2022-01-31 21:51:24 +00:00
UnregisteredUserError ,
} from '../textsecure/Errors' ;
2021-10-26 19:15:33 +00:00
import type { HTTPError } from '../textsecure/Errors' ;
2021-05-25 22:40:04 +00:00
import { IdentityKeys , SenderKeys , Sessions } from '../LibSignalStores' ;
2021-10-26 19:15:33 +00:00
import type { ConversationModel } from '../models/conversations' ;
import type { DeviceType , CallbackResultType } from '../textsecure/Types.d' ;
2023-08-10 16:43:33 +00:00
import { getKeysForServiceId } from '../textsecure/getKeysForServiceId' ;
2021-12-10 02:15:59 +00:00
import type {
ConversationAttributesType ,
SenderKeyInfoType ,
} from '../model-types.d' ;
2021-10-26 19:15:33 +00:00
import type { SendTypesType } from './handleMessageSend' ;
2022-02-22 20:34:57 +00:00
import { handleMessageSend , shouldSaveProto } from './handleMessageSend' ;
import { SEALED_SENDER } from '../types/SealedSender' ;
2021-05-25 22:40:04 +00:00
import { parseIntOrThrow } from './parseIntOrThrow' ;
import {
multiRecipient200ResponseSchema ,
multiRecipient409ResponseSchema ,
multiRecipient410ResponseSchema ,
} from '../textsecure/WebAPI' ;
2021-07-09 19:36:10 +00:00
import { SignalService as Proto } from '../protobuf' ;
2021-05-25 22:40:04 +00:00
2021-07-30 18:35:25 +00:00
import { strictAssert } from './assert' ;
2021-09-17 18:27:53 +00:00
import * as log from '../logging/log' ;
2022-01-08 02:12:13 +00:00
import { GLOBAL_ZONE } from '../SignalProtocolStore' ;
2022-11-11 04:10:30 +00:00
import { waitForAll } from './waitForAll' ;
2024-09-06 17:52:19 +00:00
import {
GroupSendEndorsementState ,
onFailedToSendWithEndorsements ,
} from './groupSendEndorsements' ;
import { maybeUpdateGroup } from '../groups' ;
import type { GroupSendToken } from '../types/GroupSendEndorsements' ;
2024-09-16 19:37:38 +00:00
import { isAciString } from './isAciString' ;
2024-10-02 19:03:10 +00:00
import { safeParseStrict , safeParseUnknown } from './schemas' ;
2021-05-25 22:40:04 +00:00
2023-01-13 22:01:47 +00:00
const UNKNOWN_RECIPIENT = 404 ;
2023-01-21 00:50:34 +00:00
const INCORRECT_AUTH_KEY = 401 ;
2021-05-25 22:40:04 +00:00
const ERROR_EXPIRED_OR_MISSING_DEVICES = 409 ;
const ERROR_STALE_DEVICES = 410 ;
const HOUR = 60 * 60 * 1000 ;
const DAY = 24 * HOUR ;
// sendWithSenderKey is recursive, but we don't want to loop back too many times.
2021-07-30 18:35:25 +00:00
const MAX_RECURSION = 10 ;
2021-05-25 22:40:04 +00:00
2021-08-04 01:02:35 +00:00
const ACCESS_KEY_LENGTH = 16 ;
const ZERO_ACCESS_KEY = Bytes . toBase64 ( new Uint8Array ( ACCESS_KEY_LENGTH ) ) ;
2021-05-25 22:40:04 +00:00
// Public API:
2021-12-10 02:15:59 +00:00
export type SenderKeyTargetType = {
getGroupId : ( ) = > string | undefined ;
getMembers : ( ) = > Array < ConversationModel > ;
2023-08-10 16:43:33 +00:00
hasMember : ( serviceId : ServiceIdString ) = > boolean ;
2021-12-10 02:15:59 +00:00
idForLogging : ( ) = > string ;
isGroupV2 : ( ) = > boolean ;
isValid : ( ) = > boolean ;
getSenderKeyInfo : ( ) = > SenderKeyInfoType | undefined ;
saveSenderKeyInfo : ( senderKeyInfo : SenderKeyInfoType ) = > Promise < void > ;
} ;
2021-07-02 18:34:17 +00:00
export async function sendToGroup ( {
2022-05-23 22:08:13 +00:00
abortSignal ,
2021-07-02 18:34:17 +00:00
contentHint ,
2021-07-15 23:48:09 +00:00
groupSendOptions ,
2021-07-02 18:34:17 +00:00
isPartialSend ,
2021-12-10 02:15:59 +00:00
messageId ,
2021-07-15 23:48:09 +00:00
sendOptions ,
2021-12-10 02:15:59 +00:00
sendTarget ,
2021-07-15 23:48:09 +00:00
sendType ,
2022-10-07 17:02:08 +00:00
story ,
2022-07-01 16:55:13 +00:00
urgent ,
2021-07-02 18:34:17 +00:00
} : {
2022-05-23 22:08:13 +00:00
abortSignal? : AbortSignal ;
2021-07-02 18:34:17 +00:00
contentHint : number ;
2021-07-15 23:48:09 +00:00
groupSendOptions : GroupSendOptionsType ;
2021-07-02 18:34:17 +00:00
isPartialSend? : boolean ;
2021-07-15 23:48:09 +00:00
messageId : string | undefined ;
sendOptions? : SendOptionsType ;
2021-12-10 02:15:59 +00:00
sendTarget : SenderKeyTargetType ;
2021-07-15 23:48:09 +00:00
sendType : SendTypesType ;
2022-10-07 17:02:08 +00:00
story? : boolean ;
2022-07-01 16:55:13 +00:00
urgent : boolean ;
2021-07-02 18:34:17 +00:00
} ) : Promise < CallbackResultType > {
2021-07-30 18:35:25 +00:00
strictAssert (
2021-05-25 22:40:04 +00:00
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
2021-11-11 22:43:05 +00:00
const protoAttributes =
window . textsecure . messaging . getAttrsFromGroupOptions ( groupSendOptions ) ;
2024-07-24 00:31:40 +00:00
const contentMessage =
await window . textsecure . messaging . getContentMessage ( protoAttributes ) ;
2021-05-25 22:40:04 +00:00
2022-05-23 22:08:13 +00:00
// Attachment upload might take too long to succeed - we don't want to proceed
// with the send if the caller aborted this call.
if ( abortSignal ? . aborted ) {
throw new Error ( 'sendToGroup was aborted' ) ;
}
2021-05-25 22:40:04 +00:00
return sendContentMessageToGroup ( {
2021-05-28 19:11:19 +00:00
contentHint ,
2021-05-25 22:40:04 +00:00
contentMessage ,
isPartialSend ,
2021-07-15 23:48:09 +00:00
messageId ,
2021-05-25 22:40:04 +00:00
recipients ,
sendOptions ,
2021-12-10 02:15:59 +00:00
sendTarget ,
2021-07-15 23:48:09 +00:00
sendType ,
2022-10-07 17:02:08 +00:00
story ,
2021-05-25 22:40:04 +00:00
timestamp ,
2022-07-01 16:55:13 +00:00
urgent ,
2021-05-25 22:40:04 +00:00
} ) ;
}
2024-09-06 17:52:19 +00:00
type SendToGroupOptions = Readonly < {
2021-05-28 19:11:19 +00:00
contentHint : number ;
2021-07-09 19:36:10 +00:00
contentMessage : Proto.Content ;
2021-05-25 22:40:04 +00:00
isPartialSend? : boolean ;
2021-07-15 23:48:09 +00:00
messageId : string | undefined ;
2021-05-25 22:40:04 +00:00
online? : boolean ;
2023-08-10 16:43:33 +00:00
recipients : ReadonlyArray < ServiceIdString > ;
2021-05-25 22:40:04 +00:00
sendOptions? : SendOptionsType ;
2021-12-10 02:15:59 +00:00
sendTarget : SenderKeyTargetType ;
2021-07-15 23:48:09 +00:00
sendType : SendTypesType ;
2022-09-30 16:59:36 +00:00
story? : boolean ;
2021-05-25 22:40:04 +00:00
timestamp : number ;
2022-07-01 16:55:13 +00:00
urgent : boolean ;
2024-09-06 17:52:19 +00:00
} > ;
// Note: This is the group send chokepoint. The 1:1 send chokepoint is sendMessageProto.
export async function sendContentMessageToGroup (
options : SendToGroupOptions
) : Promise < CallbackResultType > {
const {
contentHint ,
contentMessage ,
messageId ,
online ,
recipients ,
sendOptions ,
sendTarget ,
sendType ,
story ,
timestamp ,
urgent ,
} = options ;
2021-12-10 02:15:59 +00:00
const logId = sendTarget . idForLogging ( ) ;
2023-07-14 16:53:20 +00:00
const accountManager = window . getAccountManager ( ) ;
2023-08-10 16:43:33 +00:00
if ( accountManager . areKeysOutOfDate ( ServiceIdKind . ACI ) ) {
2023-07-14 16:53:20 +00:00
log . warn (
` sendToGroup/ ${ logId } : Keys are out of date; updating before send `
) ;
2023-08-10 16:43:33 +00:00
await accountManager . maybeUpdateKeys ( ServiceIdKind . ACI ) ;
if ( accountManager . areKeysOutOfDate ( ServiceIdKind . ACI ) ) {
2023-07-14 16:53:20 +00:00
throw new Error ( 'Keys still out of date after update' ) ;
}
}
2021-07-30 18:35:25 +00:00
strictAssert (
2021-05-25 22:40:04 +00:00
window . textsecure . messaging ,
'sendContentMessageToGroup: textsecure.messaging not available!'
) ;
2023-12-07 23:59:54 +00:00
if ( sendTarget . isValid ( ) ) {
2021-05-25 22:40:04 +00:00
try {
2024-09-06 17:52:19 +00:00
return await sendToGroupViaSenderKey (
options ,
0 ,
'init (sendContentMessageToGroup)'
) ;
2021-11-20 15:58:38 +00:00
} catch ( error : unknown ) {
if ( ! ( error instanceof Error ) ) {
throw error ;
}
2022-01-31 21:51:24 +00:00
if ( _shouldFailSend ( error , logId ) ) {
2021-11-20 15:58:38 +00:00
throw error ;
}
2021-09-17 18:27:53 +00:00
log . error (
2021-05-25 22:40:04 +00:00
` sendToGroup/ ${ logId } : Sender Key send failed, logging, proceeding to normal send ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-05-25 22:40:04 +00:00
) ;
}
}
2021-07-15 23:48:09 +00:00
const sendLogCallback = window . textsecure . messaging . makeSendLogCallback ( {
contentHint ,
messageId ,
proto : Buffer.from ( Proto . Content . encode ( contentMessage ) . finish ( ) ) ,
sendType ,
timestamp ,
2022-07-01 16:55:13 +00:00
urgent ,
2022-08-15 21:53:33 +00:00
hasPniSignatureMessage : false ,
2021-07-15 23:48:09 +00:00
} ) ;
2021-12-10 02:15:59 +00:00
const groupId = sendTarget . isGroupV2 ( ) ? sendTarget . getGroupId ( ) : undefined ;
2021-07-02 18:34:17 +00:00
return window . textsecure . messaging . sendGroupProto ( {
2021-05-28 19:11:19 +00:00
contentHint ,
groupId ,
2021-07-02 18:34:17 +00:00
options : { . . . sendOptions , online } ,
2021-07-15 23:48:09 +00:00
proto : contentMessage ,
recipients ,
sendLogCallback ,
2022-09-30 16:59:36 +00:00
story ,
2021-07-15 23:48:09 +00:00
timestamp ,
2022-07-01 16:55:13 +00:00
urgent ,
2021-07-02 18:34:17 +00:00
} ) ;
2021-05-25 22:40:04 +00:00
}
// The Primary Sender Key workflow
2024-09-06 17:52:19 +00:00
export async function sendToGroupViaSenderKey (
options : SendToGroupOptions ,
recursionCount : number ,
recursionReason : string
) : Promise < CallbackResultType > {
2021-05-25 22:40:04 +00:00
const {
2021-05-28 19:11:19 +00:00
contentHint ,
2021-05-25 22:40:04 +00:00
contentMessage ,
isPartialSend ,
2021-07-15 23:48:09 +00:00
messageId ,
2021-05-25 22:40:04 +00:00
online ,
recipients ,
sendOptions ,
2021-12-10 02:15:59 +00:00
sendTarget ,
2021-07-15 23:48:09 +00:00
sendType ,
2022-09-30 16:59:36 +00:00
story ,
2021-05-25 22:40:04 +00:00
timestamp ,
2022-07-01 16:55:13 +00:00
urgent ,
2021-05-25 22:40:04 +00:00
} = options ;
2021-07-09 19:36:10 +00:00
const { ContentHint } = Proto . UnidentifiedSenderMessage . Message ;
2021-05-25 22:40:04 +00:00
2021-12-10 02:15:59 +00:00
const logId = sendTarget . idForLogging ( ) ;
2021-09-17 18:27:53 +00:00
log . info (
2024-09-06 17:52:19 +00:00
` sendToGroupViaSenderKey/ ${ logId } : Starting ${ timestamp } , recursion count ${ recursionCount } , reason: ${ recursionReason } ... `
2021-05-25 22:40:04 +00:00
) ;
if ( recursionCount > MAX_RECURSION ) {
throw new Error (
` sendToGroupViaSenderKey/ ${ logId } : Too much recursion! Count is at ${ recursionCount } `
) ;
}
2021-12-10 02:15:59 +00:00
const groupId = sendTarget . getGroupId ( ) ;
if ( ! sendTarget . isValid ( ) ) {
2021-05-25 22:40:04 +00:00
throw new Error (
2021-12-10 02:15:59 +00:00
` sendToGroupViaSenderKey/ ${ logId } : sendTarget is not valid! `
2021-05-25 22:40:04 +00:00
) ;
}
2021-05-28 19:11:19 +00:00
if (
2021-06-08 21:51:58 +00:00
contentHint !== ContentHint . DEFAULT &&
2021-05-28 19:11:19 +00:00
contentHint !== ContentHint . RESENDABLE &&
2021-06-08 21:51:58 +00:00
contentHint !== ContentHint . IMPLICIT
2021-05-28 19:11:19 +00:00
) {
throw new Error (
` sendToGroupViaSenderKey/ ${ logId } : Invalid contentHint ${ contentHint } `
) ;
}
2021-07-30 18:35:25 +00:00
strictAssert (
2021-05-25 22:40:04 +00:00
window . textsecure . messaging ,
'sendToGroupViaSenderKey: textsecure.messaging not available!'
) ;
// 1. Add sender key info if we have none, or clear out if it's too old
2021-12-10 02:15:59 +00:00
// Note: From here on, generally need to recurse if we change senderKeyInfo
const senderKeyInfo = sendTarget . getSenderKeyInfo ( ) ;
if ( ! senderKeyInfo ) {
2021-09-17 18:27:53 +00:00
log . info (
2021-05-25 22:40:04 +00:00
` sendToGroupViaSenderKey/ ${ logId } : Adding initial sender key info `
) ;
2021-12-10 02:15:59 +00:00
await sendTarget . saveSenderKeyInfo ( {
createdAtDate : Date.now ( ) ,
2023-08-10 16:43:33 +00:00
distributionId : generateUuid ( ) ,
2021-12-10 02:15:59 +00:00
memberDevices : [ ] ,
} ) ;
// Restart here because we updated senderKeyInfo
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'Added missing sender key info'
) ;
2021-12-10 02:15:59 +00:00
}
2024-09-06 17:52:19 +00:00
const EXPIRE_DURATION = getSenderKeyExpireDuration ( ) ;
2022-02-11 21:09:35 +00:00
if ( isOlderThan ( senderKeyInfo . createdAtDate , EXPIRE_DURATION ) ) {
2021-12-10 02:15:59 +00:00
const { createdAtDate } = senderKeyInfo ;
2021-09-17 18:27:53 +00:00
log . info (
2021-05-25 22:40:04 +00:00
` sendToGroupViaSenderKey/ ${ logId } : Resetting sender key; ${ createdAtDate } is too old `
) ;
2021-12-10 02:15:59 +00:00
await resetSenderKey ( sendTarget ) ;
// Restart here because we updated senderKeyInfo
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'sender key info expired'
) ;
2021-05-25 22:40:04 +00:00
}
// 2. Fetch all devices we believe we'll be sending to
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
const { devices : currentDevices , emptyServiceIds } =
await window . textsecure . storage . protocol . getOpenDevices ( ourAci , recipients ) ;
2021-05-25 22:40:04 +00:00
2024-09-06 17:52:19 +00:00
const conversation =
groupId != null
? ( window . ConversationController . get ( groupId ) ? ? null )
: null ;
let groupSendEndorsementState : GroupSendEndorsementState | null = null ;
if ( groupId != null ) {
2024-09-16 19:37:38 +00:00
strictAssert ( conversation , 'Must have conversation for endorsements' ) ;
2024-09-06 17:52:19 +00:00
const data = await DataReader . getGroupSendEndorsementsData ( groupId ) ;
if ( data == null ) {
2024-09-16 19:37:38 +00:00
if ( conversation . isMember ( ourAci ) ) {
onFailedToSendWithEndorsements (
new Error (
` sendToGroupViaSenderKey/ ${ logId } : Missing all endorsements for group `
)
) ;
}
2024-09-06 17:52:19 +00:00
} else {
log . info (
` sendToGroupViaSenderKey/ ${ logId } : Loaded endorsements for ${ data . memberEndorsements . length } members `
) ;
const groupSecretParamsBase64 = conversation . get ( 'secretParams' ) ;
strictAssert ( groupSecretParamsBase64 , 'Must have secret params' ) ;
groupSendEndorsementState = new GroupSendEndorsementState (
data ,
groupSecretParamsBase64
) ;
if (
groupSendEndorsementState != null &&
! groupSendEndorsementState . isSafeExpirationRange ( )
) {
log . info (
` sendToGroupViaSenderKey/ ${ logId } : Endorsements close to expiration ( ${ groupSendEndorsementState . getExpiration ( ) . getTime ( ) } , ${ Date . now ( ) } ), refreshing group `
) ;
await maybeUpdateGroup ( { conversation } ) ;
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'group send endorsements outside expiration range'
) ;
}
}
}
2021-05-25 22:40:04 +00:00
// 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 (
2023-08-10 16:43:33 +00:00
emptyServiceIds . length > 0 &&
2023-08-16 20:54:39 +00:00
emptyServiceIds . some ( isServiceIdRegistered )
2021-05-25 22:40:04 +00:00
) {
2024-09-06 17:52:19 +00:00
await fetchKeysForServiceIds ( emptyServiceIds , groupSendEndorsementState ) ;
2021-05-25 22:40:04 +00:00
2021-07-30 18:35:25 +00:00
// Restart here to capture devices for accounts we just started sessions with
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'fetched prekey bundles'
) ;
2021-05-25 22:40:04 +00:00
}
2023-08-16 20:54:39 +00:00
const { memberDevices , distributionId , createdAtDate } = senderKeyInfo ;
2021-12-10 02:15:59 +00:00
const memberSet = new Set ( sendTarget . getMembers ( ) ) ;
2021-08-19 15:52:08 +00:00
2021-05-25 22:40:04 +00:00
// 4. Partition devices into sender key and non-sender key groups
2024-09-06 17:52:19 +00:00
const devicesForSenderKey : Array < DeviceType > = [ ] ;
const devicesForNormalSend : Array < DeviceType > = [ ] ;
for ( const device of currentDevices ) {
if (
isValidSenderKeyRecipient (
memberSet ,
groupSendEndorsementState ,
device . serviceId ,
{ story }
)
) {
devicesForSenderKey . push ( device ) ;
} else {
devicesForNormalSend . push ( device ) ;
}
}
2021-07-15 23:48:09 +00:00
2023-08-10 16:43:33 +00:00
const senderKeyRecipients = getServiceIdsFromDevices ( devicesForSenderKey ) ;
const normalSendRecipients = getServiceIdsFromDevices ( devicesForNormalSend ) ;
2021-09-17 18:27:53 +00:00
log . info (
2021-07-15 23:48:09 +00:00
` sendToGroupViaSenderKey/ ${ logId } : ` +
` ${ senderKeyRecipients . length } accounts for sender key ( ${ devicesForSenderKey . length } devices), ` +
` ${ normalSendRecipients . length } accounts for normal send ( ${ devicesForNormalSend . length } devices) `
2021-05-25 22:40:04 +00:00
) ;
2021-05-27 20:47:39 +00:00
// 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
2021-05-25 22:40:04 +00:00
const {
newToMemberDevices ,
2023-08-10 16:43:33 +00:00
newToMemberServiceIds ,
2021-05-25 22:40:04 +00:00
removedFromMemberDevices ,
2023-08-10 16:43:33 +00:00
removedFromMemberServiceIds ,
2021-05-25 22:40:04 +00:00
} = _analyzeSenderKeyDevices (
memberDevices ,
devicesForSenderKey ,
isPartialSend
) ;
2021-05-27 20:47:39 +00:00
// 7. If members have been removed from the group, we need to reset our sender key, then
2021-05-25 22:40:04 +00:00
// start over to get a fresh set of target devices.
2023-08-10 16:43:33 +00:00
const keyNeedsReset = Array . from ( removedFromMemberServiceIds ) . some (
serviceId = > ! sendTarget . hasMember ( serviceId )
2021-05-25 22:40:04 +00:00
) ;
if ( keyNeedsReset ) {
2021-12-10 02:15:59 +00:00
await resetSenderKey ( sendTarget ) ;
2021-05-25 22:40:04 +00:00
// Restart here to start over; empty memberDevices means we'll send distribution
// message to everyone.
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'removed members in send target'
) ;
2021-05-25 22:40:04 +00:00
}
2021-05-27 20:47:39 +00:00
// 8. If there are new members or new devices in the group, we need to ensure that they
2021-05-25 22:40:04 +00:00
// have our sender key before we send sender key messages to them.
2023-08-10 16:43:33 +00:00
if ( newToMemberServiceIds . length > 0 ) {
2021-09-17 18:27:53 +00:00
log . info (
2021-05-25 22:40:04 +00:00
` sendToGroupViaSenderKey/ ${ logId } : Sending sender key to ${
2023-08-10 16:43:33 +00:00
newToMemberServiceIds . length
} members : $ { JSON . stringify ( newToMemberServiceIds ) } `
2021-05-25 22:40:04 +00:00
) ;
2022-06-02 21:25:55 +00:00
try {
await handleMessageSend (
window . textsecure . messaging . sendSenderKeyDistributionMessage (
{
2022-11-28 23:30:42 +00:00
contentHint ,
2022-06-02 21:25:55 +00:00
distributionId ,
groupId ,
2023-08-10 16:43:33 +00:00
serviceIds : newToMemberServiceIds ,
2022-10-17 17:39:10 +00:00
// SKDMs should only have story=true if we're sending to a distribution list
story : sendTarget.getGroupId ( ) ? false : story ,
2022-07-01 16:55:13 +00:00
urgent ,
2022-06-02 21:25:55 +00:00
} ,
sendOptions ? { . . . sendOptions , online : false } : undefined
) ,
{ messageIds : [ ] , sendType : 'senderKeyDistributionMessage' }
) ;
} catch ( error ) {
// If we partially fail to send the sender key distribution message (SKDM), we don't
// want the successful SKDM sends to be considered an overall success.
if ( error instanceof SendMessageProtoError ) {
throw new SendMessageProtoError ( {
. . . error ,
sendIsNotFinal : true ,
} ) ;
}
throw error ;
}
2021-07-30 18:35:25 +00:00
2021-08-03 00:42:00 +00:00
// Update memberDevices with new devices
const updatedMemberDevices = [ . . . memberDevices , . . . newToMemberDevices ] ;
2021-12-10 02:15:59 +00:00
await sendTarget . saveSenderKeyInfo ( {
createdAtDate ,
distributionId ,
2023-08-16 20:54:39 +00:00
memberDevices : updatedMemberDevices ,
2021-08-03 00:42:00 +00:00
} ) ;
2021-07-30 18:35:25 +00:00
// Restart here because we might have discovered new or dropped devices as part of
// distributing our sender key.
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'sent skdm to new members'
) ;
2021-05-25 22:40:04 +00:00
}
2021-08-03 00:42:00 +00:00
// 9. Update memberDevices with removals which didn't require a reset.
if ( removedFromMemberDevices . length > 0 ) {
2021-05-25 22:40:04 +00:00
const updatedMemberDevices = [
. . . differenceWith < DeviceType , DeviceType > (
memberDevices ,
removedFromMemberDevices ,
deviceComparator
) ,
] ;
2021-12-10 02:15:59 +00:00
await sendTarget . saveSenderKeyInfo ( {
createdAtDate ,
distributionId ,
2023-08-16 20:54:39 +00:00
memberDevices : updatedMemberDevices ,
2021-05-25 22:40:04 +00:00
} ) ;
2021-12-10 02:15:59 +00:00
// Note, we do not need to restart here because we don't refer back to senderKeyInfo
// after this point.
2021-05-25 22:40:04 +00:00
}
// 10. Send the Sender Key message!
2021-07-15 23:48:09 +00:00
let sendLogId : number ;
2023-08-10 16:43:33 +00:00
let senderKeyRecipientsWithDevices : Record <
ServiceIdString ,
Array < number >
> = { } ;
2021-07-15 23:48:09 +00:00
devicesForSenderKey . forEach ( item = > {
2023-08-10 16:43:33 +00:00
const { id , serviceId } = item ;
senderKeyRecipientsWithDevices [ serviceId ] || = [ ] ;
senderKeyRecipientsWithDevices [ serviceId ] . push ( id ) ;
2021-07-15 23:48:09 +00:00
} ) ;
2024-09-06 17:52:19 +00:00
let groupSendToken : GroupSendToken | undefined ;
let accessKeys : Buffer | undefined ;
if ( groupSendEndorsementState != null ) {
strictAssert ( conversation , 'Must have conversation for endorsements' ) ;
2024-09-16 19:37:38 +00:00
try {
groupSendToken = groupSendEndorsementState . buildToken (
new Set ( senderKeyRecipients )
) ;
} catch ( error ) {
onFailedToSendWithEndorsements ( error ) ;
}
2024-09-06 17:52:19 +00:00
} else {
accessKeys = getXorOfAccessKeys ( devicesForSenderKey , { story } ) ;
}
2021-05-25 22:40:04 +00:00
try {
const messageBuffer = await encryptForSenderKey ( {
2021-05-28 19:11:19 +00:00
contentHint ,
2021-05-25 22:40:04 +00:00
devices : devicesForSenderKey ,
distributionId ,
2021-09-24 00:49:05 +00:00
contentMessage : Proto.Content.encode ( contentMessage ) . finish ( ) ,
2021-05-25 22:40:04 +00:00
groupId ,
} ) ;
2022-07-01 16:55:13 +00:00
const result = await window . textsecure . messaging . server . sendWithSenderKey (
2021-09-24 00:49:05 +00:00
messageBuffer ,
accessKeys ,
2024-09-06 17:52:19 +00:00
groupSendToken ,
2021-05-25 22:40:04 +00:00
timestamp ,
2022-10-07 17:02:08 +00:00
{ online , story , urgent }
2021-05-25 22:40:04 +00:00
) ;
2024-10-02 19:03:10 +00:00
const parsed = safeParseStrict ( multiRecipient200ResponseSchema , result ) ;
2021-05-25 22:40:04 +00:00
if ( parsed . success ) {
const { uuids404 } = parsed . data ;
if ( uuids404 && uuids404 . length > 0 ) {
2022-11-11 04:10:30 +00:00
await waitForAll ( {
2021-11-11 22:43:05 +00:00
tasks : uuids404.map (
2023-08-10 16:43:33 +00:00
serviceId = > async ( ) = > markServiceIdUnregistered ( serviceId )
2021-05-25 22:40:04 +00:00
) ,
} ) ;
}
2021-07-15 23:48:09 +00:00
senderKeyRecipientsWithDevices = omit (
senderKeyRecipientsWithDevices ,
uuids404 || [ ]
) ;
2021-05-25 22:40:04 +00:00
} else {
2021-09-17 18:27:53 +00:00
log . error (
2021-05-25 22:40:04 +00:00
` sendToGroupViaSenderKey/ ${ logId } : Server returned unexpected 200 response ${ JSON . stringify (
parsed . error . flatten ( )
) } `
) ;
}
2021-07-15 23:48:09 +00:00
if ( shouldSaveProto ( sendType ) ) {
2024-07-22 18:16:33 +00:00
sendLogId = await DataWriter . insertSentProto (
2021-07-15 23:48:09 +00:00
{
contentHint ,
proto : Buffer.from ( Proto . Content . encode ( contentMessage ) . finish ( ) ) ,
timestamp ,
2022-07-01 16:55:13 +00:00
urgent ,
2022-08-15 21:53:33 +00:00
hasPniSignatureMessage : false ,
2021-07-15 23:48:09 +00:00
} ,
{
recipients : senderKeyRecipientsWithDevices ,
messageIds : messageId ? [ messageId ] : [ ] ,
}
) ;
}
2021-05-25 22:40:04 +00:00
} catch ( error ) {
2023-01-13 22:01:47 +00:00
if ( error . code === UNKNOWN_RECIPIENT ) {
2024-09-06 17:52:19 +00:00
onFailedToSendWithEndorsements ( error ) ;
2023-01-13 22:01:47 +00:00
throw new UnknownRecipientError ( ) ;
}
2023-01-21 00:50:34 +00:00
if ( error . code === INCORRECT_AUTH_KEY ) {
2024-09-06 17:52:19 +00:00
onFailedToSendWithEndorsements ( error ) ;
2023-01-21 00:50:34 +00:00
throw new IncorrectSenderKeyAuthError ( ) ;
}
2023-01-13 22:01:47 +00:00
2021-05-25 22:40:04 +00:00
if ( error . code === ERROR_EXPIRED_OR_MISSING_DEVICES ) {
2024-09-06 17:52:19 +00:00
await handle409Response ( sendTarget , groupSendEndorsementState , error ) ;
2021-05-25 22:40:04 +00:00
// Restart here to capture the right set of devices for our next send.
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'error: expired or missing devices'
) ;
2021-05-25 22:40:04 +00:00
}
if ( error . code === ERROR_STALE_DEVICES ) {
2024-09-06 17:52:19 +00:00
await handle410Response ( sendTarget , groupSendEndorsementState , error ) ;
2021-05-25 22:40:04 +00:00
// 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.
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'error: stale devices'
) ;
2021-05-25 22:40:04 +00:00
}
2022-11-22 18:43:43 +00:00
if (
error instanceof LibSignalErrorBase &&
error . code === ErrorCode . InvalidRegistrationId
) {
2021-09-02 20:44:34 +00:00
const address = error . addr as ProtocolAddress ;
const name = address . name ( ) ;
const brokenAccount = window . ConversationController . get ( name ) ;
if ( brokenAccount ) {
2021-09-17 18:27:53 +00:00
log . warn (
2021-09-02 20:44:34 +00:00
` sendToGroupViaSenderKey/ ${ logId } : Disabling sealed sender for ${ brokenAccount . idForLogging ( ) } `
) ;
brokenAccount . set ( { sealedSender : SEALED_SENDER.DISABLED } ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . updateConversation ( brokenAccount . attributes ) ;
2021-09-02 20:44:34 +00:00
// Now that we've eliminate this problematic account, we can try the send again.
2024-09-06 17:52:19 +00:00
return sendToGroupViaSenderKey (
options ,
recursionCount + 1 ,
'error: invalid registration id'
) ;
2021-09-02 20:44:34 +00:00
}
}
2021-05-25 22:40:04 +00:00
2024-09-16 19:37:38 +00:00
if ( groupSendEndorsementState != null ) {
onFailedToSendWithEndorsements ( error ) ;
}
2022-09-30 20:33:24 +00:00
log . error (
` sendToGroupViaSenderKey/ ${ logId } : Returned unexpected error code: ${
2021-07-15 23:48:09 +00:00
error . code
2022-09-30 20:33:24 +00:00
} , error class : $ { typeof error } `
2021-05-25 22:40:04 +00:00
) ;
2022-09-30 20:33:24 +00:00
throw error ;
2021-05-25 22:40:04 +00:00
}
// 11. Return early if there are no normal send recipients
2021-07-15 23:48:09 +00:00
if ( normalSendRecipients . length === 0 ) {
2021-05-25 22:40:04 +00:00
return {
2021-07-09 19:36:10 +00:00
dataMessage : contentMessage.dataMessage
2021-09-24 00:49:05 +00:00
? Proto . DataMessage . encode ( contentMessage . dataMessage ) . finish ( )
2021-07-09 19:36:10 +00:00
: undefined ,
2023-05-10 01:25:37 +00:00
editMessage : contentMessage.editMessage
? Proto . EditMessage . encode ( contentMessage . editMessage ) . finish ( )
: undefined ,
2023-08-10 16:43:33 +00:00
successfulServiceIds : senderKeyRecipients ,
2021-05-25 22:40:04 +00:00
unidentifiedDeliveries : senderKeyRecipients ,
2021-07-15 23:48:09 +00:00
contentHint ,
timestamp ,
contentProto : Buffer.from ( Proto . Content . encode ( contentMessage ) . finish ( ) ) ,
recipients : senderKeyRecipientsWithDevices ,
2022-07-01 16:55:13 +00:00
urgent ,
2021-05-25 22:40:04 +00:00
} ;
}
// 12. Send normal message to the leftover normal recipients. Then combine normal send
// result with result from sender key send for final return value.
2021-07-15 23:48:09 +00:00
// 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 ( {
2023-08-10 16:43:33 +00:00
serviceId ,
2021-07-15 23:48:09 +00:00
deviceIds ,
} : {
2023-08-10 16:43:33 +00:00
serviceId : ServiceIdString ;
2021-07-15 23:48:09 +00:00
deviceIds : Array < number > ;
} ) = > {
if ( ! shouldSaveProto ( sendType ) ) {
return ;
}
2023-08-10 16:43:33 +00:00
const sentToConversation = window . ConversationController . get ( serviceId ) ;
2021-07-15 23:48:09 +00:00
if ( ! sentToConversation ) {
2021-09-17 18:27:53 +00:00
log . warn (
2023-08-10 16:43:33 +00:00
` sendToGroupViaSenderKey/callback: Unable to find conversation for serviceId ${ serviceId } `
2021-07-15 23:48:09 +00:00
) ;
return ;
}
2023-08-16 20:54:39 +00:00
const recipientServiceId = sentToConversation . getServiceId ( ) ;
2023-08-10 16:43:33 +00:00
if ( ! recipientServiceId ) {
2021-09-17 18:27:53 +00:00
log . warn (
2023-08-16 20:54:39 +00:00
` sendToGroupViaSenderKey/callback: Conversation ${ sentToConversation . idForLogging ( ) } had no service id `
2021-07-15 23:48:09 +00:00
) ;
return ;
}
2024-07-22 18:16:33 +00:00
await DataWriter . insertProtoRecipients ( {
2021-07-15 23:48:09 +00:00
id : sendLogId ,
2023-08-10 16:43:33 +00:00
recipientServiceId ,
2021-07-15 23:48:09 +00:00
deviceIds ,
} ) ;
} ;
2021-05-25 22:40:04 +00:00
2022-02-25 01:22:19 +00:00
try {
const normalSendResult = await window . textsecure . messaging . sendGroupProto ( {
contentHint ,
groupId ,
options : { . . . sendOptions , online } ,
proto : contentMessage ,
recipients : normalSendRecipients ,
sendLogCallback ,
timestamp ,
2022-07-01 16:55:13 +00:00
urgent ,
2022-02-25 01:22:19 +00:00
} ) ;
return mergeSendResult ( {
result : normalSendResult ,
senderKeyRecipients ,
senderKeyRecipientsWithDevices ,
} ) ;
} catch ( error : unknown ) {
if ( error instanceof SendMessageProtoError ) {
const callbackResult = mergeSendResult ( {
result : error ,
senderKeyRecipients ,
senderKeyRecipientsWithDevices ,
} ) ;
throw new SendMessageProtoError ( callbackResult ) ;
}
throw error ;
}
}
2024-02-09 03:56:23 +00:00
// Public utility methods
export async function resetSenderKey (
sendTarget : SenderKeyTargetType
) : Promise < void > {
const logId = sendTarget . idForLogging ( ) ;
log . info ( ` resetSenderKey/ ${ logId } : Sender key needs reset. Clearing data... ` ) ;
const senderKeyInfo = sendTarget . getSenderKeyInfo ( ) ;
if ( ! senderKeyInfo ) {
log . warn ( ` resetSenderKey/ ${ logId } : No sender key info ` ) ;
return ;
}
const { distributionId } = senderKeyInfo ;
const ourAddress = getOurAddress ( ) ;
// Note: We preserve existing distributionId to minimize space for sender key storage
await sendTarget . saveSenderKeyInfo ( {
createdAtDate : Date.now ( ) ,
distributionId ,
memberDevices : [ ] ,
} ) ;
const ourAci = window . storage . user . getCheckedAci ( ) ;
await window . textsecure . storage . protocol . removeSenderKey (
new QualifiedAddress ( ourAci , ourAddress ) ,
distributionId
) ;
}
2022-02-25 01:22:19 +00:00
// Utility Methods
function mergeSendResult ( {
result ,
senderKeyRecipients ,
senderKeyRecipientsWithDevices ,
} : {
result : CallbackResultType | SendMessageProtoError ;
2023-08-10 16:43:33 +00:00
senderKeyRecipients : Array < ServiceIdString > ;
senderKeyRecipientsWithDevices : Record < ServiceIdString , Array < number > > ;
2022-02-25 01:22:19 +00:00
} ) : CallbackResultType {
2021-05-25 22:40:04 +00:00
return {
2022-02-25 01:22:19 +00:00
. . . result ,
2023-08-10 16:43:33 +00:00
successfulServiceIds : [
. . . ( result . successfulServiceIds || [ ] ) ,
2021-05-25 22:40:04 +00:00
. . . senderKeyRecipients ,
] ,
unidentifiedDeliveries : [
2022-02-25 01:22:19 +00:00
. . . ( result . unidentifiedDeliveries || [ ] ) ,
2021-05-25 22:40:04 +00:00
. . . senderKeyRecipients ,
] ,
2021-07-15 23:48:09 +00:00
recipients : {
2022-02-25 01:22:19 +00:00
. . . result . recipients ,
2021-07-15 23:48:09 +00:00
. . . senderKeyRecipientsWithDevices ,
} ,
2021-05-25 22:40:04 +00:00
} ;
}
2022-02-11 21:09:35 +00:00
const MAX_SENDER_KEY_EXPIRE_DURATION = 90 * DAY ;
function getSenderKeyExpireDuration ( ) : number {
try {
const parsed = parseIntOrThrow (
2022-05-03 20:24:31 +00:00
getValue ( 'desktop.senderKeyMaxAge' ) ,
2022-02-11 21:09:35 +00:00
'getSenderKeyExpireDuration'
) ;
const duration = Math . min ( parsed , MAX_SENDER_KEY_EXPIRE_DURATION ) ;
log . info (
` getSenderKeyExpireDuration: using expire duration of ${ duration } `
) ;
return duration ;
} catch ( error ) {
log . warn (
` getSenderKeyExpireDuration: Failed to parse integer. Using default of ${ MAX_SENDER_KEY_EXPIRE_DURATION } . ` ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2022-02-11 21:09:35 +00:00
) ;
return MAX_SENDER_KEY_EXPIRE_DURATION ;
}
}
2022-01-31 21:51:24 +00:00
export function _shouldFailSend ( error : unknown , logId : string ) : boolean {
const logError = ( message : string ) = > {
log . error ( ` _shouldFailSend/ ${ logId } : ${ message } ` ) ;
} ;
2023-01-21 00:50:34 +00:00
// We need to fail over to a normal send if multi_recipient/ endpoint returns 404 or 401
2023-01-13 22:01:47 +00:00
if ( error instanceof UnknownRecipientError ) {
return false ;
}
2023-01-21 00:50:34 +00:00
if ( error instanceof IncorrectSenderKeyAuthError ) {
return false ;
}
2023-01-13 22:01:47 +00:00
2022-11-22 18:43:43 +00:00
if (
error instanceof LibSignalErrorBase &&
error . code === ErrorCode . UntrustedIdentity
) {
2022-01-31 21:51:24 +00:00
logError ( "'untrusted identity' error, failing." ) ;
return true ;
}
if ( error instanceof OutgoingIdentityKeyError ) {
logError ( 'OutgoingIdentityKeyError error, failing.' ) ;
return true ;
}
if ( error instanceof UnregisteredUserError ) {
logError ( 'UnregisteredUserError error, failing.' ) ;
return true ;
}
if ( error instanceof ConnectTimeoutError ) {
logError ( 'ConnectTimeoutError error, failing.' ) ;
return true ;
}
// Known error types captured here:
// HTTPError
// OutgoingMessageError
// SendMessageNetworkError
// SendMessageChallengeError
// MessageError
if ( isRecord ( error ) && typeof error . code === 'number' ) {
2023-11-17 18:16:48 +00:00
if ( error . code === - 1 ) {
logError ( "We don't have connectivity. Failing." ) ;
return true ;
}
2022-05-23 16:27:40 +00:00
if ( error . code === 400 ) {
logError ( 'Invalid request, failing.' ) ;
return true ;
}
2022-01-31 21:51:24 +00:00
if ( error . code === 404 ) {
2023-01-13 22:01:47 +00:00
logError ( 'Failed to fetch metadata before send, failing.' ) ;
2022-01-31 21:51:24 +00:00
return true ;
}
2022-02-25 00:26:58 +00:00
if ( error . code === 413 || error . code === 429 ) {
2022-01-31 21:51:24 +00:00
logError ( 'Rate limit error, failing.' ) ;
return true ;
}
if ( error . code === 428 ) {
logError ( 'Challenge error, failing.' ) ;
return true ;
}
if ( error . code === 500 ) {
logError ( 'Server error, failing.' ) ;
return true ;
}
if ( error . code === 508 ) {
logError ( 'Fail job error, failing.' ) ;
return true ;
}
}
if ( error instanceof SendMessageProtoError ) {
if ( ! error . errors || ! error . errors . length ) {
2023-06-21 00:06:38 +00:00
logError ( 'SendMessageProtoError had no errors but was thrown! Failing.' ) ;
return true ;
}
2023-08-10 16:43:33 +00:00
if ( error . successfulServiceIds && error . successfulServiceIds . length > 0 ) {
2023-06-21 00:06:38 +00:00
logError (
'SendMessageProtoError had successful sends; no further sends needed. Failing.'
) ;
2022-01-31 21:51:24 +00:00
return true ;
}
for ( const innerError of error . errors ) {
const shouldFail = _shouldFailSend ( innerError , logId ) ;
if ( shouldFail ) {
return true ;
}
}
}
return false ;
}
2023-08-10 16:43:33 +00:00
function getRecipients (
options : GroupSendOptionsType
) : ReadonlyArray < ServiceIdString > {
2021-05-25 22:40:04 +00:00
if ( options . groupV2 ) {
return options . groupV2 . members ;
}
throw new Error ( 'getRecipients: Unable to extract recipients!' ) ;
}
2023-08-10 16:43:33 +00:00
async function markServiceIdUnregistered ( serviceId : ServiceIdString ) {
2021-05-25 22:40:04 +00:00
const conversation = window . ConversationController . getOrCreate (
2023-08-10 16:43:33 +00:00
serviceId ,
2021-05-25 22:40:04 +00:00
'private'
) ;
conversation . setUnregistered ( ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . updateConversation ( conversation . attributes ) ;
2021-05-25 22:40:04 +00:00
2023-08-10 16:43:33 +00:00
await window . textsecure . storage . protocol . archiveAllSessions ( serviceId ) ;
2021-05-25 22:40:04 +00:00
}
2023-08-16 20:54:39 +00:00
function isServiceIdRegistered ( serviceId : ServiceIdString ) {
2021-05-25 22:40:04 +00:00
const conversation = window . ConversationController . getOrCreate (
2023-08-16 20:54:39 +00:00
serviceId ,
2021-05-25 22:40:04 +00:00
'private'
) ;
const isUnregistered = conversation . isUnregistered ( ) ;
return ! isUnregistered ;
}
2024-09-06 17:52:19 +00:00
async function handle409Response (
sendTarget : SenderKeyTargetType ,
groupSendEndorsementState : GroupSendEndorsementState | null ,
error : HTTPError
) {
const logId = sendTarget . idForLogging ( ) ;
2024-10-02 19:03:10 +00:00
const parsed = safeParseUnknown (
multiRecipient409ResponseSchema ,
error . response
) ;
2021-05-25 22:40:04 +00:00
if ( parsed . success ) {
2022-11-11 04:10:30 +00:00
await waitForAll ( {
2021-05-25 22:40:04 +00:00
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 ) {
2024-09-06 17:52:19 +00:00
await fetchKeysForServiceId (
uuid ,
devices . missingDevices ,
groupSendEndorsementState
) ;
2021-05-25 22:40:04 +00:00
}
// Archive sessions with devices that have been removed
if ( devices . extraDevices && devices . extraDevices . length > 0 ) {
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
2021-09-10 02:38:11 +00:00
2022-11-11 04:10:30 +00:00
await waitForAll ( {
2021-05-25 22:40:04 +00:00
tasks : devices.extraDevices.map ( deviceId = > async ( ) = > {
2021-09-10 02:38:11 +00:00
await window . textsecure . storage . protocol . archiveSession (
2023-08-10 16:43:33 +00:00
new QualifiedAddress ( ourAci , Address . create ( uuid , deviceId ) )
2021-09-10 02:38:11 +00:00
) ;
2021-05-25 22:40:04 +00:00
} ) ,
} ) ;
}
} ) ,
maxConcurrency : 2 ,
} ) ;
} else {
2021-09-17 18:27:53 +00:00
log . error (
2021-05-25 22:40:04 +00:00
` handle409Response/ ${ logId } : Server returned unexpected 409 response ${ JSON . stringify (
parsed . error . flatten ( )
) } `
) ;
throw error ;
}
}
async function handle410Response (
2021-12-10 02:15:59 +00:00
sendTarget : SenderKeyTargetType ,
2024-09-06 17:52:19 +00:00
groupSendEndorsementState : GroupSendEndorsementState | null ,
2021-09-22 00:58:03 +00:00
error : HTTPError
2021-05-25 22:40:04 +00:00
) {
2021-12-10 02:15:59 +00:00
const logId = sendTarget . idForLogging ( ) ;
2021-05-25 22:40:04 +00:00
2024-10-02 19:03:10 +00:00
const parsed = safeParseUnknown (
multiRecipient410ResponseSchema ,
error . response
) ;
2021-05-25 22:40:04 +00:00
if ( parsed . success ) {
2022-11-11 04:10:30 +00:00
await waitForAll ( {
2021-05-25 22:40:04 +00:00
tasks : parsed.data.map ( item = > async ( ) = > {
const { uuid , devices } = item ;
if ( devices . staleDevices && devices . staleDevices . length > 0 ) {
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
2021-09-10 02:38:11 +00:00
2021-05-25 22:40:04 +00:00
// First, archive our existing sessions with these devices
2022-11-11 04:10:30 +00:00
await waitForAll ( {
2021-05-25 22:40:04 +00:00
tasks : devices.staleDevices.map ( deviceId = > async ( ) = > {
2021-09-10 02:38:11 +00:00
await window . textsecure . storage . protocol . archiveSession (
2023-08-10 16:43:33 +00:00
new QualifiedAddress ( ourAci , Address . create ( uuid , deviceId ) )
2021-09-10 02:38:11 +00:00
) ;
2021-05-25 22:40:04 +00:00
} ) ,
} ) ;
// Start new sessions with these devices
2024-09-06 17:52:19 +00:00
await fetchKeysForServiceId (
uuid ,
devices . staleDevices ,
groupSendEndorsementState
) ;
2021-05-25 22:40:04 +00:00
// Forget that we've sent our sender key to these devices, since they've
// been re-registered or re-linked.
2021-12-10 02:15:59 +00:00
const senderKeyInfo = sendTarget . getSenderKeyInfo ( ) ;
2021-05-25 22:40:04 +00:00
if ( senderKeyInfo ) {
2021-11-11 22:43:05 +00:00
const devicesToRemove : Array < PartialDeviceType > =
2023-08-10 16:43:33 +00:00
devices . staleDevices . map ( id = > ( { id , serviceId : uuid } ) ) ;
2021-12-10 02:15:59 +00:00
await sendTarget . saveSenderKeyInfo ( {
. . . senderKeyInfo ,
memberDevices : differenceWith (
2023-08-16 20:54:39 +00:00
senderKeyInfo . memberDevices ,
2021-12-10 02:15:59 +00:00
devicesToRemove ,
partialDeviceComparator
2023-08-16 20:54:39 +00:00
) ,
2021-05-25 22:40:04 +00:00
} ) ;
}
}
} ) ,
maxConcurrency : 2 ,
} ) ;
} else {
2021-09-17 18:27:53 +00:00
log . error (
2021-05-25 22:40:04 +00:00
` handle410Response/ ${ logId } : Server returned unexpected 410 response ${ JSON . stringify (
parsed . error . flatten ( )
) } `
) ;
throw error ;
}
}
2022-10-07 17:02:08 +00:00
function getXorOfAccessKeys (
devices : Array < DeviceType > ,
{ story } : { story? : boolean } = { }
) : Buffer {
2023-08-10 16:43:33 +00:00
const uuids = getServiceIdsFromDevices ( devices ) ;
2021-05-25 22:40:04 +00:00
const result = Buffer . alloc ( ACCESS_KEY_LENGTH ) ;
2021-07-30 18:35:25 +00:00
strictAssert (
2021-05-25 22:40:04 +00:00
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 } `
) ;
}
2022-10-07 17:02:08 +00:00
const accessKey = getAccessKey ( conversation . attributes , { story } ) ;
2021-05-25 22:40:04 +00:00
if ( ! accessKey ) {
throw new Error ( ` getXorOfAccessKeys: No accessKey for UUID ${ uuid } ` ) ;
}
2024-09-06 17:52:19 +00:00
strictAssert (
typeof accessKey === 'string' ,
'Cannot be endorsement in getXorOfAccessKeys'
) ;
2021-05-25 22:40:04 +00:00
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 ( {
2021-05-28 19:11:19 +00:00
contentHint ,
contentMessage ,
2021-05-25 22:40:04 +00:00
devices ,
distributionId ,
groupId ,
} : {
2021-05-28 19:11:19 +00:00
contentHint : number ;
2021-09-24 00:49:05 +00:00
contentMessage : Uint8Array ;
2021-05-25 22:40:04 +00:00
devices : Array < DeviceType > ;
distributionId : string ;
2021-12-10 02:15:59 +00:00
groupId? : string ;
2021-05-25 22:40:04 +00:00
} ) : Promise < Buffer > {
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
2021-05-25 22:40:04 +00:00
const ourDeviceId = window . textsecure . storage . user . getDeviceId ( ) ;
2021-09-10 02:38:11 +00:00
if ( ! ourDeviceId ) {
2021-05-25 22:40:04 +00:00
throw new Error (
'encryptForSenderKey: Unable to fetch our uuid or deviceId'
) ;
}
const sender = ProtocolAddress . new (
2023-08-10 16:43:33 +00:00
ourAci ,
2021-05-25 22:40:04 +00:00
parseIntOrThrow ( ourDeviceId , 'encryptForSenderKey, ourDeviceId' )
) ;
const ourAddress = getOurAddress ( ) ;
2023-08-10 16:43:33 +00:00
const senderKeyStore = new SenderKeys ( {
ourServiceId : ourAci ,
zone : GLOBAL_ZONE ,
} ) ;
2021-09-24 00:49:05 +00:00
const message = Buffer . from ( padMessage ( contentMessage ) ) ;
2021-05-25 22:40:04 +00:00
2021-11-11 22:43:05 +00:00
const ciphertextMessage =
await window . textsecure . storage . protocol . enqueueSenderKeyJob (
2023-08-10 16:43:33 +00:00
new QualifiedAddress ( ourAci , ourAddress ) ,
2021-11-11 22:43:05 +00:00
( ) = > groupEncrypt ( sender , distributionId , senderKeyStore , message )
) ;
2021-05-25 22:40:04 +00:00
2021-12-10 02:15:59 +00:00
const groupIdBuffer = groupId ? Buffer . from ( groupId , 'base64' ) : null ;
2021-05-25 22:40:04 +00:00
const senderCertificateObject = await senderCertificateService . get (
SenderCertificateMode . WithoutE164
) ;
if ( ! senderCertificateObject ) {
2022-02-09 20:33:19 +00:00
throw new Error ( 'encryptForSenderKey: Unable to fetch sender certificate!' ) ;
2021-05-25 22:40:04 +00:00
}
const senderCertificate = SenderCertificate . deserialize (
Buffer . from ( senderCertificateObject . serialized )
) ;
const content = UnidentifiedSenderMessageContent . new (
ciphertextMessage ,
senderCertificate ,
contentHint ,
groupIdBuffer
) ;
2021-08-17 15:45:57 +00:00
const recipients = devices
. slice ( )
. sort ( ( a , b ) : number = > {
2023-08-10 16:43:33 +00:00
if ( a . serviceId === b . serviceId ) {
2021-08-17 15:45:57 +00:00
return 0 ;
}
2023-08-10 16:43:33 +00:00
if ( a . serviceId < b . serviceId ) {
2021-08-17 15:45:57 +00:00
return - 1 ;
}
return 1 ;
} )
2021-09-10 02:38:11 +00:00
. map ( device = > {
2023-08-10 16:43:33 +00:00
return ProtocolAddress . new ( device . serviceId , device . id ) ;
2021-09-10 02:38:11 +00:00
} ) ;
2023-08-10 16:43:33 +00:00
const identityKeyStore = new IdentityKeys ( { ourServiceId : ourAci } ) ;
const sessionStore = new Sessions ( { ourServiceId : ourAci } ) ;
2021-05-25 22:40:04 +00:00
return sealedSenderMultiRecipientEncrypt (
content ,
recipients ,
identityKeyStore ,
sessionStore
) ;
}
function isValidSenderKeyRecipient (
2021-08-19 15:52:08 +00:00
members : Set < ConversationModel > ,
2024-09-06 17:52:19 +00:00
groupSendEndorsementState : GroupSendEndorsementState | null ,
2023-08-16 20:54:39 +00:00
serviceId : ServiceIdString ,
2022-10-07 17:02:08 +00:00
{ story } : { story? : boolean } = { }
2021-05-25 22:40:04 +00:00
) : boolean {
2023-08-16 20:54:39 +00:00
const memberConversation = window . ConversationController . get ( serviceId ) ;
2021-05-25 22:40:04 +00:00
if ( ! memberConversation ) {
2021-09-17 18:27:53 +00:00
log . warn (
2023-08-16 20:54:39 +00:00
` isValidSenderKeyRecipient: Missing conversation model for member ${ serviceId } `
2021-05-25 22:40:04 +00:00
) ;
return false ;
}
2021-08-19 15:52:08 +00:00
if ( ! members . has ( memberConversation ) ) {
2021-09-17 18:27:53 +00:00
log . info (
2023-08-16 20:54:39 +00:00
` isValidSenderKeyRecipient: Sending to ${ serviceId } , not a group member `
2021-08-19 15:52:08 +00:00
) ;
return false ;
}
2024-09-06 17:52:19 +00:00
if ( groupSendEndorsementState != null ) {
2024-09-16 19:37:38 +00:00
if ( ! groupSendEndorsementState . hasMember ( serviceId ) ) {
2024-09-06 17:52:19 +00:00
onFailedToSendWithEndorsements (
new Error (
` isValidSenderKeyRecipient: Sending to ${ serviceId } , missing endorsement `
)
) ;
return false ;
}
} else if ( ! getAccessKey ( memberConversation . attributes , { story } ) ) {
2021-05-25 22:40:04 +00:00
return false ;
}
if ( memberConversation . isUnregistered ( ) ) {
2023-08-16 20:54:39 +00:00
log . warn ( ` isValidSenderKeyRecipient: Member ${ serviceId } is unregistered ` ) ;
2021-05-25 22:40:04 +00:00
return false ;
}
return true ;
}
function deviceComparator ( left? : DeviceType , right? : DeviceType ) : boolean {
2021-07-30 18:35:25 +00:00
return Boolean (
left &&
right &&
left . id === right . id &&
2023-08-10 16:43:33 +00:00
left . serviceId === right . serviceId &&
2021-07-30 18:35:25 +00:00
left . registrationId === right . registrationId
) ;
}
type PartialDeviceType = Omit < DeviceType , ' registrationId ' > ;
function partialDeviceComparator (
left? : PartialDeviceType ,
right? : PartialDeviceType
) : boolean {
2021-05-25 22:40:04 +00:00
return Boolean (
2023-08-10 16:43:33 +00:00
left && right && left . id === right . id && left . serviceId === right . serviceId
2021-05-25 22:40:04 +00:00
) ;
}
2023-08-10 16:43:33 +00:00
function getServiceIdsFromDevices (
2022-07-08 20:46:25 +00:00
devices : Array < DeviceType >
2023-08-10 16:43:33 +00:00
) : Array < ServiceIdString > {
return [ . . . new Set ( devices . map ( ( { serviceId } ) = > serviceId ) ) ] ;
2021-05-25 22:40:04 +00:00
}
export function _analyzeSenderKeyDevices (
memberDevices : Array < DeviceType > ,
devicesForSend : Array < DeviceType > ,
isPartialSend? : boolean
) : {
newToMemberDevices : Array < DeviceType > ;
2023-08-10 16:43:33 +00:00
newToMemberServiceIds : Array < ServiceIdString > ;
2021-05-25 22:40:04 +00:00
removedFromMemberDevices : Array < DeviceType > ;
2023-08-10 16:43:33 +00:00
removedFromMemberServiceIds : Array < ServiceIdString > ;
2021-05-25 22:40:04 +00:00
} {
const newToMemberDevices = differenceWith < DeviceType , DeviceType > (
devicesForSend ,
memberDevices ,
deviceComparator
) ;
2023-08-10 16:43:33 +00:00
const newToMemberServiceIds = getServiceIdsFromDevices ( newToMemberDevices ) ;
2021-05-25 22:40:04 +00:00
// If this is a partial send, we won't do anything with device removals
if ( isPartialSend ) {
return {
newToMemberDevices ,
2023-08-10 16:43:33 +00:00
newToMemberServiceIds ,
2021-05-25 22:40:04 +00:00
removedFromMemberDevices : [ ] ,
2023-08-10 16:43:33 +00:00
removedFromMemberServiceIds : [ ] ,
2021-05-25 22:40:04 +00:00
} ;
}
const removedFromMemberDevices = differenceWith < DeviceType , DeviceType > (
memberDevices ,
devicesForSend ,
deviceComparator
) ;
2023-08-10 16:43:33 +00:00
const removedFromMemberServiceIds = getServiceIdsFromDevices (
removedFromMemberDevices
) ;
2021-05-25 22:40:04 +00:00
return {
newToMemberDevices ,
2023-08-10 16:43:33 +00:00
newToMemberServiceIds ,
2021-05-25 22:40:04 +00:00
removedFromMemberDevices ,
2023-08-10 16:43:33 +00:00
removedFromMemberServiceIds ,
2021-05-25 22:40:04 +00:00
} ;
}
2021-09-10 02:38:11 +00:00
function getOurAddress ( ) : Address {
2023-08-10 16:43:33 +00:00
const ourAci = window . textsecure . storage . user . getCheckedAci ( ) ;
2021-05-25 22:40:04 +00:00
const ourDeviceId = window . textsecure . storage . user . getDeviceId ( ) ;
2021-09-10 02:38:11 +00:00
if ( ! ourDeviceId ) {
throw new Error ( 'getOurAddress: Unable to fetch our deviceId' ) ;
2021-05-25 22:40:04 +00:00
}
2023-08-10 16:43:33 +00:00
return new Address ( ourAci , ourDeviceId ) ;
2021-05-25 22:40:04 +00:00
}
function getAccessKey (
2022-10-07 17:02:08 +00:00
attributes : ConversationAttributesType ,
{ story } : { story? : boolean }
2024-09-06 17:52:19 +00:00
) : string | null {
2021-05-25 22:40:04 +00:00
const { sealedSender , accessKey } = attributes ;
2022-10-07 17:02:08 +00:00
if ( story ) {
return accessKey || ZERO_ACCESS_KEY ;
}
2021-08-04 01:02:35 +00:00
if ( sealedSender === SEALED_SENDER . ENABLED ) {
2024-09-06 17:52:19 +00:00
return accessKey || null ;
2021-08-04 01:02:35 +00:00
}
2024-06-10 23:23:10 +00:00
if ( sealedSender === SEALED_SENDER . UNKNOWN ) {
return accessKey || ZERO_ACCESS_KEY ;
}
if ( sealedSender === SEALED_SENDER . UNRESTRICTED ) {
2021-08-04 01:02:35 +00:00
return ZERO_ACCESS_KEY ;
2021-05-25 22:40:04 +00:00
}
2024-09-06 17:52:19 +00:00
return null ;
2021-05-25 22:40:04 +00:00
}
2023-08-10 16:43:33 +00:00
async function fetchKeysForServiceIds (
2024-09-06 17:52:19 +00:00
serviceIds : Array < ServiceIdString > ,
groupSendEndorsementState : GroupSendEndorsementState | null
2021-05-25 22:40:04 +00:00
) : Promise < void > {
2021-09-17 18:27:53 +00:00
log . info (
2023-08-10 16:43:33 +00:00
` fetchKeysForServiceIds: Fetching keys for ${ serviceIds . length } serviceIds `
2021-05-25 22:40:04 +00:00
) ;
try {
2022-11-11 04:10:30 +00:00
await waitForAll ( {
2023-08-10 16:43:33 +00:00
tasks : serviceIds.map (
2024-09-06 17:52:19 +00:00
serviceId = > async ( ) = >
fetchKeysForServiceId ( serviceId , null , groupSendEndorsementState )
2021-05-25 22:40:04 +00:00
) ,
} ) ;
} catch ( error ) {
2021-09-17 18:27:53 +00:00
log . error (
2023-08-10 16:43:33 +00:00
'fetchKeysForServiceIds: Failed to fetch keys:' ,
2022-11-22 18:43:43 +00:00
Errors . toLogFormat ( error )
2021-05-25 22:40:04 +00:00
) ;
2021-11-20 15:58:38 +00:00
throw error ;
2021-05-25 22:40:04 +00:00
}
}
2023-08-10 16:43:33 +00:00
async function fetchKeysForServiceId (
serviceId : ServiceIdString ,
2024-09-06 17:52:19 +00:00
devices : Array < number > | null ,
groupSendEndorsementState : GroupSendEndorsementState | null
2021-05-25 22:40:04 +00:00
) : Promise < void > {
2021-09-17 18:27:53 +00:00
log . info (
2023-08-10 16:43:33 +00:00
` fetchKeysForServiceId: Fetching ${
2021-05-25 22:40:04 +00:00
devices || 'all'
2023-08-10 16:43:33 +00:00
} devices for $ { serviceId } `
2021-05-25 22:40:04 +00:00
) ;
if ( ! window . textsecure ? . messaging ? . server ) {
2023-08-10 16:43:33 +00:00
throw new Error ( 'fetchKeysForServiceId: No server available!' ) ;
2021-05-25 22:40:04 +00:00
}
const emptyConversation = window . ConversationController . getOrCreate (
2023-08-10 16:43:33 +00:00
serviceId ,
2021-05-25 22:40:04 +00:00
'private'
) ;
2024-09-16 19:37:38 +00:00
let useGroupSendEndorsement = isAciString ( serviceId ) ;
if ( ! groupSendEndorsementState ? . hasMember ( serviceId ) ) {
log . error ( ` fetchKeysForServiceId: ${ serviceId } does not have endorsements ` ) ;
useGroupSendEndorsement = false ;
}
2021-05-25 22:40:04 +00:00
try {
2023-01-01 11:41:40 +00:00
// Note: we have no way to make an unrestricted unauthenticated key fetch as part of a
2022-10-07 17:02:08 +00:00
// story send, so we hardcode story=false.
2024-09-06 17:52:19 +00:00
const accessKey = getAccessKey ( emptyConversation . attributes , {
story : false ,
} ) ;
2024-09-16 19:37:38 +00:00
let groupSendToken : GroupSendToken | null = null ;
if ( useGroupSendEndorsement && groupSendEndorsementState != null ) {
groupSendToken = groupSendEndorsementState . buildToken (
new Set ( [ serviceId ] )
) ;
}
2024-09-06 17:52:19 +00:00
2023-08-10 16:43:33 +00:00
const { accessKeyFailed } = await getKeysForServiceId (
serviceId ,
2021-05-25 22:40:04 +00:00
window . textsecure ? . messaging ? . server ,
devices ,
2024-09-06 17:52:19 +00:00
accessKey ,
groupSendToken
2021-05-25 22:40:04 +00:00
) ;
if ( accessKeyFailed ) {
2021-09-17 18:27:53 +00:00
log . info (
2023-08-10 16:43:33 +00:00
` fetchKeysForServiceIds: Setting sealedSender to DISABLED for conversation ${ emptyConversation . idForLogging ( ) } `
2021-05-25 22:40:04 +00:00
) ;
emptyConversation . set ( {
sealedSender : SEALED_SENDER.DISABLED ,
} ) ;
2024-07-22 18:16:33 +00:00
await DataWriter . updateConversation ( emptyConversation . attributes ) ;
2021-05-25 22:40:04 +00:00
}
2022-02-25 23:39:24 +00:00
} catch ( error : unknown ) {
if ( error instanceof UnregisteredUserError ) {
2023-08-10 16:43:33 +00:00
await markServiceIdUnregistered ( serviceId ) ;
2021-05-25 22:40:04 +00:00
return ;
}
2024-09-17 23:11:25 +00:00
if ( useGroupSendEndorsement ) {
onFailedToSendWithEndorsements ( error as Error ) ;
}
2022-12-03 01:05:27 +00:00
log . error (
2023-08-10 16:43:33 +00:00
` fetchKeysForServiceId: Error fetching ${
2022-12-03 01:05:27 +00:00
devices || 'all'
2023-08-10 16:43:33 +00:00
} devices for $ { serviceId } ` ,
2022-12-03 01:05:27 +00:00
Errors . toLogFormat ( error )
) ;
2021-05-25 22:40:04 +00:00
throw error ;
}
}