TM-SGNL-iOS/SignalServiceKit/Messages/OWSMessageDecrypter.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

784 lines
34 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
public class OWSMessageDecrypter {
private var senderIdsResetDuringCurrentBatch = NSMutableSet()
private var placeholderCleanupTimer: Timer? {
didSet { oldValue?.invalidate() }
}
public init(appReadiness: AppReadiness) {
SwiftSingletons.register(self)
NotificationCenter.default.addObserver(
self,
selector: #selector(messageProcessorDidDrainQueue),
name: MessageProcessor.messageProcessorDidDrainQueue,
object: nil
)
appReadiness.runNowOrWhenAppDidBecomeReadyAsync { [weak self] in
guard let self = self else { return }
guard CurrentAppContext().isMainApp else { return }
self.cleanUpExpiredPlaceholders()
}
}
@objc
func messageProcessorDidDrainQueue() {
// We don't want to send additional resets until we
// have received the "empty" response from the WebSocket
// or finished at least one REST fetch.
guard SSKEnvironment.shared.messageFetcherJobRef.hasCompletedInitialFetch else { return }
// We clear all recently reset sender ids any time the
// decryption queue has drained, so that any new messages
// that fail to decrypt will reset the session again.
senderIdsResetDuringCurrentBatch.removeAllObjects()
}
private func trySendNullMessage(
in contactThread: TSContactThread,
senderId: String,
transaction: SDSAnyWriteTransaction
) {
if RemoteConfig.current.automaticSessionResetKillSwitch {
Logger.warn("Skipping null message after undecryptable message from \(senderId) due to kill switch.")
return
}
let store = KeyValueStore(collection: "OWSMessageDecrypter+NullMessage")
let lastNullMessageDate = store.getDate(senderId, transaction: transaction.asV2Read)
let timeSinceNullMessage = abs(lastNullMessageDate?.timeIntervalSinceNow ?? .infinity)
guard timeSinceNullMessage > RemoteConfig.current.automaticSessionResetAttemptInterval else {
Logger.warn("Skipping null message after undecryptable message from \(senderId), " +
"last null message sent \(lastNullMessageDate!.ows_millisecondsSince1970).")
return
}
Logger.info("Sending null message to reset session after undecryptable message from: \(senderId)")
store.setDate(Date(), key: senderId, transaction: transaction.asV2Write)
transaction.addAsyncCompletionOffMain {
SSKEnvironment.shared.databaseStorageRef.write { transaction in
let nullMessage = OWSOutgoingNullMessage(contactThread: contactThread, transaction: transaction)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: nullMessage
)
SSKEnvironment.shared.messageSenderJobQueueRef.add(
.promise,
message: preparedMessage,
transaction: transaction
).done(on: DispatchQueue.global()) {
Logger.info("Successfully sent null message after session reset " +
"for undecryptable message from \(senderId)")
}.catch(on: DispatchQueue.global()) { error in
if error is UntrustedIdentityError {
Logger.info("Failed to send null message after session reset for " +
"for undecryptable message from \(senderId) (\(error))")
} else {
owsFailDebug("Failed to send null message after session reset " +
"for undecryptable message from \(senderId) (\(error))")
}
}
}
}
}
private func trySendReactiveProfileKey(to sourceAci: Aci, tx transaction: SDSAnyWriteTransaction) {
let store = KeyValueStore(collection: "OWSMessageDecrypter+ReactiveProfileKey")
let lastProfileKeyMessageDate = store.getDate(sourceAci.serviceIdUppercaseString, transaction: transaction.asV2Read)
let timeSinceProfileKeyMessage = abs(lastProfileKeyMessageDate?.timeIntervalSinceNow ?? .infinity)
guard timeSinceProfileKeyMessage > RemoteConfig.current.reactiveProfileKeyAttemptInterval else {
Logger.warn("Skipping reactive profile key for \(sourceAci), last reactive profile key message sent \(lastProfileKeyMessageDate!.ows_millisecondsSince1970).")
return
}
Logger.info("Sending reactive profile key to \(sourceAci)")
store.setDate(Date(), key: sourceAci.serviceIdUppercaseString, transaction: transaction.asV2Write)
let contactThread = TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(sourceAci),
transaction: transaction
)
let profileKeyMessage = OWSProfileKeyMessage(thread: contactThread, transaction: transaction)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: profileKeyMessage
)
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
}
private struct UnsealedEnvelope {
let sourceAci: Aci
let sourceDeviceId: UInt32
let content: Data?
let cipherType: CiphertextMessage.MessageType
let untrustedGroupId: Data?
let contentHint: SealedSenderContentHint
}
private func processError(
_ error: Error,
validatedEnvelope: ValidatedIncomingEnvelope,
unsealedEnvelope: UnsealedEnvelope?,
tx transaction: SDSAnyWriteTransaction
) -> Error {
let logString = "Error while decrypting \(Self.description(for: validatedEnvelope.envelope)), error: \(error)"
if case SignalError.duplicatedMessage(_) = error {
Logger.warn(logString)
// Duplicate messages are not recorded in the database.
return OWSError(error: .failedToDecryptDuplicateMessage,
description: "Duplicate message",
isRetryable: false,
userInfo: [NSUnderlyingErrorKey: error])
}
Logger.error(logString)
let wrappedError: Error
if (error as NSError).domain == OWSError.errorDomain {
wrappedError = error
} else {
wrappedError = OWSError(error: .failedToDecryptMessage,
description: "Decryption error",
isRetryable: false,
userInfo: [NSUnderlyingErrorKey: error])
}
guard let unsealedEnvelope else {
return wrappedError
}
let sourceAci = unsealedEnvelope.sourceAci
let sourceAddress = SignalServiceAddress(sourceAci)
if
SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(sourceAddress, transaction: transaction) ||
DependenciesBridge.shared.recipientHidingManager.isHiddenAddress(sourceAddress, tx: transaction.asV2Read)
{
Logger.info("Ignoring decryption error for blocked or hidden user \(sourceAddress) \(wrappedError).")
return wrappedError
}
let contactThread = TSContactThread.getOrCreateThread(withContactAddress: sourceAddress, transaction: transaction)
let errorMessage: TSErrorMessage?
switch validatedEnvelope.localIdentity {
case .aci:
if
!RemoteConfig.current.messageResendKillSwitch,
let modernResendErrorMessageBytes = buildResendRequestDecryptionError(
validatedEnvelope: validatedEnvelope,
unsealedEnvelope: unsealedEnvelope
)
{
Logger.info("Performing modern resend of \(unsealedEnvelope.contentHint) content with timestamp \(validatedEnvelope.timestamp)")
switch unsealedEnvelope.contentHint {
case .default:
// If default, insert an error message right away
errorMessage = .failedDecryption(
sender: sourceAddress,
groupId: unsealedEnvelope.untrustedGroupId,
timestamp: validatedEnvelope.timestamp,
tx: transaction
)
case .resendable:
// If resendable, insert a placeholder
let recoverableErrorMessage = OWSRecoverableDecryptionPlaceholder(
failedEnvelopeTimestamp: validatedEnvelope.timestamp,
sourceAci: AciObjC(sourceAci),
untrustedGroupId: unsealedEnvelope.untrustedGroupId,
transaction: transaction
)
if let recoverableErrorMessage {
schedulePlaceholderCleanupIfNecessary(for: recoverableErrorMessage)
}
errorMessage = recoverableErrorMessage
case .implicit:
errorMessage = nil
default:
owsFailDebug("Unexpected content hint")
errorMessage = nil
}
// We always send a resend request, even if the contentHint indicates the sender
// won't be able to fulfill the request. This will notify the sender to reset
// the session.
sendResendRequest(
errorMessageBytes: modernResendErrorMessageBytes,
sourceAci: sourceAci,
failedEnvelopeGroupId: unsealedEnvelope.untrustedGroupId,
transaction: transaction
)
} else {
Logger.info("Performing legacy session reset of \(unsealedEnvelope.contentHint) content with timestamp \(validatedEnvelope.timestamp)")
let didReset = resetSessionIfNecessary(
for: sourceAci,
sourceDeviceId: unsealedEnvelope.sourceDeviceId,
contactThread: contactThread,
transaction: transaction
)
if didReset {
// Always notify the user that we have performed an automatic archive.
errorMessage = .sessionRefresh(thread: contactThread)
} else {
errorMessage = nil
}
}
case .pni:
Logger.info("Not resetting or requesting resend of message sent to PNI.")
DependenciesBridge.shared.linkedDevicePniKeyManager
.recordSuspectedIssueWithPniIdentityKey(tx: transaction.asV2Write)
errorMessage = .failedDecryption(
sender: sourceAddress,
groupId: unsealedEnvelope.untrustedGroupId,
timestamp: validatedEnvelope.timestamp,
tx: transaction
)
}
switch error as? SignalError {
case .untrustedIdentity:
// Should no longer get here, since we now record the new identity for incoming messages.
owsFailDebug("Failed to trust identity on incoming message from \(sourceAci)")
case .duplicatedMessage:
preconditionFailure("checked above")
default: // another SignalError, or another kind of Error altogether
break
}
if let errorMessage = errorMessage {
errorMessage.anyInsert(transaction: transaction)
SSKEnvironment.shared.notificationPresenterRef.notifyUser(forErrorMessage: errorMessage, thread: contactThread, transaction: transaction)
SSKEnvironment.shared.notificationPresenterRef.notifyTestPopulation(ofErrorMessage: "Failed decryption of envelope: \(validatedEnvelope.timestamp)")
}
return wrappedError
}
private func buildResendRequestDecryptionError(
validatedEnvelope: ValidatedIncomingEnvelope,
unsealedEnvelope: UnsealedEnvelope
) -> Data? {
guard validatedEnvelope.localIdentity == .aci else {
return nil
}
guard [.whisper, .senderKey, .preKey, .plaintext].contains(unsealedEnvelope.cipherType) else {
return nil
}
guard let unsealedContent = unsealedEnvelope.content else {
return nil
}
do {
let errorMessage = try DecryptionErrorMessage(
originalMessageBytes: unsealedContent,
type: unsealedEnvelope.cipherType,
timestamp: validatedEnvelope.timestamp,
originalSenderDeviceId: unsealedEnvelope.sourceDeviceId
)
return Data(errorMessage.serialize())
} catch {
owsFailDebug("Could not build DecryptionError: \(error)")
return nil
}
}
private func sendResendRequest(
errorMessageBytes: Data,
sourceAci: Aci,
failedEnvelopeGroupId: Data?,
transaction: SDSAnyWriteTransaction
) {
let resendRequest = OWSOutgoingResendRequest(
errorMessageBytes: errorMessageBytes,
sourceAci: AciObjC(sourceAci),
failedEnvelopeGroupId: failedEnvelopeGroupId,
transaction: transaction
)
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: resendRequest
)
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
}
private func resetSessionIfNecessary(
for sourceAci: Aci,
sourceDeviceId: UInt32,
contactThread: TSContactThread,
transaction: SDSAnyWriteTransaction
) -> Bool {
// Since the message failed to decrypt, we want to reset our session
// with this device to ensure future messages we receive are decryptable.
// We achieve this by archiving our current session with this device.
// It's important we don't do this if we've already recently reset the
// session for a given device, for example if we're processing a backlog
// of 50 message from Alice that all fail to decrypt we don't want to
// reset the session 50 times. We accomplish this by tracking the UUID +
// device ID pair that we have recently reset, so we can skip subsequent
// resets. When the message decrypt queue is drained, the list of recently
// reset IDs is cleared.
let senderId = "\(sourceAci).\(sourceDeviceId)"
if !senderIdsResetDuringCurrentBatch.contains(senderId) {
senderIdsResetDuringCurrentBatch.add(senderId)
// We don't reset sessions for messages sent to our PNI because those are
// receive-only & we don't send retries FROM our PNI back to the sender.
Logger.warn("Archiving session for undecryptable message from \(senderId)")
let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
sessionStore.archiveSession(for: sourceAci, deviceId: sourceDeviceId, tx: transaction.asV2Write)
trySendNullMessage(in: contactThread, senderId: senderId, transaction: transaction)
return true
} else {
Logger.warn("Skipping session reset for undecryptable message from \(senderId), " +
"already reset during this batch")
return false
}
}
func decryptIdentifiedEnvelope(
_ validatedEnvelope: ValidatedIncomingEnvelope,
cipherType: CiphertextMessage.MessageType,
localIdentifiers: LocalIdentifiers,
tx transaction: SDSAnyWriteTransaction
) throws -> DecryptedIncomingEnvelope {
// This method is only used for identified envelopes. If an unidentified
// envelope is ever passed here, we'll reject on the next line because it
// won't have a source.
let (sourceAci, sourceDeviceId) = try validatedEnvelope.validateSource(Aci.self)
let localIdentity = validatedEnvelope.localIdentity
do {
guard let encryptedData = validatedEnvelope.envelope.content else {
throw OWSError(error: .failedToDecryptMessage,
description: "Envelope has no content",
isRetryable: false)
}
let identityManager = DependenciesBridge.shared.identityManager
let protocolAddress = ProtocolAddress(sourceAci, deviceId: sourceDeviceId)
let signalProtocolStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: validatedEnvelope.localIdentity)
let plaintext: [UInt8]
switch cipherType {
case .whisper:
let message = try SignalMessage(bytes: encryptedData)
plaintext = try signalDecrypt(
message: message,
from: protocolAddress,
sessionStore: signalProtocolStore.sessionStore,
identityStore: identityManager.libSignalStore(for: localIdentity, tx: transaction.asV2Write),
context: transaction
)
sendReactiveProfileKeyIfNecessary(to: sourceAci, tx: transaction)
case .preKey:
if DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction.asV2Read).isRegistered {
DependenciesBridge.shared.preKeyManager.checkPreKeysIfNecessary(tx: transaction.asV2Read)
}
let message = try PreKeySignalMessage(bytes: encryptedData)
plaintext = try signalDecryptPreKey(
message: message,
from: protocolAddress,
sessionStore: signalProtocolStore.sessionStore,
identityStore: identityManager.libSignalStore(for: localIdentity, tx: transaction.asV2Write),
preKeyStore: signalProtocolStore.preKeyStore,
signedPreKeyStore: signalProtocolStore.signedPreKeyStore,
kyberPreKeyStore: signalProtocolStore.kyberPreKeyStore,
context: transaction
)
case .senderKey:
plaintext = try groupDecrypt(
encryptedData,
from: protocolAddress,
store: SSKEnvironment.shared.senderKeyStoreRef,
context: transaction
)
case .plaintext:
let plaintextMessage = try PlaintextContent(bytes: encryptedData)
plaintext = plaintextMessage.body
// FIXME: return this to @unknown default once cipherType is represented
// as a finite enum.
default:
owsFailDebug("Unexpected ciphertext type: \(cipherType.rawValue)")
throw OWSError(error: .failedToDecryptMessage,
description: "Unexpected Ciphertext type.",
isRetryable: false)
}
let plaintextData = Data(plaintext).withoutPadding()
let decryptedEnvelope = DecryptedIncomingEnvelope(
validatedEnvelope: validatedEnvelope,
updatedEnvelope: validatedEnvelope.envelope,
sourceAci: sourceAci,
sourceDeviceId: sourceDeviceId,
wasReceivedByUD: false,
plaintextData: plaintextData
)
processDecryptedEnvelope(
decryptedEnvelope,
localIdentifiers: localIdentifiers,
mergeRecipient: { transaction in
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
return recipientFetcher.fetchOrCreate(serviceId: sourceAci, tx: transaction)
},
tx: transaction.asV2Write
)
return decryptedEnvelope
} catch {
throw processError(
error,
validatedEnvelope: validatedEnvelope,
unsealedEnvelope: UnsealedEnvelope(
sourceAci: sourceAci,
sourceDeviceId: sourceDeviceId,
content: validatedEnvelope.envelope.content,
cipherType: cipherType,
untrustedGroupId: nil,
contentHint: .default
),
tx: transaction
)
}
}
private func sendReactiveProfileKeyIfNecessary(to sourceAci: Aci, tx transaction: SDSAnyWriteTransaction) {
if DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)?.aci == sourceAci {
return
}
if SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(SignalServiceAddress(sourceAci), transaction: transaction) {
Logger.info("Skipping send of reactive profile key to blocked address")
return
}
// We do this work in an async completion so we don't delay
// receipt of this message.
transaction.addAsyncCompletionOffMain {
let needsReactiveProfileKeyMessage: Bool = SSKEnvironment.shared.databaseStorageRef.read { transaction in
// This user is whitelisted, they should have our profile key / be sending UD messages
// Send them our profile key in case they somehow lost it.
if SSKEnvironment.shared.profileManagerRef.isUser(
inProfileWhitelist: SignalServiceAddress(sourceAci),
transaction: transaction
) {
return true
}
// If we're in a V2 group with this user, they should also have our profile key /
// be sending UD messages. Send them it in case they somehow lost it.
var needsReactiveProfileKeyMessage = false
TSGroupThread.enumerateGroupThreads(
with: SignalServiceAddress(sourceAci),
transaction: transaction
) { thread, stop in
guard thread.isGroupV2Thread else { return }
guard thread.isLocalUserFullMember else { return }
stop.pointee = true
needsReactiveProfileKeyMessage = true
}
return needsReactiveProfileKeyMessage
}
if needsReactiveProfileKeyMessage {
SSKEnvironment.shared.databaseStorageRef.write { transaction in
self.trySendReactiveProfileKey(to: sourceAci, tx: transaction)
}
}
}
}
func decryptUnidentifiedSenderEnvelope(
_ validatedEnvelope: ValidatedIncomingEnvelope,
localIdentifiers: LocalIdentifiers,
localDeviceId: UInt32,
tx transaction: SDSAnyWriteTransaction
) throws -> DecryptedIncomingEnvelope {
let localIdentity = validatedEnvelope.localIdentity
guard let encryptedData = validatedEnvelope.envelope.content else {
throw OWSAssertionError("UD Envelope is missing content.")
}
let identityManager = DependenciesBridge.shared.identityManager
let signalProtocolStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: localIdentity)
let cipher = try SMKSecretSessionCipher(
sessionStore: signalProtocolStore.sessionStore,
preKeyStore: signalProtocolStore.preKeyStore,
signedPreKeyStore: signalProtocolStore.signedPreKeyStore,
kyberPreKeyStore: signalProtocolStore.kyberPreKeyStore,
identityStore: identityManager.libSignalStore(for: localIdentity, tx: transaction.asV2Write),
senderKeyStore: SSKEnvironment.shared.senderKeyStoreRef
)
let decryptResult: SMKDecryptResult
do {
decryptResult = try cipher.decryptMessage(
trustRoot: SSKEnvironment.shared.udManagerRef.trustRoot,
cipherTextData: encryptedData,
timestamp: validatedEnvelope.serverTimestamp,
localIdentifiers: localIdentifiers,
localDeviceId: localDeviceId,
protocolContext: transaction
)
} catch let outerError as SecretSessionKnownSenderError {
throw handleUnidentifiedSenderDecryptionError(
outerError.underlyingError,
validatedEnvelope: validatedEnvelope,
unsealedEnvelope: UnsealedEnvelope(
sourceAci: outerError.senderAci,
sourceDeviceId: outerError.senderDeviceId,
content: outerError.unsealedContent,
cipherType: outerError.cipherType,
untrustedGroupId: outerError.groupId,
contentHint: SealedSenderContentHint(outerError.contentHint)
),
transaction: transaction
)
} catch {
throw handleUnidentifiedSenderDecryptionError(
error,
validatedEnvelope: validatedEnvelope,
unsealedEnvelope: nil,
transaction: transaction
)
}
if
decryptResult.messageType == .prekey,
DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction.asV2Read).isRegistered
{
DependenciesBridge.shared.preKeyManager.checkPreKeysIfNecessary(tx: transaction.asV2Read)
}
let rawSourceDeviceId = decryptResult.senderDeviceId
guard rawSourceDeviceId > 0, rawSourceDeviceId < UInt32.max else {
throw OWSAssertionError("Invalid UD sender device id.")
}
let sourceDeviceId = rawSourceDeviceId
let envelopeBuilder = validatedEnvelope.envelope.asBuilder()
envelopeBuilder.setSourceServiceID(decryptResult.senderAci.serviceIdString)
envelopeBuilder.setSourceDevice(sourceDeviceId)
let decryptedEnvelope = DecryptedIncomingEnvelope(
validatedEnvelope: validatedEnvelope,
updatedEnvelope: try envelopeBuilder.build(),
sourceAci: decryptResult.senderAci,
sourceDeviceId: sourceDeviceId,
wasReceivedByUD: validatedEnvelope.envelope.sourceServiceID == nil,
plaintextData: decryptResult.paddedPayload.withoutPadding()
)
processDecryptedEnvelope(
decryptedEnvelope,
localIdentifiers: localIdentifiers,
mergeRecipient: { transaction in
return DependenciesBridge.shared.recipientMerger.applyMergeFromSealedSender(
localIdentifiers: localIdentifiers,
aci: decryptResult.senderAci,
phoneNumber: E164(decryptResult.senderE164),
tx: transaction
)
},
tx: transaction.asV2Write
)
return decryptedEnvelope
}
private func handleUnidentifiedSenderDecryptionError(
_ error: Error,
validatedEnvelope: ValidatedIncomingEnvelope,
unsealedEnvelope: UnsealedEnvelope?,
transaction: SDSAnyWriteTransaction
) -> Error {
switch error {
case SMKSecretSessionCipherError.selfSentMessage:
// Self-sent messages can be safely discarded. Return as-is.
return error
case is SignalError,
SSKPreKeyStore.Error.noPreKeyWithId(_),
SSKSignedPreKeyStore.Error.noPreKeyWithId(_),
SSKKyberPreKeyStore.Error.noKyberPreKeyWithId(_):
return processError(
error,
validatedEnvelope: validatedEnvelope,
unsealedEnvelope: unsealedEnvelope,
tx: transaction
)
default:
owsFailDebug("Could not decrypt UD message: \(error), source: \(String(describing: unsealedEnvelope?.sourceAci)), envelope: \(Self.description(for: validatedEnvelope.envelope))")
return error
}
}
private func processDecryptedEnvelope(
_ decryptedEnvelope: DecryptedIncomingEnvelope,
localIdentifiers: LocalIdentifiers,
mergeRecipient: (DBWriteTransaction) -> SignalRecipient,
tx: DBWriteTransaction
) {
// We need to handle the PNI signature first b/c `mergeRecipient()`
// might produce a visible event and the PNI signature won't.
handlePniSignatureIfNeeded(in: decryptedEnvelope, localIdentifiers: localIdentifiers, tx: tx)
let recipientManager = DependenciesBridge.shared.recipientManager
recipientManager.markAsRegisteredAndSave(
mergeRecipient(tx),
deviceId: decryptedEnvelope.sourceDeviceId,
shouldUpdateStorageService: true,
tx: tx
)
}
private func handlePniSignatureIfNeeded(
in envelope: DecryptedIncomingEnvelope,
localIdentifiers: LocalIdentifiers,
tx: DBWriteTransaction
) {
guard let pniSignatureMessage = envelope.content?.pniSignatureMessage else {
return
}
do {
try PniSignatureProcessorImpl(
identityManager: DependenciesBridge.shared.identityManager,
recipientDatabaseTable: DependenciesBridge.shared.recipientDatabaseTable,
recipientMerger: DependenciesBridge.shared.recipientMerger
).handlePniSignature(
pniSignatureMessage,
from: envelope.sourceAci,
localIdentifiers: localIdentifiers,
tx: tx
)
} catch {
Logger.warn("Ignoring Pni signature message: \(error)")
}
}
private func schedulePlaceholderCleanupIfNecessary(for placeholder: OWSRecoverableDecryptionPlaceholder) {
DispatchQueue.main.async {
self.schedulePlaceholderCleanup(noLaterThan: placeholder.expirationDate)
}
}
private func schedulePlaceholderCleanup(noLaterThan expirationDate: Date) {
let fireDate = placeholderCleanupTimer?.fireDate ?? .distantFuture
// Only change the fireDate if it's changed "enough", where we consider
// about 5 seconds of leeway sufficient.
let latestAcceptableFireDate = expirationDate.addingTimeInterval(5)
if latestAcceptableFireDate.isBefore(fireDate) {
placeholderCleanupTimer = Timer.scheduledTimer(
withTimeInterval: expirationDate.timeIntervalSinceNow,
repeats: false,
block: { [weak self] _ in self?.cleanUpExpiredPlaceholders() }
)
}
}
func cleanUpExpiredPlaceholders() {
Task { await self._cleanUpExpiredPlaceholders() }
}
private func _cleanUpExpiredPlaceholders() async {
let (expiredPlaceholderIds, nextExpirationDate) = SSKEnvironment.shared.databaseStorageRef.read { tx in
var expiredPlaceholderIds = [String]()
var nextExpirationDate: Date?
InteractionFinder.enumeratePlaceholders(transaction: tx) { placeholder in
guard placeholder.expirationDate.isBeforeNow else {
nextExpirationDate = [nextExpirationDate, placeholder.expirationDate].compacted().min()
return
}
expiredPlaceholderIds.append(placeholder.uniqueId)
}
return (expiredPlaceholderIds, nextExpirationDate)
}
let batchSize = 25
var remainingPlaceholderIds = expiredPlaceholderIds[...]
while !remainingPlaceholderIds.isEmpty {
let thisBatchPlaceholderIds = remainingPlaceholderIds.prefix(batchSize)
remainingPlaceholderIds = remainingPlaceholderIds.dropFirst(batchSize)
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
for placeholderId in thisBatchPlaceholderIds {
guard let placeholder = OWSRecoverableDecryptionPlaceholder.anyFetchRecoverableDecryptionPlaceholder(
uniqueId: placeholderId,
transaction: tx
) else {
continue
}
Logger.info("Cleaning up placeholder \(placeholder.timestamp)")
DependenciesBridge.shared.interactionDeleteManager
.delete(placeholder, sideEffects: .default(), tx: tx.asV2Write)
guard let thread = placeholder.thread(tx: tx) else {
return
}
let errorMessage: TSErrorMessage = .failedDecryption(
thread: thread,
timestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp(),
sender: placeholder.sender
)
errorMessage.anyInsert(transaction: tx)
SSKEnvironment.shared.notificationPresenterRef.notifyUser(forErrorMessage: errorMessage, thread: thread, transaction: tx)
}
}
}
if let nextExpirationDate {
await MainActor.run { self.schedulePlaceholderCleanup(noLaterThan: nextExpirationDate) }
}
}
// MARK: - OWSMessageHandler methods
private static func descriptionForEnvelopeType(_ envelope: SSKProtoEnvelope) -> String {
guard envelope.hasType else {
return "Missing Type."
}
switch envelope.unwrappedType {
case .unknown:
// Shouldn't happen
return "Unknown"
case .ciphertext:
return "SignalEncryptedMessage"
case .keyExchange:
// Unsupported
return "KeyExchange"
case .prekeyBundle:
return "PreKeyEncryptedMessage"
case .receipt:
return "DeliveryReceipt"
case .unidentifiedSender:
return "UnidentifiedSender"
case .senderkeyMessage:
return "SenderKey"
case .plaintextContent:
return "PlaintextContent"
}
}
static func description(for envelope: SSKProtoEnvelope) -> String {
return "<Envelope type: \(descriptionForEnvelopeType(envelope)), source: \(envelope.formattedAddress), timestamp: \(envelope.timestamp), serverTimestamp: \(envelope.serverTimestamp), serverGuid: \(envelope.serverGuid ?? "(null)"), content.length: \(envelope.content?.count ?? 0) />"
}
}