Signal-iOS/SignalServiceKit/Messages/MessageSender+SenderKey.swift
2024-04-23 13:58:27 -05:00

823 lines
37 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
extension MessageSender {
private var senderKeyQueue: DispatchQueue { .global(qos: .utility) }
private static var maxSenderKeyEnvelopeSize: UInt64 { 256 * 1024 }
struct Recipient {
let serviceId: ServiceId
let devices: [UInt32]
var protocolAddresses: [ProtocolAddress] {
return devices.map { ProtocolAddress(serviceId, deviceId: $0) }
}
init(serviceId: ServiceId, transaction tx: SDSAnyReadTransaction) {
self.serviceId = serviceId
self.devices = {
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
return recipientDatabaseTable.fetchRecipient(serviceId: serviceId, transaction: tx.asV2Read)?.deviceIds ?? []
}()
}
}
private enum SenderKeyError: Error, IsRetryableProvider, UserErrorDescriptionProvider {
case invalidAuthHeader
case invalidRecipient
case deviceUpdate
case staleDevices
case oversizeMessage
case recipientSKDMFailed(Error)
var isRetryableProvider: Bool { true }
var asSSKError: NSError {
let result: Error
switch self {
case let .recipientSKDMFailed(underlyingError):
result = underlyingError
case .invalidAuthHeader, .invalidRecipient, .oversizeMessage:
// For all of these error types, there's a chance that a fanout send may be successful. This
// error is retryable, but indicates that the next send attempt should restrict itself to fanout
// send only.
result = SenderKeyUnavailableError(customLocalizedDescription: localizedDescription)
case .deviceUpdate, .staleDevices:
result = SenderKeyEphemeralError(customLocalizedDescription: localizedDescription)
}
return (result as NSError)
}
var localizedDescription: String {
// Since this is a retryable error, so it's unlikely to be surfaced to the user. I think the only situation
// where it would is it happens to be the last error hit before we run out of resend attempts. In that case,
// we should just show a generic error just to be safe.
// TODO: This probably isn't the only error like this. Should we have a fallback generic string
// for all retryable errors without a description that exhaust retry attempts?
switch self {
case .recipientSKDMFailed(let error):
return error.localizedDescription
default:
return OWSLocalizedString("ERROR_DESCRIPTION_CLIENT_SENDING_FAILURE",
comment: "Generic notice when message failed to send.")
}
}
}
class SenderKeyStatus {
enum ParticipantState {
case SenderKeyReady
case NeedsSKDM
case FanoutOnly
}
var participants: [ServiceId: ParticipantState]
init(numberOfParticipants: Int) {
self.participants = Dictionary(minimumCapacity: numberOfParticipants)
}
convenience init(fanoutOnlyParticipants: [ServiceId]) {
self.init(numberOfParticipants: fanoutOnlyParticipants.count)
fanoutOnlyParticipants.forEach { self.participants[$0] = .FanoutOnly }
}
var fanoutParticipants: [ServiceId] {
Array(participants.lazy.filter { $0.value == .FanoutOnly }.map { $0.key })
}
var allSenderKeyParticipants: [ServiceId] {
Array(participants.lazy.filter { $0.value != .FanoutOnly }.map { $0.key })
}
var participantsNeedingSKDM: [ServiceId] {
Array(participants.lazy.filter { $0.value == .NeedsSKDM }.map { $0.key })
}
var readyParticipants: [ServiceId] {
Array(participants.lazy.filter { $0.value == .SenderKeyReady }.map { $0.key })
}
}
/// Filters the list of participants for a thread that support SenderKey
func senderKeyStatus(
for thread: TSThread,
intendedRecipients: [ServiceId],
udAccessMap: [ServiceId: OWSUDSendingAccess]
) -> SenderKeyStatus {
guard thread.usesSenderKey else {
return .init(fanoutOnlyParticipants: intendedRecipients)
}
return databaseStorage.read { readTx in
let isCurrentKeyValid = senderKeyStore.isKeyValid(for: thread, readTx: readTx)
let recipientsWithoutSenderKey = senderKeyStore.recipientsInNeedOfSenderKey(
for: thread,
serviceIds: intendedRecipients,
readTx: readTx
)
let senderKeyStatus = SenderKeyStatus(numberOfParticipants: intendedRecipients.count)
let threadRecipients = thread.recipientAddresses(with: readTx).compactMap { $0.serviceId }
intendedRecipients.forEach { candidate in
// Sender key requires that you're a full member of the group and you support UD
guard
threadRecipients.contains(candidate),
[.enabled, .unrestricted].contains(udAccessMap[candidate]?.udAccess.udAccessMode)
else {
senderKeyStatus.participants[candidate] = .FanoutOnly
return
}
guard !SignalServiceAddress(candidate).isLocalAddress else {
senderKeyStatus.participants[candidate] = .FanoutOnly
owsFailBeta("Callers must not provide UD access for the local ACI.")
return
}
// If all registrationIds aren't valid, we should fallback to fanout
// This should be removed once we've sorted out why there are invalid
// registrationIds
let registrationIdStatus = Self.registrationIdStatus(for: candidate, transaction: readTx)
switch registrationIdStatus {
case .valid:
// All good, keep going.
break
case .invalid:
// Don't bother with SKDM, fall back to fanout.
senderKeyStatus.participants[candidate] = .FanoutOnly
return
case .noSession:
// This recipient has no session; thats ok, just fall back to SKDM.
senderKeyStatus.participants[candidate] = .NeedsSKDM
return
}
// The recipient is good to go for sender key! Though, they need an SKDM
// if they don't have a current valid sender key.
if recipientsWithoutSenderKey.contains(candidate) || !isCurrentKeyValid {
senderKeyStatus.participants[candidate] = .NeedsSKDM
} else {
senderKeyStatus.participants[candidate] = .SenderKeyReady
}
}
return senderKeyStatus
}
}
func senderKeyMessageSendPromise(
message: TSOutgoingMessage,
plaintextContent: Data,
payloadId: Int64?,
thread: TSThread,
status: SenderKeyStatus,
udAccessMap: [ServiceId: OWSUDSendingAccess],
senderCertificates: SenderCertificates,
localIdentifiers: LocalIdentifiers,
sendErrorBlock: @escaping (ServiceId, NSError) -> Void
) -> Promise<Void> {
// Because of the way promises are combined further up the chain, we need to ensure that if
// *any* send fails, the entire Promise rejcts. The error it rejects with doesn't really matter
// and isn't consulted.
let didHitAnyFailure = AtomicBool(false, lock: .sharedGlobal)
let wrappedSendErrorBlock = { (serviceId: ServiceId, error: Error) -> Void in
Logger.info("Sender key send failed for \(serviceId): \(error)")
_ = didHitAnyFailure.tryToSetFlag()
if let senderKeyError = error as? SenderKeyError {
sendErrorBlock(serviceId, senderKeyError.asSSKError)
} else {
sendErrorBlock(serviceId, (error as NSError))
}
}
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
guard let localIdentifiers = self.databaseStorage.read(block: { tx in
tsAccountManager.localIdentifiers(tx: tx.asV2Read)
}) else {
return .init(error: OWSAssertionError("Not registered."))
}
// To ensure we don't accidentally throw an error early in our promise chain
// Without calling the perRecipient failures, we declare this as a guarantee.
// All errors must be caught and handled. If not, we may end up with sends that
// pend indefinitely.
let senderKeyGuarantee: Guarantee<Void>
senderKeyGuarantee = { () -> Guarantee<[ServiceId]> in
// If none of our recipients need an SKDM let's just skip the database write.
if status.participantsNeedingSKDM.count > 0 {
return senderKeyDistributionPromise(
recipients: status.allSenderKeyParticipants,
thread: thread,
originalMessage: message,
udAccessMap: udAccessMap,
localIdentifiers: localIdentifiers,
sendErrorBlock: wrappedSendErrorBlock)
} else {
return .value(status.readyParticipants)
}
}().then(on: senderKeyQueue) { (senderKeyRecipients: [ServiceId]) -> Guarantee<Void> in
guard senderKeyRecipients.count > 0 else {
// Something went wrong with the SKDM promise. Exit early.
owsAssertDebug(didHitAnyFailure.get())
return .value(())
}
return firstly { () -> Promise<SenderKeySendResult> in
Logger.info("Sending sender key message with timestamp \(message.timestamp) to \(senderKeyRecipients)")
return self.sendSenderKeyRequest(
message: message,
plaintext: plaintextContent,
thread: thread,
serviceIds: senderKeyRecipients,
udAccessMap: udAccessMap,
senderCertificates: senderCertificates
)
}.done(on: self.senderKeyQueue) { (sendResult: SenderKeySendResult) in
Logger.info("Sender key message with timestamp \(message.timestamp) sent! Recipients: \(sendResult.successServiceIds). Unregistered: \(sendResult.unregisteredServiceIds)")
return self.databaseStorage.write { tx in
sendResult.unregisteredServiceIds.forEach { serviceId in
self.markAsUnregistered(serviceId: serviceId, message: message, thread: thread, transaction: tx)
let error = MessageSenderNoSuchSignalRecipientError()
wrappedSendErrorBlock(serviceId, error)
}
sendResult.success.forEach { recipient in
message.update(
withSentRecipient: ServiceIdObjC.wrapValue(recipient.serviceId),
wasSentByUD: true,
transaction: tx
)
// If we're sending a story, we generally get a 200, even if the account
// doesn't exist. Therefore, don't use this to mark accounts as registered.
if !message.isStorySend {
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let recipient = recipientFetcher.fetchOrCreate(serviceId: recipient.serviceId, tx: tx.asV2Write)
let recipientManager = DependenciesBridge.shared.recipientManager
recipientManager.markAsRegisteredAndSave(recipient, shouldUpdateStorageService: true, tx: tx.asV2Write)
}
self.profileManager.didSendOrReceiveMessage(
serviceId: recipient.serviceId,
localIdentifiers: localIdentifiers,
tx: tx.asV2Write
)
guard let payloadId = payloadId, let recipientAci = recipient.serviceId as? Aci else { return }
recipient.devices.forEach { deviceId in
let messageSendLog = SSKEnvironment.shared.messageSendLogRef
messageSendLog.recordPendingDelivery(
payloadId: payloadId,
recipientAci: recipientAci,
recipientDeviceId: deviceId,
message: message,
tx: tx
)
}
}
}
}.recover(on: self.senderKeyQueue) { error -> Void in
// If the sender key message failed to send, fail each recipient that we hoped to send it to.
Logger.error("Sender key send failed: \(error)")
senderKeyRecipients.forEach { wrappedSendErrorBlock($0, error) }
}
}
// Since we know the guarantee is always successful, on any per-recipient failure, this generic error is used
// to fail the returned promise.
return senderKeyGuarantee.done(on: DispatchQueue.global()) {
if didHitAnyFailure.get() {
// MessageSender just uses this error as a sentinel to consult the per-recipient errors. The
// actual error doesn't matter.
throw OWSGenericError("Failed to send to at least one SenderKey participant")
}
}
}
private static func senderCertificate(from senderCertificates: SenderCertificates, tx: SDSAnyReadTransaction) -> SenderCertificate {
switch udManager.phoneNumberSharingMode(tx: tx.asV2Read).orDefault {
case .everybody:
return senderCertificates.defaultCert
case .nobody:
return senderCertificates.uuidOnlyCert
}
}
// Given a list of recipients, ensures that all recipients have been sent an
// SKDM. If an intended recipient does not have an SKDM, it sends one. If we
// fail to send an SKDM, invokes the per-recipient error block.
//
// Returns the list of all recipients ready for the SenderKeyMessage.
private func senderKeyDistributionPromise(
recipients: [ServiceId],
thread: TSThread,
originalMessage: TSOutgoingMessage,
udAccessMap: [ServiceId: OWSUDSendingAccess],
localIdentifiers: LocalIdentifiers,
sendErrorBlock: @escaping (ServiceId, Error) -> Void
) -> Guarantee<[ServiceId]> {
var recipientsNotNeedingSKDM: Set<ServiceId> = Set()
return databaseStorage.write(.promise) { writeTx -> [(OWSMessageSend, SealedSenderParameters?)] in
// Here we fetch all of the recipients that need an SKDM
// We then construct an OWSMessageSend for each recipient that needs an SKDM.
// Even though earlier in the promise chain we check key expiration and who needs an SKDM
// We should recheck all of this info again just in case that data is no longer valid.
// e.g. The key expired since we last checked. Now *all* recipients need the current SKDM,
// not just the ones that needed it when we last checked.
self.senderKeyStore.expireSendingKeyIfNecessary(for: thread, writeTx: writeTx)
let recipientsNeedingSKDM = self.senderKeyStore.recipientsInNeedOfSenderKey(
for: thread,
serviceIds: recipients,
readTx: writeTx
)
recipientsNotNeedingSKDM = Set(recipients).subtracting(recipientsNeedingSKDM)
guard !recipientsNeedingSKDM.isEmpty else { return [] }
guard let skdmBytes = self.senderKeyStore.skdmBytesForThread(
thread,
localAci: localIdentifiers.aci,
localDeviceId: DependenciesBridge.shared.tsAccountManager.storedDeviceId(tx: writeTx.asV2Read),
tx: writeTx
) else {
throw OWSAssertionError("Couldn't build SKDM")
}
return recipientsNeedingSKDM.compactMap { (serviceId) -> (OWSMessageSend, SealedSenderParameters?)? in
Logger.info("Sending SKDM to \(serviceId) for thread \(thread.uniqueId)")
let contactThread = TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(serviceId),
transaction: writeTx
)
let skdmMessage = OWSOutgoingSenderKeyDistributionMessage(
thread: contactThread,
senderKeyDistributionMessageBytes: skdmBytes,
transaction: writeTx
)
skdmMessage.configureAsSentOnBehalfOf(originalMessage, in: thread)
guard let serializedMessage = self.buildAndRecordMessage(skdmMessage, in: contactThread, tx: writeTx) else {
sendErrorBlock(serviceId, SenderKeyError.recipientSKDMFailed(OWSAssertionError("Couldn't build message.")))
return nil
}
let messageSend = OWSMessageSend(
message: skdmMessage,
plaintextContent: serializedMessage.plaintextData,
plaintextPayloadId: serializedMessage.payloadId,
thread: contactThread,
serviceId: serviceId,
localIdentifiers: localIdentifiers
)
let sealedSenderParameters = udAccessMap[serviceId].map {
SealedSenderParameters(message: skdmMessage, udSendingAccess: $0)
}
return (messageSend, sealedSenderParameters)
}
}.then(on: senderKeyQueue) { (skdmSends) -> Guarantee<[Result<OWSMessageSend, Error>]> in
// For each SKDM request we kick off a sendMessage promise.
// - If it succeeds, great! Propagate along the successful OWSMessageSend.
// - Otherwise, invoke the sendErrorBlock and rethrow so it gets packaged into the Guarantee.
// We use when(resolved:) because we want the promise to wait for
// all sub-promises to finish, even if some failed.
Guarantee.when(resolved: skdmSends.map { (messageSend, sealedSenderParameters) in
return Promise.wrapAsync {
try await self.performMessageSend(messageSend, sealedSenderParameters: sealedSenderParameters)
return messageSend
}.recover(on: self.senderKeyQueue) { error -> Promise<OWSMessageSend> in
if error is MessageSenderNoSuchSignalRecipientError {
self.databaseStorage.write { transaction in
self.markAsUnregistered(
serviceId: messageSend.serviceId,
message: originalMessage,
thread: thread,
transaction: transaction
)
}
}
// Note that we still rethrow. It's just easier to access the address
// while we still have the messageSend in scope.
let wrappedError = SenderKeyError.recipientSKDMFailed(error)
sendErrorBlock(messageSend.serviceId, wrappedError)
throw wrappedError
}
})
}.map(on: self.senderKeyQueue) { resultArray -> [ServiceId] in
// This is a hot path, so we do a bit of a dance here to prepare all of the successful send
// info before opening the write transaction. We need the recipient address and the SKDM
// timestamp.
let successfulSendInfo: [(recipient: ServiceId, timestamp: UInt64)]
successfulSendInfo = resultArray.compactMap { result in
switch result {
case let .success(messageSend):
return (recipient: messageSend.serviceId, timestamp: messageSend.message.timestamp)
case .failure:
return nil
}
}
if successfulSendInfo.count > 0 {
try self.databaseStorage.write { writeTx in
try successfulSendInfo.forEach {
try self.senderKeyStore.recordSenderKeySent(
for: thread,
to: ServiceIdObjC.wrapValue($0.recipient),
timestamp: $0.timestamp,
writeTx: writeTx
)
}
}
}
// We want to return all recipients that are now ready for sender key
return Array(recipientsNotNeedingSKDM) + successfulSendInfo.map { $0.recipient }
}.recover(on: senderKeyQueue) { error in
// If we hit *any* error that we haven't handled, we should fail the send
// for everyone.
let wrappedError = SenderKeyError.recipientSKDMFailed(error)
recipients.forEach { sendErrorBlock($0, wrappedError) }
return .value([])
}
}
fileprivate struct SenderKeySendResult {
let success: [Recipient]
let unregistered: [Recipient]
var successServiceIds: [ServiceId] { success.map { $0.serviceId } }
var unregisteredServiceIds: [ServiceId] { unregistered.map { $0.serviceId } }
}
// Encrypts and sends the message using SenderKey
// If the Promise is successful, the message was sent to every provided address *except* those returned
// in the promise. The server reported those addresses as unregistered.
fileprivate func sendSenderKeyRequest(
message: TSOutgoingMessage,
plaintext: Data,
thread: TSThread,
serviceIds: [ServiceId],
udAccessMap: [ServiceId: OWSUDSendingAccess],
senderCertificates: SenderCertificates
) -> Promise<SenderKeySendResult> {
return self.databaseStorage.write(.promise) { writeTx -> ([Recipient], Data) in
let senderCertificate = Self.senderCertificate(from: senderCertificates, tx: writeTx)
let recipients = serviceIds.map { Recipient(serviceId: $0, transaction: writeTx) }
let ciphertext = try self.senderKeyMessageBody(
plaintext: plaintext,
message: message,
thread: thread,
recipients: recipients,
senderCertificate: senderCertificate,
transaction: writeTx
)
return (recipients, ciphertext)
}.then(on: senderKeyQueue) { (recipients: [Recipient], ciphertext: Data) -> Promise<SenderKeySendResult> in
self._sendSenderKeyRequest(
encryptedMessageBody: ciphertext,
timestamp: message.timestamp,
isOnline: message.isOnline,
isUrgent: message.isUrgent,
isStory: message.isStorySend,
thread: thread,
recipients: recipients,
udAccessMap: udAccessMap,
remainingAttempts: 3
)
}
}
// TODO: This is a similar pattern to RequestMaker. An opportunity to reduce duplication.
fileprivate func _sendSenderKeyRequest(
encryptedMessageBody: Data,
timestamp: UInt64,
isOnline: Bool,
isUrgent: Bool,
isStory: Bool,
thread: TSThread,
recipients: [Recipient],
udAccessMap: [ServiceId: OWSUDSendingAccess],
remainingAttempts: UInt
) -> Promise<SenderKeySendResult> {
return firstly { () -> Promise<HTTPResponse> in
try self.performSenderKeySend(
ciphertext: encryptedMessageBody,
timestamp: timestamp,
isOnline: isOnline,
isUrgent: isUrgent,
isStory: isStory,
thread: thread,
recipients: recipients,
udAccessMap: udAccessMap
)
}.map(on: senderKeyQueue) { response -> SenderKeySendResult in
guard response.responseStatusCode == 200 else { throw
OWSAssertionError("Unhandled error")
}
let response = try Self.decodeSuccessResponse(data: response.responseBodyData)
let unregisteredServiceIds = Set(response.unregisteredServiceIds.map { $0.wrappedValue })
let successful = recipients.filter { !unregisteredServiceIds.contains($0.serviceId) }
let unregistered = recipients.filter { unregisteredServiceIds.contains($0.serviceId) }
return SenderKeySendResult(success: successful, unregistered: unregistered)
}.recover(on: senderKeyQueue) { error -> Promise<SenderKeySendResult> in
let retryIfPossible = { () throws -> Promise<SenderKeySendResult> in
if remainingAttempts > 0 {
return self._sendSenderKeyRequest(
encryptedMessageBody: encryptedMessageBody,
timestamp: timestamp,
isOnline: isOnline,
isUrgent: isUrgent,
isStory: isStory,
thread: thread,
recipients: recipients,
udAccessMap: udAccessMap,
remainingAttempts: remainingAttempts-1
)
} else {
throw error
}
}
if error.isNetworkFailureOrTimeout {
return try retryIfPossible()
} else if let httpError = error as? OWSHTTPError {
let statusCode = httpError.httpStatusCode ?? 0
let responseData = httpError.httpResponseData
switch statusCode {
case 401:
owsFailDebug("Invalid composite authorization header for sender key send request. Falling back to fanout")
throw SenderKeyError.invalidAuthHeader
case 404:
Logger.warn("One of the recipients could not match an account. We don't know which. Falling back to fanout.")
throw SenderKeyError.invalidRecipient
case 409:
// Incorrect device set. We should add/remove devices and try again.
let responseBody = try Self.decode409Response(data: responseData)
self.databaseStorage.write { tx in
for account in responseBody {
self.updateDevices(
serviceId: account.serviceId,
devicesToAdd: account.devices.missingDevices,
devicesToRemove: account.devices.extraDevices,
transaction: tx
)
}
}
throw SenderKeyError.deviceUpdate
case 410:
// Server reports stale devices. We should reset our session and try again.
let responseBody = try Self.decode410Response(data: responseData)
self.databaseStorage.write { tx in
for account in responseBody {
self.handleStaleDevices(account.devices.staleDevices, for: account.serviceId, tx: tx.asV2Write)
}
}
throw SenderKeyError.staleDevices
case 428:
guard let body = responseData, let expiry = error.httpRetryAfterDate else {
throw OWSAssertionError("Invalid spam response body")
}
return Promise { future in
self.spamChallengeResolver.handleServerChallengeBody(body, retryAfter: expiry) { didSucceed in
if didSucceed {
future.resolve()
} else {
let error = SpamChallengeRequiredError()
future.reject(error)
}
}
}.then(on: self.senderKeyQueue) {
try retryIfPossible()
}
default:
// Unhandled response code.
throw error
}
} else {
owsFailDebug("Unexpected error \(error)")
throw error
}
}
}
private func senderKeyMessageBody(
plaintext: Data,
message: TSOutgoingMessage,
thread: TSThread,
recipients: [Recipient],
senderCertificate: SenderCertificate,
transaction writeTx: SDSAnyWriteTransaction
) throws -> Data {
let groupIdForSending: Data
if let groupThread = thread as? TSGroupThread {
// multiRecipient messages really need to have the USMC groupId actually match the target thread. Otherwise
// this breaks sender key recovery. So we'll always use the thread's groupId here, but we'll verify that
// we're not trying to send any messages with a special envelope groupId.
// These are only ever set on resend request/response messages, which are only sent through a 1:1 session,
// but we should be made aware if that ever changes.
owsAssertDebug(message.envelopeGroupIdWithTransaction(writeTx) == groupThread.groupId)
groupIdForSending = groupThread.groupId
} else {
// If we're not a group thread, we don't have a groupId.
// TODO: Eventually LibSignalClient could allow passing `nil` in this case
groupIdForSending = Data()
}
let identityManager = DependenciesBridge.shared.identityManager
let signalProtocolStoreManager = DependenciesBridge.shared.signalProtocolStoreManager
let protocolAddresses = recipients.flatMap { $0.protocolAddresses }
let secretCipher = try SMKSecretSessionCipher(
sessionStore: signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore,
preKeyStore: signalProtocolStoreManager.signalProtocolStore(for: .aci).preKeyStore,
signedPreKeyStore: signalProtocolStoreManager.signalProtocolStore(for: .aci).signedPreKeyStore,
kyberPreKeyStore: signalProtocolStoreManager.signalProtocolStore(for: .aci).kyberPreKeyStore,
identityStore: identityManager.libSignalStore(for: .aci, tx: writeTx.asV2Write),
senderKeyStore: Self.senderKeyStore)
let distributionId = senderKeyStore.distributionIdForSendingToThread(thread, writeTx: writeTx)
let ciphertext = try secretCipher.groupEncryptMessage(
recipients: protocolAddresses,
paddedPlaintext: plaintext.paddedMessageBody,
senderCertificate: senderCertificate,
groupId: groupIdForSending,
distributionId: distributionId,
contentHint: message.contentHint.signalClientHint,
protocolContext: writeTx)
guard ciphertext.count <= Self.maxSenderKeyEnvelopeSize else {
Logger.error("serializedMessage: \(ciphertext.count) > \(Self.maxSenderKeyEnvelopeSize)")
throw SenderKeyError.oversizeMessage
}
return ciphertext
}
private func performSenderKeySend(
ciphertext: Data,
timestamp: UInt64,
isOnline: Bool,
isUrgent: Bool,
isStory: Bool,
thread: TSThread,
recipients: [Recipient],
udAccessMap: [ServiceId: OWSUDSendingAccess]
) throws -> Promise<HTTPResponse> {
// Sender key messages use an access key composed of every recipient's individual access key.
let allAccessKeys = recipients.compactMap {
udAccessMap[$0.serviceId]?.udAccess.senderKeyUDAccessKey
}
guard recipients.count == allAccessKeys.count else {
throw OWSAssertionError("Incomplete access key set")
}
guard let firstKey = allAccessKeys.first else {
throw OWSAssertionError("Must provide at least one address")
}
let remainingKeys = allAccessKeys.dropFirst()
let compositeKey = remainingKeys.reduce(firstKey, ^)
let request = OWSRequestFactory.submitMultiRecipientMessageRequest(
ciphertext: ciphertext,
compositeUDAccessKey: compositeKey,
timestamp: timestamp,
isOnline: isOnline,
isUrgent: isUrgent,
isStory: isStory
)
return networkManager.makePromise(request: request, canUseWebSocket: true)
}
}
extension MessageSender {
struct SuccessPayload: Decodable {
let unregisteredServiceIds: [ServiceIdString]
enum CodingKeys: String, CodingKey {
case unregisteredServiceIds = "uuids404"
}
}
typealias ResponseBody409 = [Account409]
struct Account409: Decodable {
@ServiceIdString var serviceId: ServiceId
let devices: DeviceSet
enum CodingKeys: String, CodingKey {
case serviceId = "uuid"
case devices
}
struct DeviceSet: Decodable {
let missingDevices: [UInt32]
let extraDevices: [UInt32]
}
}
typealias ResponseBody410 = [Account410]
struct Account410: Decodable {
@ServiceIdString var serviceId: ServiceId
let devices: DeviceSet
enum CodingKeys: String, CodingKey {
case serviceId = "uuid"
case devices
}
struct DeviceSet: Decodable {
let staleDevices: [UInt32]
}
}
static func decodeSuccessResponse(data: Data?) throws -> SuccessPayload {
guard let data = data else {
throw OWSAssertionError("No data provided")
}
return try JSONDecoder().decode(SuccessPayload.self, from: data)
}
static func decode409Response(data: Data?) throws -> ResponseBody409 {
guard let data = data else {
throw OWSAssertionError("No data provided")
}
return try JSONDecoder().decode(ResponseBody409.self, from: data)
}
static func decode410Response(data: Data?) throws -> ResponseBody410 {
guard let data = data else {
throw OWSAssertionError("No data provided")
}
return try JSONDecoder().decode(ResponseBody410.self, from: data)
}
}
fileprivate extension MessageSender {
enum RegistrationIdStatus {
/// The address has a session with a valid registration id
case valid
/// LibSignalClient expects registrationIds to fit in 15 bits for multiRecipientEncrypt,
/// but there are some reports of clients having larger registrationIds. Unclear why.
case invalid
/// There is no session for this address. Unclear why this would happen; but in this case
/// the address should receive an SKDM.
case noSession
}
/// We shouldn't send a SenderKey message to addresses with a session record with
/// an invalid registrationId.
/// We should send an SKDM to addresses with no session record at all.
///
/// For now, let's perform a check to filter out invalid registrationIds. An
/// investigation into cleaning up these invalid registrationIds is ongoing.
///
/// Also check for missing sessions (shouldn't happen if we've gotten this far, since
/// SenderKeyStore already said this address has previous Sender Key sends). We should
/// investigate how this ever happened, but for now fall back to sending another SKDM.
static func registrationIdStatus(for serviceId: ServiceId, transaction tx: SDSAnyReadTransaction) -> RegistrationIdStatus {
let candidateDevices = MessageSender.Recipient(serviceId: serviceId, transaction: tx).devices
let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
for deviceId in candidateDevices {
do {
guard
let sessionRecord = try sessionStore.loadSession(
for: serviceId,
deviceId: deviceId,
tx: tx.asV2Read
),
sessionRecord.hasCurrentState
else { return .noSession }
let registrationId = try sessionRecord.remoteRegistrationId()
let isValidRegistrationId = (registrationId & 0x3fff == registrationId)
owsAssertDebug(isValidRegistrationId)
if !isValidRegistrationId {
return .invalid
}
} catch {
// An error is never thrown on nil result; only if there's something
// on disk but parsing fails.
owsFailDebug("Failed to fetch registrationId for \(serviceId): \(error)")
return .invalid
}
}
return .valid
}
}