823 lines
37 KiB
Swift
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
|
|
}
|
|
}
|