// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient // MARK: - Message "isXYZ" properties private extension TSOutgoingMessage { var isTransientSKDM: Bool { (self as? OWSOutgoingSenderKeyDistributionMessage)?.isSentOnBehalfOfOnlineMessage ?? false } var isResendRequest: Bool { self is OWSOutgoingResendRequest } var isSyncMessage: Bool { self is OWSOutgoingSyncMessage } var canSendToLocalAddress: Bool { return (isSyncMessage || self is OWSOutgoingCallMessage || self is OWSOutgoingResendRequest || self is OWSOutgoingResendResponse) } } // MARK: - MessageSender public class MessageSender: Dependencies { private var preKeyManager: PreKeyManager { DependenciesBridge.shared.preKeyManager } public init() { SwiftSingletons.register(self) } private let pendingTasks = PendingTasks(label: "Message Sends") public func pendingSendsPromise() -> Promise { // This promise blocks on all operations already in the queue, // but will not block on new operations added after this promise // is created. That's intentional to ensure that NotificationService // instances complete in a timely way. pendingTasks.pendingTasksPromise() } // MARK: - Creating Signal Protocol Sessions private func containsValidSession(for serviceId: ServiceId, deviceId: UInt32, tx: DBReadTransaction) throws -> Bool { let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore do { guard let session = try sessionStore.loadSession(for: serviceId, deviceId: deviceId, tx: tx) else { return false } return session.hasCurrentState } catch { switch error { case RecipientIdError.mustNotUsePniBecauseAciExists: throw error default: return false } } } /// Establishes a session with the recipient if one doesn't already exist. private func ensureRecipientHasSession( recipientId: AccountId, serviceId: ServiceId, deviceId: UInt32, isOnlineMessage: Bool, isTransientSenderKeyDistributionMessage: Bool, isStoryMessage: Bool, udAccess: OWSUDAccess? ) async throws { let hasSession = try databaseStorage.read { tx in try containsValidSession(for: serviceId, deviceId: deviceId, tx: tx.asV2Read) } if hasSession { return } let preKeyBundle = try await makePrekeyRequest( recipientId: recipientId, serviceId: serviceId, deviceId: deviceId, isOnlineMessage: isOnlineMessage, isTransientSenderKeyDistributionMessage: isTransientSenderKeyDistributionMessage, isStoryMessage: isStoryMessage, udAccess: udAccess ) try await databaseStorage.awaitableWrite { tx in try self.createSession( for: preKeyBundle, recipientId: recipientId, serviceId: serviceId, deviceId: deviceId, transaction: tx ) } } private func makePrekeyRequest( recipientId: AccountId?, serviceId: ServiceId, deviceId: UInt32, isOnlineMessage: Bool, isTransientSenderKeyDistributionMessage: Bool, isStoryMessage: Bool, udAccess: OWSUDAccess? ) async throws -> SignalServiceKit.PreKeyBundle { Logger.info("serviceId: \(serviceId).\(deviceId)") if deviceRecentlyReportedMissing(serviceId: serviceId, deviceId: deviceId) { // We don't want to retry prekey requests if we've recently gotten a "404 // missing device" for the same recipient/device. Fail immediately as // though we hit the "404 missing device" error again. Logger.info("Skipping prekey request to avoid missing device error.") throw MessageSenderError.missingDevice } // As an optimization, skip the request if an error is likely. if let recipientId, willLikelyHaveUntrustedIdentityKeyError(for: recipientId) { Logger.info("Skipping prekey request due to untrusted identity.") throw UntrustedIdentityError(serviceId: serviceId) } if let recipientId, willLikelyHaveInvalidKeySignatureError(for: recipientId) { Logger.info("Skipping prekey request due to invalid prekey signature.") // Check if this error is happening repeatedly for this recipientId. // If so, return an InvalidKeySignatureError as a terminal failure. throw InvalidKeySignatureError(serviceId: serviceId, isTerminalFailure: true) } if isOnlineMessage || isTransientSenderKeyDistributionMessage { Logger.info("Skipping prekey request for transient message") throw MessageSenderNoSessionForTransientMessageError() } let requestMaker = RequestMaker( label: "Prekey Fetch", requestFactoryBlock: { (udAccessKeyForRequest: SMKUDAccessKey?) -> TSRequest? in return OWSRequestFactory.recipientPreKeyRequest( withServiceId: ServiceIdObjC.wrapValue(serviceId), deviceId: deviceId, udAccessKey: udAccessKeyForRequest ) }, serviceId: serviceId, // Don't use UD for story preKey fetches, we don't have a valid UD auth key // TODO: (PreKey Cleanup) udAccess: isStoryMessage ? nil : udAccess, authedAccount: .implicit(), options: [] ) do { let result = try await requestMaker.makeRequest().awaitable() guard let responseObject = result.responseJson as? [String: Any] else { throw OWSAssertionError("Prekey fetch missing response object.") } guard let bundle = SignalServiceKit.PreKeyBundle(from: responseObject, forDeviceNumber: NSNumber(value: deviceId)) else { throw OWSAssertionError("Prekey fetch returned an invalid bundle.") } return bundle } catch { switch error.httpStatusCode { case 404: self.reportMissingDeviceError(serviceId: serviceId, deviceId: deviceId) throw MessageSenderError.missingDevice case 413, 429: throw MessageSenderError.prekeyRateLimit case 428: // SPAM TODO: Only retry messages with -hasRenderableContent try await spamChallengeResolver.tryToHandleSilently( bodyData: error.httpResponseData, retryAfter: error.httpRetryAfterDate ) // The resolver has 10s to asynchronously resolve a challenge. If it // resolves, great! We'll let MessageSender auto-retry. Otherwise, it'll be // marked as "pending". throw SpamChallengeResolvedError() default: throw error } } } private func createSession( for preKeyBundle: SignalServiceKit.PreKeyBundle, recipientId: String, serviceId: ServiceId, deviceId: UInt32, transaction: SDSAnyWriteTransaction ) throws { assert(!Thread.isMainThread) Logger.info("Creating session for \(serviceId), deviceId: \(deviceId)") if try containsValidSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write) { Logger.warn("Session already exists.") return } let bundle: LibSignalClient.PreKeyBundle if preKeyBundle.preKeyPublic.isEmpty { if preKeyBundle.pqPreKeyPublic.isEmpty { Logger.info("Creating prekey bundle with signed prekey (\(preKeyBundle.signedPreKeyId))") bundle = try LibSignalClient.PreKeyBundle( registrationId: UInt32(bitPattern: preKeyBundle.registrationId), deviceId: UInt32(bitPattern: preKeyBundle.deviceId), signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId), signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic), signedPrekeySignature: preKeyBundle.signedPreKeySignature, identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey)) } else { Logger.info("Creating prekey bundle with signed (\(preKeyBundle.signedPreKeyId)) and pq (\(preKeyBundle.pqPreKeyId)) prekey") bundle = try LibSignalClient.PreKeyBundle( registrationId: UInt32(bitPattern: preKeyBundle.registrationId), deviceId: UInt32(bitPattern: preKeyBundle.deviceId), signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId), signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic), signedPrekeySignature: preKeyBundle.signedPreKeySignature, identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey), kyberPrekeyId: UInt32(bitPattern: preKeyBundle.pqPreKeyId), kyberPrekey: try KEMPublicKey(preKeyBundle.pqPreKeyPublic), kyberPrekeySignature: preKeyBundle.pqPreKeySignature ) } } else { if preKeyBundle.pqPreKeyPublic.isEmpty { Logger.info("Creating prekey bundle with signed (\(preKeyBundle.signedPreKeyId)) and one-time (\(preKeyBundle.preKeyId)) prekey") bundle = try LibSignalClient.PreKeyBundle( registrationId: UInt32(bitPattern: preKeyBundle.registrationId), deviceId: UInt32(bitPattern: preKeyBundle.deviceId), prekeyId: UInt32(bitPattern: preKeyBundle.preKeyId), prekey: try PublicKey(preKeyBundle.preKeyPublic), signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId), signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic), signedPrekeySignature: preKeyBundle.signedPreKeySignature, identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey)) } else { Logger.info("Creating prekey bundle with signed (\(preKeyBundle.signedPreKeyId)) and one-time (\(preKeyBundle.preKeyId)) and pq \(preKeyBundle.pqPreKeyId) prekey") bundle = try LibSignalClient.PreKeyBundle( registrationId: UInt32(bitPattern: preKeyBundle.registrationId), deviceId: UInt32(bitPattern: preKeyBundle.deviceId), prekeyId: UInt32(bitPattern: preKeyBundle.preKeyId), prekey: try PublicKey(preKeyBundle.preKeyPublic), signedPrekeyId: UInt32(bitPattern: preKeyBundle.signedPreKeyId), signedPrekey: try PublicKey(preKeyBundle.signedPreKeyPublic), signedPrekeySignature: preKeyBundle.signedPreKeySignature, identity: try LibSignalClient.IdentityKey(bytes: preKeyBundle.identityKey), kyberPrekeyId: UInt32(bitPattern: preKeyBundle.pqPreKeyId), kyberPrekey: try KEMPublicKey(preKeyBundle.pqPreKeyPublic), kyberPrekeySignature: preKeyBundle.pqPreKeySignature ) } } do { let identityManager = DependenciesBridge.shared.identityManager let protocolAddress = ProtocolAddress(serviceId, deviceId: deviceId) try processPreKeyBundle( bundle, for: protocolAddress, sessionStore: DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore, identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write), context: transaction ) } catch SignalError.untrustedIdentity(_) { Logger.error("Found untrusted identity for \(serviceId)") handleUntrustedIdentityKeyError( serviceId: serviceId, recipientId: recipientId, preKeyBundle: preKeyBundle, transaction: transaction ) throw UntrustedIdentityError(serviceId: serviceId) } catch SignalError.invalidSignature(_) { Logger.error("Invalid key signature for \(serviceId)") // Received this error from the server, so this could either be // an invalid key due to a broken client, or it may be a random // corruption in transit. Mark having encountered an error for // this recipient so later checks can determine if this has happend // more than once and fail early. // The error thrown here is considered non-terminal which allows // the request to be retried. hadInvalidKeySignatureError(for: recipientId) throw InvalidKeySignatureError(serviceId: serviceId, isTerminalFailure: false) } owsAssertDebug(try containsValidSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write), "Couldn't create session.") } // MARK: - Untrusted Identities private let staleIdentityCache = AtomicDictionary(lock: .init()) private func handleUntrustedIdentityKeyError( serviceId: ServiceId, recipientId: AccountId, preKeyBundle: SignalServiceKit.PreKeyBundle, transaction tx: SDSAnyWriteTransaction ) { do { let identityManager = DependenciesBridge.shared.identityManager let newIdentityKey = try IdentityKey(bytes: preKeyBundle.identityKey) identityManager.saveIdentityKey(newIdentityKey, for: serviceId, tx: tx.asV2Write) staleIdentityCache[recipientId] = Date() } catch { owsFailDebug("Error: \(error)") } } private func willLikelyHaveUntrustedIdentityKeyError(for recipientId: AccountId) -> Bool { assert(!Thread.isMainThread) // Prekey rate limits are strict. Therefore, we want to avoid requesting // prekey bundles that can't be processed. After a prekey request, we might // not be able to process it if the new identity key isn't trusted. We // therefore expect all subsequent fetches to fail until that key is // trusted, so we don't bother sending them unless the key is trusted. guard let mostRecentErrorDate = staleIdentityCache[recipientId] else { // We don't have a recent error, so a fetch will probably work. return false } let staleIdentityLifetime = kMinuteInterval * 5 guard abs(mostRecentErrorDate.timeIntervalSinceNow) < staleIdentityLifetime else { // It's been more than five minutes since our last fetch. It's reasonable // to try again, even if we don't think it will work. (This helps us // discover if there's yet another new identity key.) return false } let identityManager = DependenciesBridge.shared.identityManager return databaseStorage.read { tx in guard let recipient = SignalRecipient.anyFetch(uniqueId: recipientId, transaction: tx) else { return false } // Otherwise, skip the request if we don't trust the identity. let untrustedIdentity = identityManager.untrustedIdentityForSending( to: recipient.address, untrustedThreshold: nil, tx: tx.asV2Read ) return untrustedIdentity != nil } } // MARK: - Invalid Signatures private typealias InvalidSignatureCache = [AccountId: InvalidSignatureCacheItem] private struct InvalidSignatureCacheItem { let lastErrorDate: Date let errorCount: UInt32 } private let invalidKeySignatureCache = AtomicValue(InvalidSignatureCache(), lock: .init()) private func hadInvalidKeySignatureError(for recipientId: AccountId) { invalidKeySignatureCache.update { cache in var errorCount: UInt32 = 1 if let mostRecentError = cache[recipientId] { errorCount = mostRecentError.errorCount + 1 } cache[recipientId] = InvalidSignatureCacheItem( lastErrorDate: Date(), errorCount: errorCount ) } } private func willLikelyHaveInvalidKeySignatureError(for recipientId: AccountId) -> Bool { assert(!Thread.isMainThread) // Similar to untrusted identity errors, when an invalid signature for a prekey // is encountered, it will probably be encountered for a while until the // target client rotates prekeys and hopfully fixes the bad signature. // To avoid running into prekey rate limits, remember when an error is // encountered and slow down sending prekey requests for this recipient. // // Additionally, there is always a chance of corruption of the prekey // bundle during data transmission, which would result in an invalid // signature of an otherwise correct bundle. To handle this rare case, // don't begin limiting the prekey request until after encounting the // second bad signature for a particular recipient. guard let mostRecentError = invalidKeySignatureCache.get()[recipientId] else { return false } let staleIdentityLifetime = kMinuteInterval * 5 guard abs(mostRecentError.lastErrorDate.timeIntervalSinceNow) < staleIdentityLifetime else { // Error has expired, remove it to reset the count invalidKeySignatureCache.update { cache in _ = cache.removeValue(forKey: recipientId) } return false } // Let the first error go, only skip starting on the second error guard mostRecentError.errorCount > 1 else { return false } return true } // MARK: - Missing Devices private struct CacheKey: Hashable { let serviceId: ServiceId let deviceId: UInt32 } private let missingDevicesCache = AtomicDictionary(lock: .init()) private func reportMissingDeviceError(serviceId: ServiceId, deviceId: UInt32) { assert(!Thread.isMainThread) guard deviceId == OWSDevice.primaryDeviceId else { // For now, only bother ignoring primary devices. HTTP 404s should cause // the recipient's device list to be updated, so linked devices shouldn't // be a problem. return } let cacheKey = CacheKey(serviceId: serviceId, deviceId: deviceId) missingDevicesCache[cacheKey] = Date() } private func deviceRecentlyReportedMissing(serviceId: ServiceId, deviceId: UInt32) -> Bool { assert(!Thread.isMainThread) // Prekey rate limits are strict. Therefore, we want to avoid requesting // prekey bundles that are missing on the service (404). let cacheKey = CacheKey(serviceId: serviceId, deviceId: deviceId) let recentlyReportedMissingDate = missingDevicesCache[cacheKey] guard let recentlyReportedMissingDate else { return false } // If the "missing device" was recorded more than N minutes ago, try // another prekey fetch. It's conceivable that the recipient has // registered (in the primary device case) or linked to the device (in the // secondary device case). let missingDeviceLifetime = kMinuteInterval * 1 guard abs(recentlyReportedMissingDate.timeIntervalSinceNow) < missingDeviceLifetime else { return false } return true } // MARK: - Sending Attachments public func sendTransientContactSyncAttachment( dataSource: DataSource, thread: TSThread ) async throws { let uploadResult = try await DependenciesBridge.shared.tsResourceUploadManager.uploadTransientAttachment( dataSource: dataSource ) let message = databaseStorage.read { tx in return OWSSyncContactsMessage(uploadedAttachment: uploadResult, thread: thread, tx: tx) } let preparedMessage = PreparedOutgoingMessage.preprepared(contactSyncMessage: message) let result = await Result { try await sendMessage(preparedMessage) } try result.get() } // MARK: - Constructing Message Sends public func sendMessage(_ preparedOutgoingMessage: PreparedOutgoingMessage) async throws { let priority: Operation.QueuePriority = await databaseStorage.awaitableWrite { tx in preparedOutgoingMessage.updateAllUnsentRecipientsAsSending(tx: tx) return preparedOutgoingMessage.sendingQueuePriority(tx: tx) } Logger.info("Sending \(preparedOutgoingMessage)") // We create a PendingTask so we can block on flushing all current message sends. let pendingTask = pendingTasks.buildPendingTask(label: "Message Send") defer { pendingTask.complete() } try await withCheckedThrowingContinuation { continuation in let sendMessageOperation = AwaitableAsyncBlockOperation(completionContinuation: continuation) { try await preparedOutgoingMessage.send(self.sendPreparedMessage(_:)) } sendMessageOperation.queuePriority = priority let uploadOperations = databaseStorage.read { tx in preparedOutgoingMessage.attachmentUploadOperations(tx: tx) } uploadOperations.forEach { uploadOperation in sendMessageOperation.addDependency(uploadOperation) Upload.uploadQueue.addOperation(uploadOperation) } sendingQueue(forUniqueThreadId: preparedOutgoingMessage.uniqueThreadId).addOperation(sendMessageOperation) } } private let sendingQueueMap = AtomicValue<[String: OperationQueue]>([:], lock: .init()) private func sendingQueue(forUniqueThreadId: String) -> OperationQueue { let sendingQueueKey = forUniqueThreadId return sendingQueueMap.update { sendingQueueMap in if let existingQueue = sendingQueueMap[sendingQueueKey] { return existingQueue } let sendingQueue = OperationQueue() sendingQueue.qualityOfService = .userInitiated sendingQueue.maxConcurrentOperationCount = 1 sendingQueue.name = "MessageSender-Chat" sendingQueueMap[sendingQueueKey] = sendingQueue return sendingQueue } } private func waitForPreKeyRotationIfNeeded() async throws { while let taskToWaitFor = preKeyRotationTaskIfNeeded() { try await taskToWaitFor.value } } private let pendingPreKeyRotation = AtomicValue?>(nil, lock: .init()) private func preKeyRotationTaskIfNeeded() -> Task? { return pendingPreKeyRotation.map { existingTask in if let existingTask { return existingTask } let shouldRunPreKeyRotation = databaseStorage.read { tx in preKeyManager.isAppLockedDueToPreKeyUpdateFailures(tx: tx.asV2Read) } if shouldRunPreKeyRotation { Logger.info("Rotating signed pre-key before sending message.") // Retry prekey update every time user tries to send a message while app is // disabled due to prekey update failures. // // Only try to update the signed prekey; updating it is sufficient to // re-enable message sending. return Task { try await self.preKeyManager.rotateSignedPreKeys().value self.pendingPreKeyRotation.set(nil) } } return nil } } // Mark skipped recipients as such. We may skip because: // // * A recipient is no longer in the group. // * A recipient is blocked. // * A recipient is unregistered. // * A recipient does not have the required capability. private func markSkippedRecipients( of message: TSOutgoingMessage, sendingRecipients: [ServiceId], tx: SDSAnyWriteTransaction ) { let skippedRecipients = Set(message.sendingRecipientAddresses()) .subtracting(sendingRecipients.lazy.map { SignalServiceAddress($0) }) for address in skippedRecipients { // Mark this recipient as "skipped". message.update(withSkippedRecipient: address, transaction: tx) } } private func unsentRecipients( of message: TSOutgoingMessage, in thread: TSThread, localIdentifiers: LocalIdentifiers, tx: SDSAnyReadTransaction ) throws -> [SignalServiceAddress] { if message.isSyncMessage { return [localIdentifiers.aciAddress] } if let groupThread = thread as? TSGroupThread { // Send to the intersection of: // // * "sending" recipients of the message. // * members of the group. // // I.e. try to send a message IFF: // // * The recipient was in the group when the message was first tried to be sent. // * The recipient is still in the group. // * The recipient is in the "sending" state. var recipientAddresses = Set() recipientAddresses.formUnion(message.sendingRecipientAddresses()) // Only send to members in the latest known group member list. // If a member has left the group since this message was enqueued, // they should not receive the message. let groupMembership = groupThread.groupModel.groupMembership var currentValidRecipients = groupMembership.fullMembers // ...or latest known list of "additional recipients". // // This is used to send group update messages for v2 groups to // pending members who are not included in .sendingRecipientAddresses(). if GroupManager.shouldMessageHaveAdditionalRecipients(message, groupThread: groupThread) { currentValidRecipients.formUnion(groupMembership.invitedMembers) } currentValidRecipients.remove(localIdentifiers.aciAddress) recipientAddresses.formIntersection(currentValidRecipients) let blockedAddresses = blockingManager.blockedAddresses(transaction: tx) recipientAddresses.subtract(blockedAddresses) return Array(recipientAddresses) } else if let contactAddress = (thread as? TSContactThread)?.contactAddress { // Treat 1:1 sends to blocked contacts as failures. // If we block a user, don't send 1:1 messages to them. The UI // should prevent this from occurring, but in some edge cases // you might, for example, have a pending outgoing message when // you block them. if blockingManager.isAddressBlocked(contactAddress, transaction: tx) { Logger.info("Skipping 1:1 send to blocked contact: \(contactAddress).") throw MessageSenderError.blockedContactRecipient } else { return [contactAddress] } } else { // Send to the intersection of: // // * "sending" recipients of the message. // * recipients of the thread // // I.e. try to send a message IFF: // // * The recipient was part of the thread when the message was first tried to be sent. // * The recipient is still part of the thread. // * The recipient is in the "sending" state. var recipientAddresses = Set(message.sendingRecipientAddresses()) // Only send to members in the latest known thread recipients list. let currentValidThreadRecipients = thread.recipientAddresses(with: tx) recipientAddresses.formIntersection(currentValidThreadRecipients) let blockedAddresses = blockingManager.blockedAddresses(transaction: tx) recipientAddresses.subtract(blockedAddresses) if recipientAddresses.contains(localIdentifiers.aciAddress) { owsFailDebug("Message send recipients should not include self.") } return Array(recipientAddresses) } } private static func partitionAddresses(_ addresses: [SignalServiceAddress]) -> ([ServiceId], [E164]) { var serviceIds = [ServiceId]() var phoneNumbers = [E164]() for address in addresses { if let serviceId = address.serviceId { serviceIds.append(serviceId) } else if let phoneNumber = address.e164 { phoneNumbers.append(phoneNumber) } else { owsFailDebug("Recipient has neither ServiceId nor E164.") } } return (serviceIds, phoneNumbers) } private func lookUpPhoneNumbers(_ phoneNumbers: [E164]) async throws { _ = try await contactDiscoveryManager.lookUp( phoneNumbers: Set(phoneNumbers.lazy.map { $0.stringValue }), mode: .outgoingMessage ).awaitable() } private func areAttachmentsUploadedWithSneakyTransaction(for message: TSOutgoingMessage) -> Bool { return databaseStorage.read { tx in for attachment in message.allAttachments(transaction: tx) { guard attachment.isUploadedToTransitTier else { return false } } return true } } private func sendPreparedMessage(_ message: TSOutgoingMessage) async throws { if !areAttachmentsUploadedWithSneakyTransaction(for: message) { throw OWSUnretryableMessageSenderError() } if DependenciesBridge.shared.appExpiry.isExpired { throw AppExpiredError() } if DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered.negated { throw AppDeregisteredError() } if message.shouldBeSaved { let latestCopy = databaseStorage.read { tx in TSInteraction.anyFetch(uniqueId: message.uniqueId, transaction: tx) as? TSOutgoingMessage } guard let latestCopy, latestCopy.wasRemotelyDeleted.negated else { throw MessageDeletedBeforeSentError() } } if DebugFlags.messageSendsFail.get() { throw OWSUnretryableMessageSenderError() } do { try await waitForPreKeyRotationIfNeeded() let senderCertificates = try await udManager.ensureSenderCertificates(certificateExpirationPolicy: .permissive).awaitable() try await sendPreparedMessage(message, canLookUpPhoneNumbers: true, senderCertificates: senderCertificates) } catch { if message.wasSentToAnyRecipient { // Always ignore the sync error... try? await handleMessageSentLocally(message) } // ...so that we can throw the original error for the caller. (Note that we // throw this error even if the sync message is sent successfully.) throw error } try await handleMessageSentLocally(message) } private enum SendMessageNextAction { /// Look up missing phone numbers & then try sending again. case lookUpPhoneNumbersAndTryAgain([E164]) /// Perform the `sendPreparedMessage` step. case sendPreparedMessage( serializedMessage: SerializedMessage, thread: TSThread, serviceIds: [ServiceId], udAccess: [ServiceId: OWSUDSendingAccess], localIdentifiers: LocalIdentifiers ) } private func sendPreparedMessage( _ message: TSOutgoingMessage, canLookUpPhoneNumbers: Bool, senderCertificates: SenderCertificates ) async throws { let nextAction: SendMessageNextAction? = try await databaseStorage.awaitableWrite { tx in guard let thread = message.thread(tx: tx) else { throw MessageSenderError.threadMissing } let canSendToThread: Bool = { if message is OWSOutgoingReactionMessage { return thread.canSendReactionToThread } let isChatMessage = ( ( message.shouldBeSaved && message.insertedMessageHasRenderableContent(rowId: message.sqliteRowId!, tx: tx) ) || message is OutgoingGroupCallUpdateMessage || message is OWSOutgoingCallMessage ) return isChatMessage ? thread.canSendChatMessagesToThread() : thread.canSendNonChatMessagesToThread }() guard canSendToThread else { if message.shouldBeSaved { throw OWSAssertionError("Sending to thread blocked.") } // Pretend to succeed for non-visible messages like read receipts, etc. return nil } let tsAccountManager = DependenciesBridge.shared.tsAccountManager guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read) else { throw OWSAssertionError("Not registered.") } let proposedAddresses = try self.unsentRecipients(of: message, in: thread, localIdentifiers: localIdentifiers, tx: tx) let (serviceIds, phoneNumbersToFetch) = Self.partitionAddresses(proposedAddresses) // If we haven't yet tried to look up phone numbers, send an asynchronous // request to look up phone numbers, and then try to go through this logic // *again* in a new transaction. Things may change for that subsequent // attempt, and if there's still missing phone numbers at that point, we'll // skip them for this message. if canLookUpPhoneNumbers, !phoneNumbersToFetch.isEmpty { return .lookUpPhoneNumbersAndTryAgain(phoneNumbersToFetch) } self.markSkippedRecipients(of: message, sendingRecipients: serviceIds, tx: tx) if let contactThread = thread as? TSContactThread { // In the "self-send" aka "Note to Self" special case, we only need to send // certain kinds of messages. (In particular, regular data messages are // sent via their implicit sync message only.) if contactThread.contactAddress.isLocalAddress, !message.canSendToLocalAddress { owsAssertDebug(serviceIds.count == 1) Logger.info("Dropping \(type(of: message)) sent to local address (it should be sent by sync message)") // Don't mark self-sent messages as read (or sent) until the sync transcript is sent. return nil } } if serviceIds.isEmpty { // All recipients are already sent or can be skipped. NOTE: We might still // need to send a sync transcript. return nil } guard let serializedMessage = self.buildAndRecordMessage(message, in: thread, tx: tx) else { throw OWSAssertionError("Couldn't build message.") } let senderCertificate: SenderCertificate = { switch self.udManager.phoneNumberSharingMode(tx: tx.asV2Read).orDefault { case .everybody: return senderCertificates.defaultCert case .nobody: return senderCertificates.uuidOnlyCert } }() let udAccessMap = self.fetchSealedSenderAccess( for: serviceIds, message: message, senderCertificate: senderCertificate, localIdentifiers: localIdentifiers, tx: tx ) return .sendPreparedMessage( serializedMessage: serializedMessage, thread: thread, serviceIds: serviceIds, udAccess: udAccessMap, localIdentifiers: localIdentifiers ) } switch nextAction { case .none: return case .lookUpPhoneNumbersAndTryAgain(let phoneNumbers): try await lookUpPhoneNumbers(phoneNumbers) try await sendPreparedMessage(message, canLookUpPhoneNumbers: false, senderCertificates: senderCertificates) case .sendPreparedMessage(let serializedMessage, let thread, let serviceIds, let udAccess, let localIdentifiers): let allErrors = AtomicArray<(serviceId: ServiceId, error: Error)>(lock: .init()) do { try await sendPreparedMessage( message, serializedMessage: serializedMessage, in: thread, to: serviceIds, udAccess: udAccess, localIdentifiers: localIdentifiers, senderCertificates: senderCertificates, sendErrorBlock: { serviceId, error in allErrors.append((serviceId, error)) } ) } catch { // We ignore the thrown error & consult `allErrors` instead. try await handleSendFailure(message: message, thread: thread, perRecipientErrors: allErrors.get()) } } } private func sendPreparedMessage( _ message: TSOutgoingMessage, serializedMessage: SerializedMessage, in thread: TSThread, to serviceIds: [ServiceId], udAccess sendingAccessMap: [ServiceId: OWSUDSendingAccess], localIdentifiers: LocalIdentifiers, senderCertificates: SenderCertificates, sendErrorBlock: @escaping (ServiceId, Error) -> Void ) async throws { // 3. If we have any participants that support sender key, build a promise // for their send. let senderKeyStatus = senderKeyStatus(for: thread, intendedRecipients: serviceIds, udAccessMap: sendingAccessMap) var senderKeyMessagePromise: Promise? var senderKeyServiceIds: [ServiceId] = senderKeyStatus.allSenderKeyParticipants var fanoutServiceIds: [ServiceId] = senderKeyStatus.fanoutParticipants if thread.usesSenderKey, senderKeyServiceIds.count >= 2, message.canSendWithSenderKey { senderKeyMessagePromise = senderKeyMessageSendPromise( message: message, plaintextContent: serializedMessage.plaintextData, payloadId: serializedMessage.payloadId, thread: thread, status: senderKeyStatus, udAccessMap: sendingAccessMap, senderCertificates: senderCertificates, localIdentifiers: localIdentifiers, sendErrorBlock: sendErrorBlock ) } else { senderKeyServiceIds = [] fanoutServiceIds = serviceIds if !message.canSendWithSenderKey { Logger.info("Last sender key send attempt failed for message \(message.timestamp). Fanning out") } } owsAssertDebug(fanoutServiceIds.count + senderKeyServiceIds.count == serviceIds.count) try await withThrowingTaskGroup(of: Void.self, returning: Void.self) { taskGroup in // Perform an "OWSMessageSend" for each non-senderKey recipient. for serviceId in fanoutServiceIds { let messageSend = OWSMessageSend( message: message, plaintextContent: serializedMessage.plaintextData, plaintextPayloadId: serializedMessage.payloadId, thread: thread, serviceId: serviceId, localIdentifiers: localIdentifiers ) let sealedSenderParameters = sendingAccessMap[serviceId].map { SealedSenderParameters(message: message, udSendingAccess: $0) } taskGroup.addTask { do { try await self.performMessageSend(messageSend, sealedSenderParameters: sealedSenderParameters) } catch { sendErrorBlock(serviceId, error) throw error } } } // Also wait for the sender key promise. if let senderKeyMessagePromise { taskGroup.addTask { try await senderKeyMessagePromise.awaitable() } } // Wait for everything to finish, and *then* throw an arbitrary error b/c // the caller doesn't care *what* error is thrown as long as *some* error // is thrown when a problem occurs. var results = [Result]() while let result = await taskGroup.nextResult() { results.append(result) } for result in results { try result.get() } } } private func fetchSealedSenderAccess( for serviceIds: [ServiceId], message: TSOutgoingMessage, senderCertificate: SenderCertificate, localIdentifiers: LocalIdentifiers, tx: SDSAnyReadTransaction ) -> [ServiceId: OWSUDSendingAccess] { if DebugFlags.disableUD.get() { return [:] } var result = [ServiceId: OWSUDSendingAccess]() for serviceId in serviceIds { if localIdentifiers.contains(serviceId: serviceId) { continue } let udAccess = ( message.isStorySend ? udManager.storyUdAccess() : udManager.udAccess(for: serviceId, tx: tx) ) guard let udAccess else { continue } result[serviceId] = OWSUDSendingAccess(udAccess: udAccess, senderCertificate: senderCertificate) } return result } private func handleSendFailure( message: TSOutgoingMessage, thread: TSThread, perRecipientErrors allErrors: [(serviceId: ServiceId, error: Error)] ) async throws { // Some errors should be ignored when sending messages to non 1:1 threads. // See discussion on NSError (MessageSender) category. let shouldIgnoreError = { (error: Error) -> Bool in return !(thread is TSContactThread) && error.shouldBeIgnoredForNonContactThreads } // Record the individual error for each "failed" recipient. await databaseStorage.awaitableWrite { tx in for (serviceId, error) in Dictionary(allErrors, uniquingKeysWith: { _, new in new }) { if shouldIgnoreError(error) { continue } message.update(withFailedRecipient: SignalServiceAddress(serviceId), error: error, transaction: tx) } self.normalizeRecipientStatesIfNeeded(message: message, recipientErrors: allErrors, tx: tx) } let filteredErrors = allErrors.lazy.map { $0.error }.filter { !shouldIgnoreError($0) } // Some errors should never be retried, in order to avoid hitting rate // limits, for example. Unfortunately, since group send retry is // all-or-nothing, we need to fail immediately even if some of the other // recipients had retryable errors. if let fatalError = filteredErrors.first(where: { $0.isFatalError }) { throw fatalError } // If any of the send errors are retryable, we want to retry. Therefore, // prefer to propagate a retryable error. if let retryableError = filteredErrors.first(where: { $0.isRetryable }) { throw retryableError } // Otherwise, if we have any error at all, propagate it. if let anyError = filteredErrors.first { throw anyError } // If we only received errors that we should ignore, consider this send a // success, unless the message could not be sent to any recipient. if message.sentRecipientsCount() == 0 { throw MessageSenderErrorNoValidRecipients() } } private func normalizeRecipientStatesIfNeeded( message: TSOutgoingMessage, recipientErrors: [(serviceId: ServiceId, error: Error)], tx: SDSAnyWriteTransaction ) { guard recipientErrors.contains(where: { switch $0.error { case RecipientIdError.mustNotUsePniBecauseAciExists: return true default: return false } }) else { return } let recipientStateMerger = RecipientStateMerger( recipientDatabaseTable: DependenciesBridge.shared.recipientDatabaseTable, signalServiceAddressCache: signalServiceAddressCache ) message.anyUpdateOutgoingMessage(transaction: tx) { message in recipientStateMerger.normalize(&message.recipientAddressStates, tx: tx.asV2Read) } } /// Sending a reply to a hidden recipient unhides them. But how we /// define "reply" is not inclusive of all outgoing messages. We unhide /// when the message indicates the user's intent to resume association /// with the hidden recipient. /// /// It is important to be conservative about which messages unhide a /// recipient. It is far better to not unhide when should than to /// unhide when we should not. private func shouldMessageSendUnhideRecipient(_ message: TSOutgoingMessage, tx: SDSAnyReadTransaction) -> Bool { if message.shouldBeSaved, let rowId = message.sqliteRowId, // Its a persisted message; check if its renderable message.insertedMessageHasRenderableContent(rowId: rowId, tx: tx) { return true } if message is OWSOutgoingReactionMessage { return true } if let message = message as? OWSOutgoingCallMessage, /// OWSOutgoingCallMessages include not only calling /// someone (ie, an "offer message"), but also sending /// hangup messages, busy messages, and other kinds of /// call-related "messages" that do not indicate the /// sender's intent to resume association with a recipient. message.offerMessage != nil { return true } return false } private func handleMessageSentLocally(_ message: TSOutgoingMessage) async throws { await databaseStorage.awaitableWrite { tx in if let thread = message.thread(tx: tx) as? TSContactThread, self.shouldMessageSendUnhideRecipient(message, tx: tx), let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aciAddress, !localAddress.isEqualToAddress(thread.contactAddress) { DependenciesBridge.shared.recipientHidingManager.removeHiddenRecipient( thread.contactAddress, wasLocallyInitiated: true, tx: tx.asV2Write ) } if message.shouldBeSaved { let latestInteraction = TSInteraction.anyFetch(uniqueId: message.uniqueId, transaction: tx) guard let latestMessage = latestInteraction as? TSOutgoingMessage else { Logger.warn("Could not update expiration for deleted message.") return } ViewOnceMessages.completeIfNecessary(message: latestMessage, transaction: tx) } } try await sendSyncTranscriptIfNeeded(for: message) // Don't mark self-sent messages as read (or sent) until the sync // transcript is sent. // // NOTE: This only applies to the 'note to self' conversation. if message.isSyncMessage { return } let thread = databaseStorage.read { tx in message.thread(tx: tx) } guard let contactThread = thread as? TSContactThread, contactThread.contactAddress.isLocalAddress else { return } owsAssertDebug(message.recipientAddresses().count == 1) await databaseStorage.awaitableWrite { tx in let deviceId = DependenciesBridge.shared.tsAccountManager.storedDeviceId(tx: tx.asV2Read) for sendingAddress in message.sendingRecipientAddresses() { message.update( withReadRecipient: sendingAddress, deviceId: deviceId, readTimestamp: message.timestamp, tx: tx ) if message.isVoiceMessage || message.isViewOnceMessage { message.update( withViewedRecipient: sendingAddress, deviceId: deviceId, viewedTimestamp: message.timestamp, tx: tx ) } } } } private func sendSyncTranscriptIfNeeded(for message: TSOutgoingMessage) async throws { guard message.shouldSyncTranscript() else { return } try await message.sendSyncTranscript() await databaseStorage.awaitableWrite { tx in message.update(withHasSyncedTranscript: true, transaction: tx) } } // MARK: - Performing Message Sends struct SerializedMessage { let plaintextData: Data let payloadId: Int64? } func buildAndRecordMessage( _ message: TSOutgoingMessage, in thread: TSThread, tx: SDSAnyWriteTransaction ) -> SerializedMessage? { guard let plaintextData = message.buildPlainTextData(thread, transaction: tx) else { return nil } let messageSendLog = SSKEnvironment.shared.messageSendLogRef let payloadId = messageSendLog.recordPayload(plaintextData, for: message, tx: tx) return SerializedMessage(plaintextData: plaintextData, payloadId: payloadId) } func performMessageSend( _ messageSend: OWSMessageSend, sealedSenderParameters: SealedSenderParameters? ) async throws { try await performMessageSendAttempt( messageSend, sealedSenderParameters: sealedSenderParameters, remainingAttempts: 3 ) } private func retryMessageSend( _ messageSend: OWSMessageSend, remainingAttempts: Int, orThrow error: Error, sealedSenderParameters: SealedSenderParameters? ) async throws { guard remainingAttempts > 1 else { throw error } try await performMessageSendAttempt( messageSend, sealedSenderParameters: sealedSenderParameters, remainingAttempts: remainingAttempts - 1 ) } private func performMessageSendAttempt( _ messageSend: OWSMessageSend, sealedSenderParameters: SealedSenderParameters?, remainingAttempts: Int ) async throws { // The caller has access to the error, so they must throw it if no more // retries are allowed. owsAssert(remainingAttempts > 0) let message = messageSend.message let serviceId = messageSend.serviceId Logger.info("Sending message: \(type(of: message)); timestamp: \(message.timestamp); serviceId: \(serviceId)") let deviceMessages: [DeviceMessage] do { deviceMessages = try await buildDeviceMessages( messageSend: messageSend, sealedSenderParameters: sealedSenderParameters ) } catch { switch error { case RequestMakerUDAuthError.udAuthFailure: try await retryMessageSend( messageSend, remainingAttempts: remainingAttempts, orThrow: error, sealedSenderParameters: nil // Retry as an unsealed send. ) return default: break } throw error } if shouldSkipMessageSend(messageSend, deviceMessages: deviceMessages) { // This emulates the completion logic of an actual successful send (see below). await self.databaseStorage.awaitableWrite { tx in message.update(withSkippedRecipient: messageSend.localIdentifiers.aciAddress, transaction: tx) } return } for deviceMessage in deviceMessages { let hasValidMessageType: Bool = { switch deviceMessage.type { case .unidentifiedSender: return sealedSenderParameters != nil case .ciphertext, .prekeyBundle, .plaintextContent: return sealedSenderParameters == nil case .unknown, .keyExchange, .receipt, .senderkeyMessage: return false } }() guard hasValidMessageType else { owsFailDebug("Invalid message type: \(deviceMessage.type)") throw OWSUnretryableMessageSenderError() } } try await sendDeviceMessages( deviceMessages, messageSend: messageSend, sealedSenderParameters: sealedSenderParameters, remainingAttempts: remainingAttempts ) } /// We can skip sending sync messages if we know that we have no linked /// devices. However, we need to be sure to handle the case where the linked /// device list has just changed. /// /// The linked device list is reflected in two separate pieces of state: /// /// * OWSDevice's state is updated when you link or unlink a device. /// * SignalRecipient's state is updated by 409 "Mismatched devices" /// responses from the service. /// /// If _both_ of these pieces of state agree that there are no linked /// devices, then can safely skip sending sync message. private func shouldSkipMessageSend(_ messageSend: OWSMessageSend, deviceMessages: [DeviceMessage]) -> Bool { guard messageSend.localIdentifiers.contains(serviceId: messageSend.serviceId) else { return false } owsAssertDebug(messageSend.message.canSendToLocalAddress) let hasMessageForLinkedDevice = deviceMessages.contains(where: { $0.destinationDeviceId != DependenciesBridge.shared.tsAccountManager.storedDeviceIdWithMaybeTransaction }) if hasMessageForLinkedDevice { return false } let mayHaveLinkedDevices = databaseStorage.read { tx in DependenciesBridge.shared.deviceManager.mayHaveLinkedDevices(transaction: tx.asV2Read) } if mayHaveLinkedDevices { // We may have just linked a new secondary device which is not yet // reflected in the SignalRecipient that corresponds to ourself. Continue // sending, where we expect to learn about new devices via a 409 response. return false } return true } private func buildDeviceMessages( messageSend: OWSMessageSend, sealedSenderParameters: SealedSenderParameters? ) async throws -> [DeviceMessage] { let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable let recipient = databaseStorage.read { tx in return recipientDatabaseTable.fetchRecipient(serviceId: messageSend.serviceId, transaction: tx.asV2Read) } // If we think the recipient isn't registered, don't build any device // messages. Instead, send an empty message to the server to learn if the // account has any devices. guard let recipient, recipient.isRegistered else { return [] } var recipientDeviceIds = recipient.deviceIds if messageSend.localIdentifiers.contains(serviceId: messageSend.serviceId) { let localDeviceId = DependenciesBridge.shared.tsAccountManager.storedDeviceIdWithMaybeTransaction recipientDeviceIds.removeAll(where: { $0 == localDeviceId }) } var results = [DeviceMessage]() for deviceId in recipientDeviceIds { let deviceMessage = try await buildDeviceMessage( messagePlaintextContent: messageSend.plaintextContent, messageEncryptionStyle: messageSend.message.encryptionStyle, recipientId: recipient.accountId, serviceId: messageSend.serviceId, deviceId: deviceId, isOnlineMessage: messageSend.message.isOnline, isTransientSenderKeyDistributionMessage: messageSend.message.isTransientSKDM, isStoryMessage: messageSend.message.isStorySend, isResendRequestMessage: messageSend.message.isResendRequest, sealedSenderParameters: sealedSenderParameters ) if let deviceMessage { results.append(deviceMessage) } } return results } /// Build a ``DeviceMessage`` for the given parameters describing a message. /// /// A `nil` return value indicates that the given message could not be built /// due to an invalid device ID. func buildDeviceMessage( messagePlaintextContent: Data, messageEncryptionStyle: EncryptionStyle, recipientId: AccountId, serviceId: ServiceId, deviceId: UInt32, isOnlineMessage: Bool, isTransientSenderKeyDistributionMessage: Bool, isStoryMessage: Bool, isResendRequestMessage: Bool, sealedSenderParameters: SealedSenderParameters? ) async throws -> DeviceMessage? { AssertNotOnMainThread() do { try await ensureRecipientHasSession( recipientId: recipientId, serviceId: serviceId, deviceId: deviceId, isOnlineMessage: isOnlineMessage, isTransientSenderKeyDistributionMessage: isTransientSenderKeyDistributionMessage, isStoryMessage: isStoryMessage, udAccess: sealedSenderParameters?.udSendingAccess.udAccess ) } catch let error { switch error { case MessageSenderError.missingDevice: // If we have an invalid device exception, remove this device from the // recipient and suppress the error. await databaseStorage.awaitableWrite { tx in self.updateDevices( serviceId: serviceId, devicesToAdd: [], devicesToRemove: [deviceId], transaction: tx ) } return nil case is MessageSenderNoSessionForTransientMessageError: // When users re-register, we don't want transient messages (like typing // indicators) to cause users to hit the prekey fetch rate limit. So we // silently discard these message if there is no pre-existing session for // the recipient. throw error case is UntrustedIdentityError: // This *can* happen under normal usage, but it should happen relatively // rarely. We expect it to happen whenever Bob reinstalls, and Alice // messages Bob before she can pull down his latest identity. If it's // happening a lot, we should rethink our profile fetching strategy. throw error case is InvalidKeySignatureError: // This should never happen unless a broken client is uploading invalid // keys. The server should now enforce valid signatures on upload, // resulting in this become exceedingly rare as time goes by. throw error case MessageSenderError.prekeyRateLimit: throw SignalServiceRateLimitedError() case is SpamChallengeRequiredError, is SpamChallengeResolvedError: throw error case RecipientIdError.mustNotUsePniBecauseAciExists: throw error case RequestMakerUDAuthError.udAuthFailure: throw error default: owsAssertDebug(error.isNetworkFailureOrTimeout) throw OWSRetryableMessageSenderError() } } return try await databaseStorage.awaitableWrite { tx in do { switch messageEncryptionStyle { case .whisper: return try self.encryptMessage( plaintextContent: messagePlaintextContent, serviceId: serviceId, deviceId: deviceId, sealedSenderParameters: sealedSenderParameters, transaction: tx ) case .plaintext: return try self.wrapPlaintextMessage( plaintextContent: messagePlaintextContent, serviceId: serviceId, deviceId: deviceId, isResendRequestMessage: isResendRequestMessage, sealedSenderParameters: sealedSenderParameters, transaction: tx ) @unknown default: throw OWSAssertionError("Unrecognized encryption style") } } catch IdentityManagerError.identityKeyMismatchForOutgoingMessage { Logger.warn("Found identity key mismatch on outgoing message to \(serviceId).\(deviceId). Archiving session before retrying...") let signalProtocolStoreManager = DependenciesBridge.shared.signalProtocolStoreManager let aciSessionStore = signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore aciSessionStore.archiveSession(for: serviceId, deviceId: deviceId, tx: tx.asV2Write) throw OWSRetryableMessageSenderError() } catch { Logger.warn("Failed to encrypt message \(error)") throw error } } } private func sendDeviceMessages( _ deviceMessages: [DeviceMessage], messageSend: OWSMessageSend, sealedSenderParameters: SealedSenderParameters?, remainingAttempts: Int ) async throws { let message: TSOutgoingMessage = messageSend.message let requestMaker = RequestMaker( label: "Message Send", requestFactoryBlock: { (udAccessKey: SMKUDAccessKey?) in OWSRequestFactory.submitMessageRequest( withServiceId: ServiceIdObjC.wrapValue(messageSend.serviceId), messages: deviceMessages, timestamp: message.timestamp, udAccessKey: udAccessKey, isOnline: message.isOnline, isUrgent: message.isUrgent, isStory: message.isStorySend ) }, serviceId: messageSend.serviceId, udAccess: sealedSenderParameters?.udSendingAccess.udAccess, authedAccount: .implicit(), options: [] ) do { let result = try await requestMaker.makeRequest().awaitable() await messageSendDidSucceed( messageSend, deviceMessages: deviceMessages, wasSentByUD: result.wasSentByUD, wasSentByWebsocket: result.wasSentByWebsocket ) } catch { try await messageSendDidFail( messageSend, responseError: error, sealedSenderParameters: sealedSenderParameters, remainingAttempts: remainingAttempts ) } } private func messageSendDidSucceed( _ messageSend: OWSMessageSend, deviceMessages: [DeviceMessage], wasSentByUD: Bool, wasSentByWebsocket: Bool ) async { let message: TSOutgoingMessage = messageSend.message Logger.info("Successfully sent message: \(type(of: message)), serviceId: \(messageSend.serviceId), timestamp: \(message.timestamp), wasSentByUD: \(wasSentByUD), wasSentByWebsocket: \(wasSentByWebsocket)") await databaseStorage.awaitableWrite { transaction in if deviceMessages.isEmpty, messageSend.localIdentifiers.contains(serviceId: messageSend.serviceId) { // Since we know we have no linked devices, we can record that // fact to later avoid unnecessary sync message sends unless we // later learn of a new linked device. Logger.info("Sent a message with no device messages. Recording no linked devices.") DependenciesBridge.shared.deviceManager.setMayHaveLinkedDevices( false, transaction: transaction.asV2Write ) } deviceMessages.forEach { deviceMessage in if let payloadId = messageSend.plaintextPayloadId, let recipientAci = messageSend.serviceId as? Aci { let messageSendLog = SSKEnvironment.shared.messageSendLogRef messageSendLog.recordPendingDelivery( payloadId: payloadId, recipientAci: recipientAci, recipientDeviceId: deviceMessage.destinationDeviceId, message: message, tx: transaction ) } } message.update(withSentRecipient: ServiceIdObjC.wrapValue(messageSend.serviceId), wasSentByUD: wasSentByUD, transaction: transaction) // If we've just delivered a message to a user, we know they have a valid // Signal account. However, if we're sending a story, the server will // always tell us the recipient is registered, so we can't use this as an // affirmate indication for the existence of an account. // // This is low trust because we don't actually know for sure the fully // qualified address is valid. if !message.isStorySend { let recipientFetcher = DependenciesBridge.shared.recipientFetcher let recipient = recipientFetcher.fetchOrCreate( serviceId: messageSend.serviceId, tx: transaction.asV2Write ) let recipientManager = DependenciesBridge.shared.recipientManager recipientManager.markAsRegisteredAndSave(recipient, shouldUpdateStorageService: true, tx: transaction.asV2Write) } Self.profileManager.didSendOrReceiveMessage( serviceId: messageSend.serviceId, localIdentifiers: messageSend.localIdentifiers, tx: transaction.asV2Write ) } } private struct MessageSendFailureResponse: Decodable { let code: Int? let extraDevices: [UInt32]? let missingDevices: [UInt32]? let staleDevices: [UInt32]? static func parse(_ responseData: Data?) -> MessageSendFailureResponse? { guard let responseData = responseData else { return nil } do { return try JSONDecoder().decode(MessageSendFailureResponse.self, from: responseData) } catch { owsFailDebug("Error: \(error)") return nil } } } private func messageSendDidFail( _ messageSend: OWSMessageSend, responseError: Error, sealedSenderParameters: SealedSenderParameters?, remainingAttempts: Int ) async throws { let message: TSOutgoingMessage = messageSend.message Logger.warn("\(type(of: message)) to \(messageSend.serviceId), timestamp: \(message.timestamp), error: \(responseError)") let httpError: OWSHTTPError? switch responseError { case RequestMakerUDAuthError.udAuthFailure: try await retryMessageSend( messageSend, remainingAttempts: remainingAttempts, orThrow: responseError, sealedSenderParameters: nil // Retry as an unsealed send. ) return case let responseError as OWSHTTPError: httpError = responseError default: owsFailDebug("Unexpected error when sending a message.") httpError = nil } switch httpError?.httpStatusCode { case 401: Logger.warn("Unable to send due to invalid credentials.") throw MessageSendUnauthorizedError() case 404: try await failSendForUnregisteredRecipient(messageSend) case 409: Logger.warn("Mismatched devices for \(messageSend.serviceId)") guard let response = MessageSendFailureResponse.parse(responseError.httpResponseData) else { owsFailDebug("Couldn't parse JSON response.") throw OWSRetryableMessageSenderError() } await handleMismatchedDevices(for: messageSend.serviceId, response: response) case 410: guard let response = MessageSendFailureResponse.parse(responseError.httpResponseData) else { owsFailDebug("Couldn't parse JSON response.") throw OWSRetryableMessageSenderError() } await databaseStorage.awaitableWrite { tx in self.handleStaleDevices(response.staleDevices, for: messageSend.serviceId, tx: tx.asV2Write) } case 428: // SPAM TODO: Only retry messages with -hasRenderableContent Logger.warn("Server requested user complete spam challenge.") try await spamChallengeResolver.tryToHandleSilently( bodyData: responseError.httpResponseData, retryAfter: responseError.httpRetryAfterDate ) // The resolver has 10s to asynchronously resolve a challenge If it // resolves, great! We'll let MessageSender auto-retry. Otherwise, it'll be // marked as "pending" default: break } try await retryMessageSend( messageSend, remainingAttempts: remainingAttempts, orThrow: responseError, sealedSenderParameters: sealedSenderParameters ) } private func failSendForUnregisteredRecipient(_ messageSend: OWSMessageSend) async throws -> Never { let message: TSOutgoingMessage = messageSend.message if !message.isSyncMessage { await databaseStorage.awaitableWrite { writeTx in self.markAsUnregistered( serviceId: messageSend.serviceId, message: message, thread: messageSend.thread, transaction: writeTx ) } } throw MessageSenderNoSuchSignalRecipientError() } // MARK: - Unregistered, Missing, & Stale Devices func markAsUnregistered( serviceId: ServiceId, message: TSOutgoingMessage, thread: TSThread, transaction tx: SDSAnyWriteTransaction ) { AssertNotOnMainThread() if thread.isNonContactThread { // Mark as "skipped" group members who no longer have signal accounts. message.update(withSkippedRecipient: SignalServiceAddress(serviceId), transaction: tx) } let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable guard let recipient = recipientDatabaseTable.fetchRecipient(serviceId: serviceId, transaction: tx.asV2Read) else { return } let recipientManager = DependenciesBridge.shared.recipientManager recipientManager.markAsUnregisteredAndSave(recipient, unregisteredAt: .now, shouldUpdateStorageService: true, tx: tx.asV2Write) let tsAccountManager = DependenciesBridge.shared.tsAccountManager guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read) else { Logger.warn("Can't split recipient because we're not registered.") return } let recipientMerger = DependenciesBridge.shared.recipientMerger recipientMerger.splitUnregisteredRecipientIfNeeded( localIdentifiers: localIdentifiers, unregisteredRecipient: recipient, tx: tx.asV2Write ) } private func handleMismatchedDevices(for serviceId: ServiceId, response: MessageSendFailureResponse) async { await databaseStorage.awaitableWrite { transaction in self.updateDevices( serviceId: serviceId, devicesToAdd: response.missingDevices ?? [], devicesToRemove: response.extraDevices ?? [], transaction: transaction ) } } // Called when the server indicates that the devices no longer exist - e.g. when the remote recipient has reinstalled. func handleStaleDevices(_ staleDevices: [UInt32]?, for serviceId: ServiceId, tx: DBWriteTransaction) { let staleDevices = staleDevices ?? [] Logger.warn("staleDevices: \(staleDevices) for \(serviceId)") guard !staleDevices.isEmpty else { return } let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore for staleDeviceId in staleDevices { sessionStore.archiveSession(for: serviceId, deviceId: staleDeviceId, tx: tx) } } func updateDevices( serviceId: ServiceId, devicesToAdd: [UInt32], devicesToRemove: [UInt32], transaction: SDSAnyWriteTransaction ) { AssertNotOnMainThread() owsAssertDebug(Set(devicesToAdd).isDisjoint(with: devicesToRemove)) if !devicesToAdd.isEmpty, SignalServiceAddress(serviceId).isLocalAddress { DependenciesBridge.shared.deviceManager.setMayHaveLinkedDevices( true, transaction: transaction.asV2Write ) } let recipientFetcher = DependenciesBridge.shared.recipientFetcher let recipient = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: transaction.asV2Write) let recipientManager = DependenciesBridge.shared.recipientManager recipientManager.modifyAndSave( recipient, deviceIdsToAdd: devicesToAdd, deviceIdsToRemove: devicesToRemove, shouldUpdateStorageService: true, tx: transaction.asV2Write ) if !devicesToRemove.isEmpty { Logger.info("Archiving sessions for extra devices: \(devicesToRemove)") let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore for deviceId in devicesToRemove { sessionStore.archiveSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write) } } } // MARK: - Encryption private func encryptMessage( plaintextContent plainText: Data, serviceId: ServiceId, deviceId: UInt32, sealedSenderParameters: SealedSenderParameters?, transaction: SDSAnyWriteTransaction ) throws -> DeviceMessage { owsAssertDebug(!Thread.isMainThread) guard try containsValidSession(for: serviceId, deviceId: deviceId, tx: transaction.asV2Write) else { throw MessageSendEncryptionError(serviceId: serviceId, deviceId: deviceId) } let paddedPlaintext = plainText.paddedMessageBody let serializedMessage: Data let messageType: SSKProtoEnvelopeType let identityManager = DependenciesBridge.shared.identityManager let signalProtocolStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci) let protocolAddress = ProtocolAddress(serviceId, deviceId: deviceId) if let sealedSenderParameters { let secretCipher = try SMKSecretSessionCipher( sessionStore: signalProtocolStore.sessionStore, preKeyStore: signalProtocolStore.preKeyStore, signedPreKeyStore: signalProtocolStore.signedPreKeyStore, kyberPreKeyStore: signalProtocolStore.kyberPreKeyStore, identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write), senderKeyStore: Self.senderKeyStore ) serializedMessage = try secretCipher.encryptMessage( for: serviceId, deviceId: deviceId, paddedPlaintext: paddedPlaintext, contentHint: sealedSenderParameters.contentHint.signalClientHint, groupId: sealedSenderParameters.envelopeGroupId(tx: transaction.asV2Read), senderCertificate: sealedSenderParameters.udSendingAccess.senderCertificate, protocolContext: transaction ) messageType = .unidentifiedSender } else { let result = try signalEncrypt( message: paddedPlaintext, for: protocolAddress, sessionStore: signalProtocolStore.sessionStore, identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write), context: transaction ) switch result.messageType { case .whisper: messageType = .ciphertext case .preKey: messageType = .prekeyBundle case .plaintext: messageType = .plaintextContent default: owsFailDebug("Unrecognized message type") messageType = .unknown } serializedMessage = Data(result.serialize()) // The message is smaller than the envelope, but if the message // is larger than this limit, the envelope will be too. if serializedMessage.count > MessageProcessor.largeEnvelopeWarningByteCount { owsFailDebug("Unexpectedly large encrypted message.") } } // We had better have a session after encrypting for this recipient! let session = try signalProtocolStore.sessionStore.loadSession( for: protocolAddress, context: transaction )! return DeviceMessage( type: messageType, destinationDeviceId: protocolAddress.deviceId, destinationRegistrationId: try session.remoteRegistrationId(), serializedMessage: serializedMessage ) } private func wrapPlaintextMessage( plaintextContent rawPlaintext: Data, serviceId: ServiceId, deviceId: UInt32, isResendRequestMessage: Bool, sealedSenderParameters: SealedSenderParameters?, transaction: SDSAnyWriteTransaction ) throws -> DeviceMessage { owsAssertDebug(!Thread.isMainThread) let identityManager = DependenciesBridge.shared.identityManager let protocolAddress = ProtocolAddress(serviceId, deviceId: deviceId) // Only resend request messages are allowed to use this codepath. guard isResendRequestMessage else { throw OWSAssertionError("Unexpected message type") } let plaintext = try PlaintextContent(bytes: rawPlaintext) let serializedMessage: Data let messageType: SSKProtoEnvelopeType if let sealedSenderParameters { let usmc = try UnidentifiedSenderMessageContent( CiphertextMessage(plaintext), from: sealedSenderParameters.udSendingAccess.senderCertificate, contentHint: sealedSenderParameters.contentHint.signalClientHint, groupId: sealedSenderParameters.envelopeGroupId(tx: transaction.asV2Read) ?? Data() ) let outerBytes = try sealedSenderEncrypt( usmc, for: protocolAddress, identityStore: identityManager.libSignalStore(for: .aci, tx: transaction.asV2Write), context: transaction ) serializedMessage = Data(outerBytes) messageType = .unidentifiedSender } else { serializedMessage = Data(plaintext.serialize()) messageType = .plaintextContent } let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore let session = try sessionStore.loadSession(for: protocolAddress, context: transaction)! return DeviceMessage( type: messageType, destinationDeviceId: protocolAddress.deviceId, destinationRegistrationId: try session.remoteRegistrationId(), serializedMessage: serializedMessage ) } }