import { StatusCodes as HTTP_STATUS } from 'http-status-codes' ; import { AxiosError } from 'axios' ; import { proteus as ProtobufOTR } from '@wireapp/protocol-messaging/web/otr' ; import Long from 'long' ; import { uuidToBytes } from '@wireapp/commons/src/main/util/StringUtil' ; import { APIClient } from '@wireapp/api-client' ; import { ClientMismatch , MessageSendingStatus , NewOTRMessage , OTRRecipients , QualifiedOTRRecipients , QualifiedUserClients , UserClients , } from '@wireapp/api-client/src/conversation' ; import { Encoder } from 'bazinga64' ; import { encryptAsset } from '../../cryptography/AssetCryptography' ; import { CryptographyService } from '../../cryptography' ; import { QualifiedId , QualifiedUserPreKeyBundleMap , UserPreKeyBundleMap } from '@wireapp/api-client/src/user' ; import { MessageBuilder } from './MessageBuilder' ; import { GenericMessage } from '@wireapp/protocol-messaging' ; import { GenericMessageType } from '..' ; import { flattenUserClients , flattenQualifiedUserClients } from './UserClientsUtil' ; import { isQualifiedIdArray , isStringArray } from '../../util' ; type ClientMismatchError = AxiosError < ClientMismatch | MessageSendingStatus > ; export class MessageService { constructor ( private readonly apiClient , private readonly cryptographyService ) { } param sendingClientId param recipients param plainText param options . conversationId param options . reportMissing param options . sendAsProtobuf param options . onClientMismatch return public async sendMessage ( sendingClientId , recipients , plainText , options = { } ) { let plainTextPayload = plainText ; let cipherText ; if ( this . shouldSendAsExternal ( plainText , recipients ) ) { const externalPayload = await this . generateExternalPayload ( plainText ) ; plainTextPayload = externalPayload . text ; cipherText = externalPayload . cipherText ; } const { encrypted , missing } = await this . cryptographyService . encrypt ( plainTextPayload , recipients ) ; const encryptedPayload = Object . keys ( missing ) . length ? await this . reencryptAfterMismatch ( { missing , deleted : { } } , encrypted , plainText ) : encrypted ; const send = ( payload ) => { return options . sendAsProtobuf ? this . sendOTRProtobufMessage ( sendingClientId , payload , { ... options , assetData : cipherText } ) : this . sendOTRMessage ( sendingClientId , payload , { ... options , assetData : cipherText } ) ; } ; try { return await send ( encryptedPayload ) ; } catch ( error ) { if ( ! this . isClientMismatchError ( error ) ) { throw error ; } const mismatch = error . response ! . data as ClientMismatch ; const shouldStopSending = options . onClientMismatch && ( await options . onClientMismatch ( mismatch ) ) === false ; if ( shouldStopSending ) { return { ... mismatch , errored : true } ; } const reEncryptedMessage = await this . reencryptAfterMismatch ( mismatch , encryptedPayload , plainText ) ; return send ( reEncryptedMessage ) ; } } param sendingClientId param recipients param plainText param options . conversationId param options . reportMissing param options . sendAsProtobuf param options . onClientMismatch return public async sendFederatedMessage ( sendingClientId , recipients , plainText , options ) { const send = ( payload ) => { return this . sendFederatedOtrMessage ( sendingClientId , payload , options ) ; } ; const { encrypted , missing } = await this . cryptographyService . encryptQualified ( plainText , recipients ) ; const encryptedPayload = Object . keys ( missing ) . length ? await this . reencryptAfterFederatedMismatch ( { missing , deleted : { } } , encrypted , plainText ) : encrypted ; try { return await send ( encryptedPayload ) ; } catch ( error ) { if ( ! this . isClientMismatchError ( error ) ) { throw error ; } const mismatch = error . response ! . data as MessageSendingStatus ; const shouldStopSending = options . onClientMismatch && ( await options . onClientMismatch ( mismatch ) ) === false ; if ( shouldStopSending ) { return { ... mismatch , errored : true } ; } const reEncryptedPayload = await this . reencryptAfterFederatedMismatch ( mismatch , encryptedPayload , plainText ) ; return send ( reEncryptedPayload ) ; } } private async sendFederatedOtrMessage ( sendingClientId , recipients , options ) { const qualifiedUserEntries = Object . entries ( recipients ) . map < ProtobufOTR . IQualifiedUserEntry > ( ( [ domain , otrRecipients ] ) => { const userEntries = Object . entries ( otrRecipients ) . map < ProtobufOTR . IUserEntry > ( ( [ userId , otrClientMap ] ) => { const clientEntries = Object . entries ( otrClientMap ) . map < ProtobufOTR . IClientEntry > ( ( [ clientId , payload ] ) => { return { client : { client : Long . fromString ( clientId , 16 ) , } , text : payload , } ; } ) ; return { user : { uuid : uuidToBytes ( userId ) , } , clients : clientEntries , } ; } ) ; return { domain , entries : userEntries } ; } ) ; const protoMessage = ProtobufOTR . QualifiedNewOtrMessage . create ( { recipients : qualifiedUserEntries , sender : { client : Long . fromString ( sendingClientId , 16 ) , } , nativePush : options . nativePush , } ) ; if ( options . assetData ) { protoMessage . blob = options . assetData ; } if ( options . reportMissing ) { if ( isQualifiedIdArray ( options . reportMissing ) ) { protoMessage . reportOnly = { userIds : options . reportMissing } ; } else { protoMessage . reportAll = true ; } } else { protoMessage . ignoreAll = true ; } if ( ! options . conversationId ) { throw new Error ( 'Unimplemented federated broadcast' ) ; } const { id , domain } = options . conversationId ; return this . apiClient . conversation . api . postOTRMessageV2 ( id , domain , protoMessage ) ; } private async sendOTRMessage ( sendingClientId , recipients , options ) { const message = { data : options . assetData ? Encoder . toBase64 ( options . assetData ) . asString : undefined , recipients : CryptographyService . convertArrayRecipientsToBase64 ( recipients ) , sender : sendingClientId , native_push : options . nativePush , } ; let ignoreMissing ; if ( isStringArray ( options . reportMissing ) ) { message . report_missing = options . reportMissing ; } else { ignoreMissing = typeof options . reportMissing === 'boolean' ? ! options . reportMissing : false ; } return ! options . conversationId ? this . apiClient . broadcast . api . postBroadcastMessage ( sendingClientId , message , ignoreMissing ) : this . apiClient . conversation . api . postOTRMessage ( sendingClientId , options . conversationId , message , ignoreMissing ) ; } private async generateExternalPayload ( plainText ) { const asset = await encryptAsset ( { plainText } ) ; const { cipherText , keyBytes , sha256 } = asset ; const messageId = MessageBuilder . createId ( ) ; const externalMessage = { otrKey : new Uint8Array ( keyBytes ) , sha256 : new Uint8Array ( sha256 ) , } ; const genericMessage = GenericMessage . create ( { [ GenericMessageType . EXTERNAL ] : externalMessage , messageId , } ) ; return { text : GenericMessage . encode ( genericMessage ) . finish ( ) , cipherText } ; } private shouldSendAsExternal ( plainText , preKeyBundles ) { const EXTERNAL_MESSAGE_THRESHOLD_BYTES = 200 * 1024 ; let clientCount = 0 ; for ( const user in preKeyBundles ) { clientCount += Object . keys ( preKeyBundles [ user ] ) . length ; } const messageInBytes = new Uint8Array ( plainText ) . length ; const estimatedPayloadInBytes = clientCount * messageInBytes ; return estimatedPayloadInBytes > EXTERNAL_MESSAGE_THRESHOLD_BYTES ; } private isClientMismatchError ( error ) { return error . response ?. status === HTTP_STATUS . PRECONDITION_FAILED ; } private async reencryptAfterMismatch ( mismatch , recipients , plainText ) { const deleted = flattenUserClients ( mismatch . deleted ) ; const missing = flattenUserClients ( mismatch . missing ) ; deleted . forEach ( ( { userId , data } ) => data . forEach ( clientId => delete recipients [ userId . id ] [ clientId ] ) ) ; if ( missing . length ) { const missingPreKeyBundles = await this . apiClient . user . api . postMultiPreKeyBundles ( mismatch . missing ) ; const { encrypted } = await this . cryptographyService . encrypt ( plainText , missingPreKeyBundles ) ; const reEncryptedPayloads = flattenUserClients < { [ client ] : Uint8Array ; } > ( encrypted ) ; reEncryptedPayloads . forEach ( ( { data , userId } ) => ( recipients [ userId . id ] = { ... recipients [ userId . id ] , ... data } ) ) ; } return recipients ; } param { ProtobufOTR . QualifiedNewOtrMessage } messageData param { MessageSendingStatus } messageSendingStatus param { Uint8Array } plainText return private async reencryptAfterFederatedMismatch ( mismatch , recipients , plainText ) { const deleted = flattenQualifiedUserClients ( mismatch . deleted ) ; const missing = flattenQualifiedUserClients ( mismatch . missing ) ; deleted . forEach ( ( { userId , data } ) => data . forEach ( clientId => delete recipients [ userId . domain ] [ userId . id ] [ clientId ] ) ) ; if ( Object . keys ( missing ) . length ) { const missingPreKeyBundles = await this . apiClient . user . api . postQualifiedMultiPreKeyBundles ( mismatch . missing ) ; const { encrypted } = await this . cryptographyService . encryptQualified ( plainText , missingPreKeyBundles ) ; const reEncryptedPayloads = flattenQualifiedUserClients < { [ client ] : Uint8Array ; } > ( encrypted ) ; reEncryptedPayloads . forEach ( ( { data , userId } ) => ( recipients [ userId . domain ] [ userId . id ] = { ... recipients [ userId . domain ] [ userId . id ] , ... data } ) ) ; } return recipients ; } private async sendOTRProtobufMessage ( sendingClientId , recipients , options ) { const userEntries = Object . entries ( recipients ) . map ( ( [ userId , otrClientMap ] ) => { const clients = Object . entries ( otrClientMap ) . map ( ( [ clientId , payload ] ) => { return { client : { client : Long . fromString ( clientId , 16 ) , } , text : payload , } ; } ) ; return { clients , user : { uuid : uuidToBytes ( userId ) , } , } ; } ) ; const protoMessage = ProtobufOTR . NewOtrMessage . create ( { recipients : userEntries , sender : { client : Long . fromString ( sendingClientId , 16 ) , } , } ) ; let ignoreMissing ; if ( isStringArray ( options . reportMissing ) ) { const encoder = new TextEncoder ( ) ; protoMessage . reportMissing = options . reportMissing . map ( userId => ( { uuid : encoder . encode ( userId ) } ) ) ; } else { ignoreMissing = typeof options . reportMissing === 'boolean' ? ! options . reportMissing : false ; } if ( options . assetData ) { protoMessage . blob = options . assetData ; } return ! options . conversationId ? this . apiClient . broadcast . api . postBroadcastProtobufMessage ( sendingClientId , protoMessage , ignoreMissing ) : this . apiClient . conversation . api . postOTRProtobufMessage ( sendingClientId , options . conversationId , protoMessage , ignoreMissing ) ; } }