729 lines
31 KiB
Swift
729 lines
31 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
|
|
public class OWSMessageDecrypter {
|
|
|
|
private let senderIdsResetDuringCurrentBatch = AtomicValue<Set<String>>(Set(), lock: .init())
|
|
|
|
public init(appReadiness: AppReadiness) {
|
|
SwiftSingletons.register(self)
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(self.messageProcessorDidDrainQueue),
|
|
name: MessageProcessor.messageProcessorDidDrainQueue,
|
|
object: nil,
|
|
)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
func messageProcessorDidDrainQueue() {
|
|
Task {
|
|
// We don't want to send additional resets until we have received the
|
|
// "empty" response from the WebSocket.
|
|
guard await 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.update { $0.removeAll() }
|
|
}
|
|
}
|
|
|
|
private func trySendNullMessage(
|
|
in contactThread: TSContactThread,
|
|
senderId: String,
|
|
transaction: DBWriteTransaction,
|
|
) {
|
|
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)
|
|
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)
|
|
|
|
transaction.addSyncCompletion {
|
|
Task {
|
|
let db = DependenciesBridge.shared.db
|
|
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueueRef
|
|
|
|
await db.awaitableWrite { transaction in
|
|
let nullMessage = OutgoingNullMessage(contactThread: contactThread, tx: transaction)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: nullMessage,
|
|
)
|
|
messageSenderJobQueue.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: DBWriteTransaction) {
|
|
let store = KeyValueStore(collection: "OWSMessageDecrypter+ReactiveProfileKey")
|
|
|
|
let lastProfileKeyMessageDate = store.getDate(sourceAci.serviceIdUppercaseString, transaction: transaction)
|
|
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)
|
|
|
|
let contactThread = TSContactThread.getOrCreateThread(
|
|
withContactAddress: SignalServiceAddress(sourceAci),
|
|
transaction: transaction,
|
|
)
|
|
|
|
let profileManager = SSKEnvironment.shared.profileManagerRef
|
|
guard let profileKey = profileManager.localProfileKey(tx: transaction) else {
|
|
Logger.warn("Skipping reactive profile key because we don't have a profile key.")
|
|
return
|
|
}
|
|
|
|
let profileKeyMessage = ProfileKeyMessage(
|
|
thread: contactThread,
|
|
profileKey: profileKey,
|
|
tx: transaction,
|
|
)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: profileKeyMessage,
|
|
)
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
|
|
}
|
|
|
|
private struct UnsealedEnvelope {
|
|
let sourceAci: Aci
|
|
let sourceDeviceId: DeviceId
|
|
let content: Data?
|
|
let cipherType: CiphertextMessage.MessageType
|
|
let untrustedGroupId: Data?
|
|
let contentHint: SealedSenderContentHint
|
|
}
|
|
|
|
private func processError(
|
|
_ error: Error,
|
|
validatedEnvelope: ValidatedIncomingEnvelope,
|
|
unsealedEnvelope: UnsealedEnvelope?,
|
|
tx transaction: DBWriteTransaction,
|
|
) -> 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.warn(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)
|
|
{
|
|
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("Requesting 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 recoverableErrorMessage != nil {
|
|
DependenciesBridge.shared.decryptionPlaceholderExpirationJob.restart()
|
|
}
|
|
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.")
|
|
|
|
let identityKeyMismatchManager = DependenciesBridge.shared.identityKeyMismatchManager
|
|
identityKeyMismatchManager.recordSuspectedIssueWithPniIdentityKey(tx: transaction)
|
|
Task {
|
|
try await identityKeyMismatchManager.validateLocalPniIdentityKeyIfNecessary()
|
|
}
|
|
|
|
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.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.uint32Value,
|
|
)
|
|
return errorMessage.serialize()
|
|
} catch {
|
|
owsFailDebug("Could not build DecryptionError: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func sendResendRequest(
|
|
errorMessageBytes: Data,
|
|
sourceAci: Aci,
|
|
failedEnvelopeGroupId: Data?,
|
|
transaction: DBWriteTransaction,
|
|
) {
|
|
let resendRequest = OutgoingResendRequest(
|
|
errorMessageBytes: errorMessageBytes,
|
|
sourceAci: sourceAci,
|
|
failedEnvelopeGroupId: failedEnvelopeGroupId,
|
|
tx: transaction,
|
|
)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: resendRequest,
|
|
)
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
|
|
}
|
|
|
|
private func resetSessionIfNecessary(
|
|
for sourceAci: Aci,
|
|
sourceDeviceId: DeviceId,
|
|
contactThread: TSContactThread,
|
|
transaction: DBWriteTransaction,
|
|
) -> 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.update(block: { $0.insert(senderId).inserted }) {
|
|
// 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(forServiceId: sourceAci, deviceId: sourceDeviceId, tx: transaction)
|
|
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,
|
|
localDeviceId: DeviceId,
|
|
tx transaction: DBWriteTransaction,
|
|
) 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
|
|
let plaintextData: Data
|
|
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 signalProtocolStoreManager = DependenciesBridge.shared.signalProtocolStoreManager
|
|
let signalProtocolStore = signalProtocolStoreManager.signalProtocolStore(for: validatedEnvelope.localIdentity)
|
|
let preKeyStore = signalProtocolStoreManager.preKeyStore.forIdentity(validatedEnvelope.localIdentity)
|
|
|
|
let plaintext: Data
|
|
switch cipherType {
|
|
case .whisper:
|
|
let message = try SignalMessage(bytes: encryptedData)
|
|
plaintext = try signalDecrypt(
|
|
message: message,
|
|
from: protocolAddress,
|
|
to: ProtocolAddress(validatedEnvelope.localServiceId, deviceId: localDeviceId),
|
|
sessionStore: signalProtocolStore.sessionStore,
|
|
identityStore: identityManager.libSignalStore(for: localIdentity, tx: transaction),
|
|
context: transaction,
|
|
)
|
|
sendReactiveProfileKeyIfNecessary(to: sourceAci, tx: transaction)
|
|
case .preKey:
|
|
if DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction).isRegistered {
|
|
Task {
|
|
try? await DependenciesBridge.shared.preKeyManager.checkPreKeysIfNecessary()
|
|
}
|
|
}
|
|
let message = try PreKeySignalMessage(bytes: encryptedData)
|
|
plaintext = try signalDecryptPreKey(
|
|
message: message,
|
|
from: protocolAddress,
|
|
localAddress: ProtocolAddress(validatedEnvelope.localServiceId, deviceId: localDeviceId),
|
|
sessionStore: signalProtocolStore.sessionStore,
|
|
identityStore: identityManager.libSignalStore(for: localIdentity, tx: transaction),
|
|
preKeyStore: preKeyStore,
|
|
signedPreKeyStore: preKeyStore,
|
|
kyberPreKeyStore: preKeyStore,
|
|
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,
|
|
)
|
|
}
|
|
|
|
plaintextData = plaintext.withoutPadding()
|
|
} catch {
|
|
throw processError(
|
|
error,
|
|
validatedEnvelope: validatedEnvelope,
|
|
unsealedEnvelope: UnsealedEnvelope(
|
|
sourceAci: sourceAci,
|
|
sourceDeviceId: sourceDeviceId,
|
|
content: validatedEnvelope.envelope.content,
|
|
cipherType: cipherType,
|
|
untrustedGroupId: nil,
|
|
contentHint: .default,
|
|
),
|
|
tx: transaction,
|
|
)
|
|
}
|
|
|
|
let decryptedEnvelope = try DecryptedIncomingEnvelope(
|
|
validatedEnvelope: validatedEnvelope,
|
|
updatedEnvelope: validatedEnvelope.envelope,
|
|
sourceAci: sourceAci,
|
|
sourceDeviceId: sourceDeviceId,
|
|
wasReceivedByUD: false,
|
|
plaintextData: plaintextData,
|
|
isPlaintextCipher: cipherType == .plaintext,
|
|
)
|
|
|
|
processDecryptedEnvelope(
|
|
decryptedEnvelope,
|
|
localIdentifiers: localIdentifiers,
|
|
mergeRecipient: { transaction in
|
|
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
|
|
return recipientFetcher.fetchOrCreate(serviceId: sourceAci, tx: transaction)
|
|
},
|
|
tx: transaction,
|
|
)
|
|
|
|
return decryptedEnvelope
|
|
}
|
|
|
|
private func sendReactiveProfileKeyIfNecessary(to sourceAci: Aci, tx transaction: DBWriteTransaction) {
|
|
if DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction)?.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.addSyncCompletion {
|
|
Task {
|
|
let db = DependenciesBridge.shared.db
|
|
let profileManager = SSKEnvironment.shared.profileManagerRef
|
|
|
|
let needsReactiveProfileKeyMessage: Bool = db.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.
|
|
return profileManager.isUser(
|
|
inProfileWhitelist: SignalServiceAddress(sourceAci),
|
|
transaction: transaction,
|
|
)
|
|
}
|
|
|
|
if needsReactiveProfileKeyMessage {
|
|
await db.awaitableWrite { transaction in
|
|
self.trySendReactiveProfileKey(to: sourceAci, tx: transaction)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func decryptUnidentifiedSenderEnvelope(
|
|
_ validatedEnvelope: ValidatedIncomingEnvelope,
|
|
localIdentifiers: LocalIdentifiers,
|
|
localDeviceId: DeviceId,
|
|
tx transaction: DBWriteTransaction,
|
|
) 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 signalProtocolStoreManager = DependenciesBridge.shared.signalProtocolStoreManager
|
|
let signalProtocolStore = signalProtocolStoreManager.signalProtocolStore(for: localIdentity)
|
|
let preKeyStore = signalProtocolStoreManager.preKeyStore.forIdentity(localIdentity)
|
|
|
|
let cipher = try SMKSecretSessionCipher(
|
|
sessionStore: signalProtocolStore.sessionStore,
|
|
preKeyStore: preKeyStore,
|
|
signedPreKeyStore: preKeyStore,
|
|
kyberPreKeyStore: preKeyStore,
|
|
identityStore: identityManager.libSignalStore(for: localIdentity, tx: transaction),
|
|
senderKeyStore: SSKEnvironment.shared.senderKeyStoreRef,
|
|
)
|
|
|
|
let decryptResult: SMKDecryptResult
|
|
do {
|
|
decryptResult = try cipher.decryptMessage(
|
|
trustRoots: SSKEnvironment.shared.udManagerRef.trustRoots,
|
|
cipherTextData: encryptedData,
|
|
timestamp: validatedEnvelope.serverTimestamp,
|
|
localIdentifiers: localIdentifiers,
|
|
localServiceId: validatedEnvelope.localServiceId,
|
|
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).isRegistered
|
|
{
|
|
Task {
|
|
try? await DependenciesBridge.shared.preKeyManager.checkPreKeysIfNecessary()
|
|
}
|
|
}
|
|
|
|
let envelopeBuilder = validatedEnvelope.envelope.asBuilder()
|
|
envelopeBuilder.setSourceServiceIDBinary(decryptResult.senderAci.serviceIdBinary)
|
|
envelopeBuilder.setSourceDevice(decryptResult.senderDeviceId.uint32Value)
|
|
|
|
let decryptedEnvelope = try DecryptedIncomingEnvelope(
|
|
validatedEnvelope: validatedEnvelope,
|
|
updatedEnvelope: try envelopeBuilder.build(),
|
|
sourceAci: decryptResult.senderAci,
|
|
sourceDeviceId: decryptResult.senderDeviceId,
|
|
wasReceivedByUD: !(validatedEnvelope.envelope.hasSourceServiceID || validatedEnvelope.envelope.hasSourceServiceIDBinary),
|
|
plaintextData: decryptResult.paddedPayload.withoutPadding(),
|
|
isPlaintextCipher: decryptResult.messageType == .plaintext,
|
|
)
|
|
|
|
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,
|
|
)
|
|
|
|
return decryptedEnvelope
|
|
}
|
|
|
|
private func handleUnidentifiedSenderDecryptionError(
|
|
_ error: Error,
|
|
validatedEnvelope: ValidatedIncomingEnvelope,
|
|
unsealedEnvelope: UnsealedEnvelope?,
|
|
transaction: DBWriteTransaction,
|
|
) -> Error {
|
|
switch error {
|
|
case SMKSecretSessionCipherError.selfSentMessage:
|
|
// Self-sent messages can be safely discarded. Return as-is.
|
|
return error
|
|
case is SignalError, PreKeyStore.Error.noPreKeyWithId:
|
|
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
|
|
var mergedRecipient = mergeRecipient(tx)
|
|
recipientManager.markAsRegisteredAndSave(
|
|
&mergedRecipient,
|
|
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)")
|
|
}
|
|
}
|
|
|
|
// 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 .prekeyBundle:
|
|
return "PreKeyEncryptedMessage"
|
|
case .receipt:
|
|
return "DeliveryReceipt"
|
|
case .unidentifiedSender:
|
|
return "UnidentifiedSender"
|
|
case .plaintextContent:
|
|
return "PlaintextContent"
|
|
}
|
|
}
|
|
|
|
static func description(for envelope: SSKProtoEnvelope) -> String {
|
|
let serverGuid = ValidatedIncomingEnvelope.parseServerGuid(fromEnvelope: envelope)
|
|
return "<Envelope type: \(descriptionForEnvelopeType(envelope)), source: \(envelope.formattedAddress), timestamp: \(envelope.timestamp), serverTimestamp: \(envelope.serverTimestamp), serverGuid: \(serverGuid?.uuidString.lowercased() ?? "(null)"), content.length: \(envelope.content?.count ?? 0) />"
|
|
}
|
|
}
|