Signal-iOS/SignalServiceKit/Messages/MessageReceiver.swift

2136 lines
88 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import LibSignalClient
import SignalCoreKit
/// An ObjC wrapper around UnidentifiedSenderMessageContent.ContentHint
@objc
public enum SealedSenderContentHint: Int, Codable, CustomStringConvertible {
/// Indicates that the content of a message requires rendering user-visible errors immediately
/// upon decryption failure. It is not expected that it will be resent, and we make no attempt
/// to preserve ordering if it is.
/// Insert a placeholder: No
/// Show error to user: Yes, immediately
case `default` = 0
/// Indicates that the content of a message requires rendering user-visible errors upon
/// decryption failure, but errors will be delayed in case the message is resent before
/// the user attempts to view it. In order to facilitate insertion of resent messages in situ, a
/// placeholder is inserted into the interaction table to reserve its positioning.
/// Insert a placeholder: Yes
/// Show error to user: Yes, after some deferral period
case resendable
/// Indicates that the content of a message does not require rendering any user-visible
/// errors upon decryption failure. These messages may be resent, but we don't insert
/// a placeholder for them so their ordering is not preserved.
/// Insert a placeholder: No
/// Show error to user: No
case implicit
init(_ signalClientHint: UnidentifiedSenderMessageContent.ContentHint) {
switch signalClientHint {
case .default: self = .default
case .resendable: self = .resendable
case .implicit: self = .implicit
default:
owsFailDebug("Unspecified case \(signalClientHint)")
self = .default
}
}
public var signalClientHint: UnidentifiedSenderMessageContent.ContentHint {
switch self {
case .default: return .default
case .resendable: return .resendable
case .implicit: return .implicit
}
}
public var description: String {
switch self {
case .default: return "default"
case .resendable: return "resendable"
case .implicit: return "implicit"
}
}
}
// MARK: -
public final class MessageReceiver: Dependencies {
private let callMessageHandler: any CallMessageHandler
private let deleteForMeSyncMessageReceiver: any DeleteForMeSyncMessageReceiver
init(
callMessageHandler: any CallMessageHandler,
deleteForMeSyncMessageReceiver: any DeleteForMeSyncMessageReceiver
) {
self.callMessageHandler = callMessageHandler
self.deleteForMeSyncMessageReceiver = deleteForMeSyncMessageReceiver
}
private static let pendingTasks = PendingTasks(label: "messageReceiver")
public static func pendingTasksPromise() -> Promise<Void> {
// This promise blocks on all pending tasks already in flight,
// but will not block on new tasks enqueued after this promise
// is created. That's intentional to ensure that NotificationService
// instances complete in a timely way.
pendingTasks.pendingTasksPromise()
}
public static func buildPendingTask(label: String) -> PendingTask {
Self.pendingTasks.buildPendingTask(label: label)
}
/// Performs a limited amount of time sensitive processing before scheduling
/// the remainder of message processing
///
/// Currently, the preprocess step only parses sender key distribution
/// messages to update the sender key store. It's important the sender key
/// store is updated *before* the write transaction completes since we don't
/// know if the next message to be decrypted will depend on the sender key
/// store being up to date.
///
/// Some other things worth noting:
///
/// - We should preprocess *all* envelopes, even those where the sender is
/// blocked. This is important because it protects us from a case where the
/// recipient blocks and then unblocks a user. If the sender they blocked
/// sent an SKDM while the user was blocked, their understanding of the
/// world is that we have saved the SKDM. After unblock, if we don't have
/// the SKDM we'll fail to decrypt.
///
/// - This *needs* to happen in the very same write transaction where the
/// message was decrypted. It's important to keep in mind that the NSE could
/// race with the main app when processing messages. The write transaction
/// is used to protect us from any races.
func preprocessEnvelope(
_ decryptedEnvelope: DecryptedIncomingEnvelope,
tx: SDSAnyWriteTransaction
) {
// Currently, this function is only used for SKDM processing. Since this is
// idempotent, we don't need to check for a duplicate envelope.
//
// SKDM processing is also not user-visible, so we don't want to skip if
// the sender is blocked. This ensures that we retain session info to
// decrypt future messages from a blocked sender if they're ever unblocked.
if let skdmBytes = decryptedEnvelope.content?.senderKeyDistributionMessage {
handleIncomingEnvelope(decryptedEnvelope, withSenderKeyDistributionMessage: skdmBytes, transaction: tx)
}
}
public func processEnvelope(
_ envelope: SSKProtoEnvelope,
plaintextData: Data?,
wasReceivedByUD: Bool,
serverDeliveryTimestamp: UInt64,
shouldDiscardVisibleMessages: Bool,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
do {
let validatedEnvelope = try ValidatedIncomingEnvelope(envelope, localIdentifiers: localIdentifiers)
switch validatedEnvelope.kind {
case .unidentifiedSender, .identifiedSender:
// At this point, unidentifiedSender envelopes have already been updated
// with the sourceAci, so we should be able to parse it from the envelope.
let (sourceAci, sourceDeviceId) = try validatedEnvelope.validateSource(Aci.self)
if blockingManager.isAddressBlocked(SignalServiceAddress(sourceAci), transaction: tx) {
return
}
guard let plaintextData else {
throw OWSAssertionError("Missing plaintextData.")
}
let decryptedEnvelope = DecryptedIncomingEnvelope(
validatedEnvelope: validatedEnvelope,
updatedEnvelope: validatedEnvelope.envelope,
sourceAci: sourceAci,
sourceDeviceId: sourceDeviceId,
wasReceivedByUD: wasReceivedByUD,
plaintextData: plaintextData
)
checkForUnknownLinkedDevice(in: decryptedEnvelope, tx: tx)
let buildResult = MessageReceiverRequest.buildRequest(
for: decryptedEnvelope,
serverDeliveryTimestamp: serverDeliveryTimestamp,
shouldDiscardVisibleMessages: shouldDiscardVisibleMessages,
tx: tx
)
switch buildResult {
case .discard:
break
case .request(let messageReceiverRequest):
handleRequest(
messageReceiverRequest,
context: PassthroughDeliveryReceiptContext(),
localIdentifiers: localIdentifiers,
tx: tx
)
fallthrough
case .noContent:
finishProcessingEnvelope(decryptedEnvelope, tx: tx)
}
case .serverReceipt:
owsAssertDebug(plaintextData == nil)
let envelope = try ServerReceiptEnvelope(validatedEnvelope)
handleDeliveryReceipt(envelope: envelope, context: PassthroughDeliveryReceiptContext(), tx: tx)
}
} catch {
Logger.warn("Dropping invalid envelope \(error)")
}
}
func handleRequest(
_ request: MessageReceiverRequest,
context: DeliveryReceiptContext,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
let protoContent = request.protoContent
Logger.info("Received \(request.decryptedEnvelope.timestamp) w/\(protoContent.contentDescription) from \(request.decryptedEnvelope.sourceAci)")
switch request.messageType {
case .syncMessage(let syncMessage):
handleIncomingEnvelope(request: request, syncMessage: syncMessage, localIdentifiers: localIdentifiers, tx: tx)
DependenciesBridge.shared.deviceManager.setHasReceivedSyncMessage(transaction: tx.asV2Write)
case .dataMessage(let dataMessage):
handleIncomingEnvelope(request: request, dataMessage: dataMessage, localIdentifiers: localIdentifiers, tx: tx)
case .callMessage(let callMessage):
owsAssertDebug(!request.shouldDiscardVisibleMessages)
handleIncomingEnvelope(request: request, callMessage: callMessage, localIdentifiers: localIdentifiers, tx: tx)
case .typingMessage(let typingMessage):
handleIncomingEnvelope(request: request, typingMessage: typingMessage, tx: tx)
case .nullMessage:
Logger.info("Received null message.")
case .receiptMessage(let receiptMessage):
handleIncomingEnvelope(request: request, receiptMessage: receiptMessage, context: context, tx: tx)
case .decryptionErrorMessage(let decryptionErrorMessage):
handleIncomingEnvelope(request: request, decryptionErrorMessage: decryptionErrorMessage, tx: tx)
case .storyMessage(let storyMessage):
handleIncomingEnvelope(request: request, storyMessage: storyMessage, localIdentifiers: localIdentifiers, tx: tx)
case .editMessage(let editMessage):
let result = handleIncomingEnvelope(request: request, editMessage: editMessage, tx: tx)
switch result {
case .success, .invalidEdit:
break
case .editedMessageMissing:
earlyMessageManager.recordEarlyEnvelope(
request.envelope,
plainTextData: request.plaintextData,
wasReceivedByUD: request.wasReceivedByUD,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
associatedMessageTimestamp: editMessage.targetSentTimestamp,
associatedMessageAuthor: request.decryptedEnvelope.sourceAciObjC,
transaction: tx
)
}
case .handledElsewhere:
break
case .none:
Logger.warn("Ignoring envelope with unknown type.")
}
}
/// This code path is for server-generated receipts only.
func handleDeliveryReceipt(
envelope: ServerReceiptEnvelope,
context: DeliveryReceiptContext,
tx: SDSAnyWriteTransaction
) {
// Server-generated delivery receipts don't include a "delivery timestamp".
// The envelope's timestamp gives the timestamp of the message this receipt
// is for. Unlike UD receipts, it is not meant to be the time the message
// was delivered. We use the current time as a good-enough guess. We could
// also use the envelope's serverTimestamp.
let deliveryTimestamp = NSDate.ows_millisecondTimeStamp()
guard SDS.fitsInInt64(deliveryTimestamp) else {
owsFailDebug("Invalid timestamp.")
return
}
let earlyReceiptTimestamps = receiptManager.processDeliveryReceipts(
from: envelope.sourceServiceId,
recipientDeviceId: envelope.sourceDeviceId,
sentTimestamps: [ envelope.timestamp ],
deliveryTimestamp: deliveryTimestamp,
context: context,
tx: tx
)
recordEarlyReceipts(
receiptType: .delivery,
senderServiceId: envelope.sourceServiceId,
senderDeviceId: envelope.sourceDeviceId,
associatedMessageTimestamps: earlyReceiptTimestamps,
actionTimestamp: deliveryTimestamp,
tx: tx
)
}
/// Called when we've finished processing an envelope.
///
/// If we call this method, we tried to process an envelope. However, the
/// contents of that envelope may or may not be valid.
///
/// Cases where we won't call this method:
/// - The envelope is missing a sender (or a device ID)
/// - The envelope has a sender but they're blocked
/// - The envelope is missing a timestamp
/// - The user isn't registered
///
/// Cases where we will call this method:
/// - The envelope contains a fully valid message
/// - The envelope contains a message with an invalid reaction
/// - The envelope contains a link preview but the URL isn't in the message
/// - & so on, for many "errors" that are handled elsewhere
func finishProcessingEnvelope(_ decryptedEnvelope: DecryptedIncomingEnvelope, tx: SDSAnyWriteTransaction) {
saveSpamReportingToken(for: decryptedEnvelope, tx: tx)
clearLeftoverPlaceholders(for: decryptedEnvelope, tx: tx)
}
private func saveSpamReportingToken(for decryptedEnvelope: DecryptedIncomingEnvelope, tx: SDSAnyWriteTransaction) {
guard
let rawSpamReportingToken = decryptedEnvelope.envelope.spamReportingToken,
let spamReportingToken = SpamReportingToken(data: rawSpamReportingToken)
else {
return
}
do {
try SpamReportingTokenRecord(
sourceAci: decryptedEnvelope.sourceAci,
spamReportingToken: spamReportingToken
).upsert(tx.unwrapGrdbWrite.database)
} catch {
owsFailBeta(
"Couldn't save spam reporting token record. Continuing on, to avoid interrupting message processing. Error: \(error)"
)
}
}
/// Clear any remaining placeholders for a fully-processed message.
///
/// We need to check to make sure that we clear any placeholders that may
/// have been inserted for this message. This would happen if:
///
/// - This is a resend of a message that we had previously failed to decrypt
///
/// - The message does not result in an inserted TSIncomingMessage or
/// TSOutgoingMessage. For example, a read receipt. In that case, we should
/// just clear the placeholder.
private func clearLeftoverPlaceholders(for envelope: DecryptedIncomingEnvelope, tx: SDSAnyWriteTransaction) {
do {
let placeholders = try InteractionFinder.interactions(
withTimestamp: envelope.timestamp,
filter: { ($0 as? OWSRecoverableDecryptionPlaceholder)?.sender?.serviceId == envelope.sourceAci },
transaction: tx
)
owsAssertDebug(placeholders.count <= 1)
for placeholder in placeholders {
DependenciesBridge.shared.interactionDeleteManager
.delete(placeholder, sideEffects: .default(), tx: tx.asV2Write)
}
} catch {
owsFailDebug("Failed to fetch placeholders: \(error)")
}
}
private func groupId(for dataMessage: SSKProtoDataMessage) -> Data? {
guard let groupContext = dataMessage.groupV2 else {
return nil
}
guard let masterKey = groupContext.masterKey else {
owsFailDebug("Missing masterKey.")
return nil
}
do {
return try groupsV2.groupV2ContextInfo(forMasterKeyData: masterKey).groupId
} catch {
owsFailDebug("Invalid group context.")
return nil
}
}
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
syncMessage: SSKProtoSyncMessage,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
let decryptedEnvelope = request.decryptedEnvelope
guard decryptedEnvelope.sourceAci == localIdentifiers.aci else {
// Sync messages should only come from linked devices.
owsFailDebug("Received sync message from another user.")
return
}
let envelope = decryptedEnvelope.envelope
if let sent = syncMessage.sent {
guard SDS.fitsInInt64(sent.timestamp) else {
owsFailDebug("Invalid timestamp.")
return
}
if let dataMessage = sent.message {
let groupId: Data? = groupId(for: dataMessage)
guard SDS.fitsInInt64(sent.expirationStartTimestamp) else {
owsFailDebug("Invalid expirationStartTimestamp.")
return
}
guard let transcript = OWSIncomingSentMessageTranscript.from(
sentProto: sent,
serverTimestamp: decryptedEnvelope.serverTimestamp,
tx: tx.asV2Write
) else {
owsFailDebug("Couldn't parse transcript.")
return
}
if dataMessage.hasProfileKey {
if let groupId {
profileManager.addGroupId(
toProfileWhitelist: groupId, userProfileWriter: .localUser, transaction: tx
)
} else {
// If we observe a linked device sending our profile key to another user,
// we can infer that that user belongs in our profile whitelist.
let destinationAddress = SignalServiceAddress.legacyAddress(
serviceIdString: sent.destinationServiceID,
phoneNumber: sent.destinationE164
)
if destinationAddress.isValid {
profileManager.addUser(
toProfileWhitelist: destinationAddress, userProfileWriter: .localUser, transaction: tx
)
}
}
}
if let reaction = dataMessage.reaction {
guard let thread = transcript.threadForDataMessage else {
owsFailDebug("Could not process reaction from sync transcript.")
return
}
let result = ReactionManager.processIncomingReaction(
reaction,
thread: thread,
reactor: decryptedEnvelope.sourceAci,
timestamp: sent.timestamp,
serverTimestamp: decryptedEnvelope.serverTimestamp,
expiresInSeconds: dataMessage.expireTimer,
sentTranscript: transcript,
transaction: tx
)
switch result {
case .success, .invalidReaction:
break
case .associatedMessageMissing:
let messageAuthor = Aci.parseFrom(aciString: reaction.targetAuthorAci)
earlyMessageManager.recordEarlyEnvelope(
envelope,
plainTextData: request.plaintextData,
wasReceivedByUD: request.wasReceivedByUD,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
associatedMessageTimestamp: reaction.timestamp,
associatedMessageAuthor: messageAuthor.map { AciObjC($0) },
transaction: tx
)
}
} else if let delete = dataMessage.delete {
let result = TSMessage.tryToRemotelyDeleteMessage(
fromAuthor: decryptedEnvelope.sourceAci,
sentAtTimestamp: delete.targetSentTimestamp,
threadUniqueId: transcript.threadForDataMessage?.uniqueId,
serverTimestamp: decryptedEnvelope.serverTimestamp,
transaction: tx
)
switch result {
case .success:
break
case .invalidDelete:
Logger.error("Failed to remotely delete message \(delete.targetSentTimestamp)")
case .deletedMessageMissing:
earlyMessageManager.recordEarlyEnvelope(
envelope,
plainTextData: request.plaintextData,
wasReceivedByUD: request.wasReceivedByUD,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
associatedMessageTimestamp: delete.targetSentTimestamp,
associatedMessageAuthor: decryptedEnvelope.sourceAciObjC,
transaction: tx
)
}
} else if let groupCallUpdate = dataMessage.groupCallUpdate {
if let groupId, let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: tx) {
let pendingTask = MessageReceiver.buildPendingTask(label: "GroupCallUpdate")
Task { [callMessageHandler] in
defer { pendingTask.complete() }
await callMessageHandler.receivedGroupCallUpdateMessage(
groupCallUpdate,
for: groupThread,
serverReceivedTimestamp: decryptedEnvelope.timestamp
)
}
} else {
Logger.warn("Received GroupCallUpdate for unknown groupId")
}
} else {
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read) else {
owsFailDebug("Missing local identifiers!")
return
}
DependenciesBridge.shared.sentMessageTranscriptReceiver.process(
transcript,
localIdentifiers: localIdentifiers,
tx: tx.asV2Write
)
}
} else if sent.isStoryTranscript {
do {
try StoryManager.processStoryMessageTranscript(sent, transaction: tx)
} catch {
owsFailDebug("Failed to process story message transcript \(error)")
}
} else if let editMessage = sent.editMessage {
let result = handleIncomingEnvelope(
decryptedEnvelope, sentMessage: sent, editMessage: editMessage, transaction: tx
)
switch result {
case .success, .invalidEdit:
break
case .editedMessageMissing:
earlyMessageManager.recordEarlyEnvelope(
envelope,
plainTextData: request.plaintextData,
wasReceivedByUD: request.wasReceivedByUD,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
associatedMessageTimestamp: editMessage.targetSentTimestamp,
associatedMessageAuthor: decryptedEnvelope.sourceAciObjC,
transaction: tx
)
}
}
} else if let request = syncMessage.request {
handleIncomingSyncRequest(request, tx: tx)
} else if let blocked = syncMessage.blocked {
Logger.info("Received blocked sync message.")
handleSyncedBlocklist(blocked, tx: tx)
} else if !syncMessage.read.isEmpty {
let earlyReceipts = receiptManager.processReadReceiptsFromLinkedDevice(
syncMessage.read, readTimestamp: decryptedEnvelope.timestamp, tx: tx
)
for readReceiptProto in earlyReceipts {
let messageAuthor = Aci.parseFrom(aciString: readReceiptProto.senderAci)
earlyMessageManager.recordEarlyReadReceiptFromLinkedDevice(
timestamp: decryptedEnvelope.timestamp,
associatedMessageTimestamp: readReceiptProto.timestamp,
associatedMessageAuthor: messageAuthor.map { AciObjC($0) },
transaction: tx
)
}
} else if !syncMessage.viewed.isEmpty {
let earlyReceipts = receiptManager.processViewedReceiptsFromLinkedDevice(
syncMessage.viewed, viewedTimestamp: decryptedEnvelope.timestamp, tx: tx
)
for viewedReceiptProto in earlyReceipts {
let messageAuthor = Aci.parseFrom(aciString: viewedReceiptProto.senderAci)
earlyMessageManager.recordEarlyViewedReceiptFromLinkedDevice(
timestamp: decryptedEnvelope.timestamp,
associatedMessageTimestamp: viewedReceiptProto.timestamp,
associatedMessageAuthor: messageAuthor.map { AciObjC($0) },
transaction: tx
)
}
} else if let verified = syncMessage.verified {
do {
let identityManager = DependenciesBridge.shared.identityManager
try identityManager.processIncomingVerifiedProto(verified, tx: tx.asV2Write)
identityManager.fireIdentityStateChangeNotification(after: tx.asV2Write)
} catch {
Logger.warn("Couldn't process verification state \(error)")
}
} else if !syncMessage.stickerPackOperation.isEmpty {
Logger.info("Received sticker pack operation(s): \(syncMessage.stickerPackOperation.count)")
for packOperationProto in syncMessage.stickerPackOperation {
StickerManager.processIncomingStickerPackOperation(packOperationProto, transaction: tx)
}
} else if let viewOnceOpen = syncMessage.viewOnceOpen {
Logger.info("Received view-once read receipt sync message")
let result = ViewOnceMessages.processIncomingSyncMessage(viewOnceOpen, envelope: envelope, transaction: tx)
switch result {
case .success, .invalidSyncMessage:
break
case .associatedMessageMissing(let senderAci, let associatedMessageTimestamp):
earlyMessageManager.recordEarlyEnvelope(
envelope,
plainTextData: request.plaintextData,
wasReceivedByUD: request.wasReceivedByUD,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
associatedMessageTimestamp: associatedMessageTimestamp,
associatedMessageAuthor: AciObjC(senderAci),
transaction: tx
)
}
} else if let configuration = syncMessage.configuration {
syncManager.processIncomingConfigurationSyncMessage(configuration, transaction: tx)
} else if let contacts = syncMessage.contacts {
syncManager.processIncomingContactsSyncMessage(contacts, transaction: tx)
} else if let fetchLatest = syncMessage.fetchLatest {
syncManager.processIncomingFetchLatestSyncMessage(fetchLatest, transaction: tx)
} else if let keys = syncMessage.keys {
syncManager.processIncomingKeysSyncMessage(keys, transaction: tx)
} else if let messageRequestResponse = syncMessage.messageRequestResponse {
syncManager.processIncomingMessageRequestResponseSyncMessage(messageRequestResponse, transaction: tx)
} else if let outgoingPayment = syncMessage.outgoingPayment {
// An "incoming" sync message notifies us of an "outgoing" payment.
paymentsHelper.processIncomingPaymentSyncMessage(
outgoingPayment, messageTimestamp: request.serverDeliveryTimestamp, transaction: tx
)
} else if let pniChangeNumber = syncMessage.pniChangeNumber {
let pniProcessor = DependenciesBridge.shared.incomingPniChangeNumberProcessor
pniProcessor.processIncomingPniChangePhoneNumber(
proto: pniChangeNumber,
updatedPni: envelope.updatedPni,
tx: tx.asV2Write
)
} else if let callEvent = syncMessage.callEvent {
guard let incomingCallEvent = try? IncomingCallEventSyncMessageParams.parse(
callEventProto: callEvent
) else {
CallRecordLogger.shared.warn("Failed to parse incoming call event protobuf!")
return
}
DependenciesBridge.shared.incomingCallEventSyncMessageManager
.createOrUpdateRecordForIncomingSyncMessage(
incomingSyncMessage: incomingCallEvent,
syncMessageTimestamp: decryptedEnvelope.timestamp,
tx: tx.asV2Write
)
} else if let callLogEvent = syncMessage.callLogEvent {
guard
let incomingCallLogEvent = try? IncomingCallLogEventSyncMessageParams.parse(
callLogEvent: callLogEvent
)
else {
CallRecordLogger.shared.warn("Failed to parse incoming call log event protobuf!")
return
}
DependenciesBridge.shared.incomingCallLogEventSyncMessageManager
.handleIncomingSyncMessage(
incomingSyncMessage: incomingCallLogEvent,
tx: tx.asV2Write
)
} else if let deleteForMe = syncMessage.deleteForMe {
deleteForMeSyncMessageReceiver.handleDeleteForMeProto(
deleteForMeProto: deleteForMe,
tx: tx.asV2Write
)
} else {
Logger.warn("Ignoring unsupported sync message.")
}
}
private func handleIncomingSyncRequest(_ request: SSKProtoSyncMessageRequest, tx: SDSAnyWriteTransaction) {
guard DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx.asV2Read).isRegisteredPrimaryDevice else {
// Don't respond to sync requests from a linked device.
return
}
switch request.type {
case .contacts:
// We respond asynchronously because populating the sync message will
// create transactions and it's not practical (due to locking in the
// OWSIdentityManager) to plumb our transaction through.
//
// In rare cases this means we won't respond to the sync request, but
// that's acceptable.
let pendingTask = Self.buildPendingTask(label: "syncAllContacts")
DispatchQueue.global().async {
self.syncManager.syncAllContacts().ensure(on: DispatchQueue.global()) {
pendingTask.complete()
}.catch(on: DispatchQueue.global()) { error in
Logger.warn("Error: \(error)")
}
}
case .blocked:
let pendingTask = Self.buildPendingTask(label: "syncBlockList")
blockingManager.syncBlockList { pendingTask.complete() }
case .configuration:
// We send _two_ responses to the "configuration request".
syncManager.sendConfigurationSyncMessage()
StickerManager.syncAllInstalledPacks(transaction: tx)
case .keys:
syncManager.sendKeysSyncMessage()
case .unknown, .none:
owsFailDebug("Ignoring sync request with unexpected type")
}
}
private func handleSyncedBlocklist(_ blocked: SSKProtoSyncMessageBlocked, tx: SDSAnyWriteTransaction) {
var blockedAcis = Set<Aci>()
for aciString in blocked.acis {
guard let aci = Aci.parseFrom(aciString: aciString) else {
owsFailDebug("Blocked ACI was nil.")
continue
}
blockedAcis.insert(aci)
}
blockingManager.processIncomingSync(
blockedPhoneNumbers: Set(blocked.numbers),
blockedAcis: blockedAcis,
blockedGroupIds: Set(blocked.groupIds),
tx: tx
)
}
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
dataMessage: SSKProtoDataMessage,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
let envelope = request.decryptedEnvelope
if let groupId = self.groupId(for: dataMessage) {
if blockingManager.isGroupIdBlocked(groupId, transaction: tx) {
Logger.warn("Ignoring blocked message from \(envelope.sourceAci) in group \(groupId)")
return
}
}
// This prevents replay attacks by the service.
guard !dataMessage.hasTimestamp || dataMessage.timestamp == envelope.timestamp else {
owsFailDebug("Ignoring message with non-matching data message timestamp from \(envelope.sourceAci)")
return
}
if let profileKey = dataMessage.profileKey {
setProfileKeyIfValid(profileKey, for: envelope.sourceAci, localIdentifiers: localIdentifiers, tx: tx)
}
// Pre-process the data message. For v1 and v2 group messages this involves
// checking group state, possibly creating the group thread, possibly
// responding to group info requests, etc.
//
// If we can and should try to "process" (e.g. generate user-visible
// interactions) for the data message, preprocessDataMessage will return a
// thread. If not, we should abort immediately.
guard let thread = preprocessDataMessage(dataMessage, envelope: envelope, tx: tx) else {
return
}
var message: TSIncomingMessage?
if dataMessage.flags & UInt32(SSKProtoDataMessageFlags.endSession.rawValue) != 0 {
handleIncomingEndSessionEnvelope(envelope, withDataMessage: dataMessage, tx: tx)
} else if dataMessage.flags & UInt32(SSKProtoDataMessageFlags.expirationTimerUpdate.rawValue) != 0 {
updateDisappearingMessageConfiguration(envelope: envelope, dataMessage: dataMessage, thread: thread, tx: tx)
} else if dataMessage.flags & UInt32(SSKProtoDataMessageFlags.profileKeyUpdate.rawValue) != 0 {
// Do nothing, we handle profile keys on all incoming messages above.
} else {
message = processFlaglessDataMessage(dataMessage, request: request, thread: thread, tx: tx)
}
// Send delivery receipts for "valid data" messages received via UD.
if request.wasReceivedByUD {
let receiptSender = SSKEnvironment.shared.receiptSenderRef
receiptSender.enqueueDeliveryReceipt(for: envelope, messageUniqueId: message?.uniqueId, tx: tx)
}
}
private func setProfileKeyIfValid(
_ profileKey: Data,
for aci: Aci,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
if aci == localIdentifiers.aci, tsAccountManager.registrationState(tx: tx.asV2Read).isPrimaryDevice != false {
return
}
profileManager.setProfileKeyData(
profileKey,
for: aci,
onlyFillInIfMissing: false,
shouldFetchProfile: true,
userProfileWriter: .localUser,
localIdentifiers: localIdentifiers,
authedAccount: .implicit(),
tx: tx.asV2Write
)
}
/// Returns a thread reference if message processing should proceed.
///
/// Message processing involves generating user-visible interactions, and we
/// don't do that if there's an invalid group context, or if there's a valid
/// group context but the sender/local user aren't in the group.
private func preprocessDataMessage(
_ dataMessage: SSKProtoDataMessage,
envelope: DecryptedIncomingEnvelope,
tx: SDSAnyWriteTransaction
) -> TSThread? {
guard let groupContext = dataMessage.groupV2 else {
let contactAddress = SignalServiceAddress(envelope.sourceAci)
return TSContactThread.getOrCreateThread(withContactAddress: contactAddress, transaction: tx)
}
guard let masterKey = groupContext.masterKey else {
owsFailDebug("Missing masterKey.")
return nil
}
let groupContextInfo: GroupV2ContextInfo
do {
groupContextInfo = try groupsV2.groupV2ContextInfo(forMasterKeyData: masterKey)
} catch {
owsFailDebug("Invalid group context.")
return nil
}
guard let groupThread = TSGroupThread.fetch(groupId: groupContextInfo.groupId, transaction: tx) else {
owsFailDebug("Unknown v2 group.")
return nil
}
guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else {
owsFailDebug("invalid group model.")
return nil
}
guard groupContext.hasRevision, groupModel.revision >= groupContext.revision else {
owsFailDebug("Group v2 revision larger than \(groupModel.revision) in \(groupContextInfo.groupId)")
return nil
}
guard groupThread.isLocalUserFullMember else {
// We don't want to process user-visible messages for groups in which we
// are a pending member.
Logger.info("Ignoring messages for invited group or left group.")
return nil
}
guard groupModel.groupMembership.isFullMember(envelope.sourceAci) else {
// We don't want to process group messages for non-members.
Logger.info("Ignoring message from not in group user \(envelope.sourceAci)")
return nil
}
return groupThread
}
private func processFlaglessDataMessage(
_ dataMessage: SSKProtoDataMessage,
request: MessageReceiverRequest,
thread: TSThread,
tx: SDSAnyWriteTransaction
) -> TSIncomingMessage? {
let envelope = request.decryptedEnvelope
guard !dataMessage.hasRequiredProtocolVersion || dataMessage.requiredProtocolVersion <= SSKProtos.currentProtocolVersion else {
owsFailDebug("Unknown protocol version: \(dataMessage.requiredProtocolVersion)")
OWSUnknownProtocolVersionMessage(
thread: thread,
sender: SignalServiceAddress(envelope.sourceAci),
protocolVersion: UInt(dataMessage.requiredProtocolVersion)
).anyInsert(transaction: tx)
return nil
}
let deviceAddress = "\(envelope.sourceAci).\(envelope.sourceDeviceId)"
let messageDescription: String
if let groupThread = thread as? TSGroupThread {
messageDescription = "Incoming message from \(deviceAddress) in group \(groupThread.groupModel.groupId) w/ts \(envelope.timestamp), serverTimestamp: \(envelope.serverTimestamp)"
} else {
messageDescription = "Incoming message from \(deviceAddress) w/ts \(envelope.timestamp), serverTimestamp: \(envelope.serverTimestamp)"
}
if let reaction = dataMessage.reaction {
let result = ReactionManager.processIncomingReaction(
reaction,
thread: thread,
reactor: envelope.sourceAci,
timestamp: envelope.timestamp,
serverTimestamp: envelope.serverTimestamp,
expiresInSeconds: dataMessage.expireTimer,
sentTranscript: nil,
transaction: tx
)
switch result {
case .success, .invalidReaction:
break
case .associatedMessageMissing:
earlyMessageManager.recordEarlyEnvelope(
envelope.envelope,
plainTextData: request.plaintextData,
wasReceivedByUD: request.wasReceivedByUD,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
associatedMessageTimestamp: reaction.timestamp,
associatedMessageAuthor: AciObjC(aciString: reaction.targetAuthorAci),
transaction: tx
)
}
return nil
}
if let delete = dataMessage.delete {
let result = TSMessage.tryToRemotelyDeleteMessage(
fromAuthor: envelope.sourceAci,
sentAtTimestamp: delete.targetSentTimestamp,
threadUniqueId: thread.uniqueId,
serverTimestamp: envelope.serverTimestamp,
transaction: tx
)
switch result {
case .success:
break
case .invalidDelete:
Logger.warn("Couldn't process invalid remote delete w/ts \(delete.targetSentTimestamp)")
case .deletedMessageMissing:
earlyMessageManager.recordEarlyEnvelope(
envelope.envelope,
plainTextData: request.plaintextData,
wasReceivedByUD: request.wasReceivedByUD,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
associatedMessageTimestamp: delete.targetSentTimestamp,
associatedMessageAuthor: envelope.sourceAciObjC,
transaction: tx
)
}
return nil
}
if request.shouldDiscardVisibleMessages {
// Now that "reactions" and "delete for everyone" have been processed, the
// only possible outcome of further processing is a visible message or
// group call update, both of which should be discarded.
Logger.info("Discarding message w/ts \(envelope.timestamp)")
return nil
}
if let groupCallUpdate = dataMessage.groupCallUpdate {
guard let groupThread = thread as? TSGroupThread else {
Logger.warn("Ignoring group call update for non-group thread")
return nil
}
let pendingTask = Self.buildPendingTask(label: "GroupCallUpdate")
Task { [callMessageHandler] in
defer { pendingTask.complete() }
await callMessageHandler.receivedGroupCallUpdateMessage(
groupCallUpdate,
for: groupThread,
serverReceivedTimestamp: envelope.timestamp
)
}
return nil
}
let body = dataMessage.body
let bodyRanges = dataMessage.bodyRanges.isEmpty ? nil : MessageBodyRanges(protos: dataMessage.bodyRanges)
let serverGuid = envelope.envelope.serverGuid.flatMap { UUID(uuidString: $0) }
let quotedMessageBuilder = DependenciesBridge.shared.quotedReplyManager.quotedMessage(
for: dataMessage,
thread: thread,
tx: tx.asV2Write
)
let contactBuilder: OwnedAttachmentBuilder<OWSContact>?
if let contactProto = dataMessage.contact.first {
do {
contactBuilder = try DependenciesBridge.shared.contactShareManager.validateAndBuild(
for: contactProto,
tx: tx.asV2Write
)
} catch {
Logger.error("contact share error: \(error)")
return nil
}
} else {
contactBuilder = nil
}
let linkPreviewBuilder: OwnedAttachmentBuilder<OWSLinkPreview>?
if let linkPreview = dataMessage.preview.first {
do {
linkPreviewBuilder = try DependenciesBridge.shared.linkPreviewManager.validateAndBuildLinkPreview(
from: linkPreview,
dataMessage: dataMessage,
tx: tx.asV2Write
)
} catch {
Logger.error("linkPreviewError: \(error)")
return nil
}
} else {
linkPreviewBuilder = nil
}
var messageStickerBuilder: OwnedAttachmentBuilder<MessageSticker>?
if let stickerProto = dataMessage.sticker {
do {
messageStickerBuilder = try DependenciesBridge.shared.messageStickerManager.buildValidatedMessageSticker(
from: stickerProto,
tx: tx.asV2Write
)
} catch {
Logger.error("stickerError: \(error)")
return nil
}
} else {
messageStickerBuilder = nil
}
let giftBadge = OWSGiftBadge.maybeBuild(from: dataMessage)
let isViewOnceMessage = dataMessage.hasIsViewOnce && dataMessage.isViewOnce
let paymentModels = TSPaymentModels.parsePaymentProtos(dataMessage: dataMessage, thread: thread)
if let paymentModels {
paymentsHelper.processIncomingPaymentNotification(
thread: thread,
paymentNotification: paymentModels.notification,
senderAci: envelope.sourceAciObjC,
transaction: tx
)
} else if let payment = dataMessage.payment, let activation = payment.activation {
switch activation.type {
case .none, .request:
paymentsHelper.processIncomingPaymentsActivationRequest(thread: thread, senderAci: envelope.sourceAciObjC, transaction: tx)
case .activated:
paymentsHelper.processIncomingPaymentsActivatedMessage(thread: thread, senderAci: envelope.sourceAciObjC, transaction: tx)
}
return nil
}
updateDisappearingMessageConfiguration(envelope: envelope, dataMessage: dataMessage, thread: thread, tx: tx)
var storyTimestamp: UInt64?
var storyAuthorAci: Aci?
if let storyContext = dataMessage.storyContext, storyContext.hasSentTimestamp, storyContext.hasAuthorAci {
storyTimestamp = storyContext.sentTimestamp
storyAuthorAci = Aci.parseFrom(aciString: storyContext.authorAci)
Logger.info("Processing storyContext for message w/ts \(envelope.timestamp), storyTimestamp: \(String(describing: storyTimestamp)), authorAci: \(String(describing: storyAuthorAci))")
guard storyAuthorAci != nil else {
owsFailDebug("Discarding story reply with invalid ACI")
return nil
}
}
// Legit usage of senderTimestamp when creating an incoming group message
// record.
let messageBuilder = TSIncomingMessageBuilder(
thread: thread,
timestamp: envelope.timestamp,
authorAci: envelope.sourceAci,
authorE164: nil,
messageBody: body,
bodyRanges: bodyRanges,
editState: .none,
expiresInSeconds: dataMessage.expireTimer,
expireStartedAt: 0,
read: false,
serverTimestamp: NSNumber(value: envelope.serverTimestamp),
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
serverGuid: serverGuid?.uuidString.lowercased(),
wasReceivedByUD: request.wasReceivedByUD,
isViewOnceMessage: isViewOnceMessage,
storyAuthorAci: storyAuthorAci,
storyTimestamp: storyTimestamp,
storyReactionEmoji: nil,
giftBadge: giftBadge,
paymentNotification: paymentModels?.notification
)
let message = messageBuilder.build()
guard message.shouldBeSaved else {
owsFailDebug("We should be able to save all incoming messages.")
return nil
}
let hasRenderableContent = messageBuilder.hasRenderableContent(
hasBodyAttachments: !dataMessage.attachments.isEmpty,
hasLinkPreview: linkPreviewBuilder != nil,
hasQuotedReply: quotedMessageBuilder != nil,
hasContactShare: contactBuilder != nil,
hasSticker: messageStickerBuilder != nil,
hasPayment: paymentModels != nil
)
guard hasRenderableContent else {
Logger.warn("Ignoring empty: \(messageDescription)")
return nil
}
if message.giftBadge != nil, thread.isGroupThread {
owsFailDebug("Ignoring gift sent to group.")
return nil
}
// Update attachment fields before inserting.
quotedMessageBuilder.map { message.update(with: $0.info, transaction: tx) }
contactBuilder.map { message.update(withContactShare: $0.info, transaction: tx) }
linkPreviewBuilder.map { message.update(with: $0.info, transaction: tx) }
messageStickerBuilder.map { message.update(with: $0.info, transaction: tx) }
// Check for any placeholders inserted because of a previously
// undecryptable message. The sender may have resent the message. If so, we
// should swap it in place of the placeholder.
message.insertOrReplacePlaceholder(from: SignalServiceAddress(envelope.sourceAci), transaction: tx)
// Inserting the message may have modified the thread on disk, so reload
// it. For example, we may have marked the thread as visible.
thread.anyReload(transaction: tx)
do {
try DependenciesBridge.shared.tsResourceManager.createBodyAttachmentPointers(
from: dataMessage.attachments,
message: message,
tx: tx.asV2Write
)
try quotedMessageBuilder?.finalize(
owner: .quotedReplyAttachment(.init(
messageRowId: message.sqliteRowId!,
receivedAtTimestamp: message.receivedAtTimestamp,
threadRowId: thread.sqliteRowId!
)),
tx: tx.asV2Write
)
try linkPreviewBuilder?.finalize(
owner: .messageLinkPreview(.init(
messageRowId: message.sqliteRowId!,
receivedAtTimestamp: message.receivedAtTimestamp,
threadRowId: thread.sqliteRowId!
)),
tx: tx.asV2Write
)
try messageStickerBuilder.map {
try $0.finalize(
owner: .messageSticker(.init(
messageRowId: message.sqliteRowId!,
receivedAtTimestamp: message.receivedAtTimestamp,
threadRowId: thread.sqliteRowId!,
stickerPackId: $0.info.packId,
stickerId: $0.info.stickerId
)),
tx: tx.asV2Write
)
}
try contactBuilder?.finalize(
owner: .messageContactAvatar(.init(
messageRowId: message.sqliteRowId!,
receivedAtTimestamp: message.receivedAtTimestamp,
threadRowId: thread.sqliteRowId!
)),
tx: tx.asV2Write
)
} catch {
owsFailDebug("Could not build attachments!")
DependenciesBridge.shared.interactionDeleteManager
.delete(message, sideEffects: .default(), tx: tx.asV2Write)
return nil
}
owsAssertDebug(message.insertedMessageHasRenderableContent(rowId: message.sqliteRowId!, tx: tx))
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read) else {
owsFailDebug("Can't process messages when not registered.")
return nil
}
earlyMessageManager.applyPendingMessages(for: message, localIdentifiers: localIdentifiers, transaction: tx)
// Any messages sent from the current user - from this device or another -
// should be automatically marked as read.
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
if envelope.sourceAci == tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aci {
let hasPendingMessageRequest = thread.hasPendingMessageRequest(transaction: tx)
owsFailDebug("Incoming messages from yourself aren't supported.")
// Don't send a read receipt for messages sent by ourselves.
message.markAsRead(
atTimestamp: envelope.timestamp,
thread: thread,
circumstance: hasPendingMessageRequest ? .onLinkedDeviceWhilePendingMessageRequest : .onLinkedDevice,
shouldClearNotifications: false, // not required, since no notifications if sent by local
transaction: tx
)
}
DependenciesBridge.shared.tsResourceDownloadManager.enqueueDownloadOfAttachmentsForMessage(message, tx: tx.asV2Write)
notificationPresenter.notifyUser(forIncomingMessage: message, thread: thread, transaction: tx)
if CurrentAppContext().isMainApp {
DispatchQueue.main.async {
Self.typingIndicatorsImpl.didReceiveIncomingMessage(
inThread: thread,
senderAci: envelope.sourceAciObjC,
deviceId: envelope.sourceDeviceId
)
}
}
return nil
}
private func updateDisappearingMessageConfiguration(
envelope: DecryptedIncomingEnvelope,
dataMessage: SSKProtoDataMessage,
thread: TSThread,
tx: SDSAnyWriteTransaction
) {
guard let contactThread = thread as? TSContactThread else {
return
}
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read) else {
owsFailDebug("Not registered.")
return
}
GroupManager.remoteUpdateDisappearingMessages(
withContactThread: contactThread,
disappearingMessageToken: .token(forProtoExpireTimerSeconds: dataMessage.expireTimer),
changeAuthor: envelope.sourceAciObjC,
localIdentifiers: LocalIdentifiersObjC(localIdentifiers),
transaction: tx
)
}
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
callMessage: SSKProtoCallMessage,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
let envelope = request.decryptedEnvelope
// If destinationDevice is defined, ignore messages not addressed to this device.
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
let localDeviceId = tsAccountManager.storedDeviceId(tx: tx.asV2Read)
if callMessage.hasDestinationDeviceID, callMessage.destinationDeviceID != localDeviceId {
Logger.info("Ignoring call message for other device #\(callMessage.destinationDeviceID)")
return
}
if let profileKey = callMessage.profileKey {
setProfileKeyIfValid(profileKey, for: envelope.sourceAci, localIdentifiers: localIdentifiers, tx: tx)
}
let callEnvelope: CallEnvelopeType
if let offer = callMessage.offer {
callEnvelope = .offer(offer)
} else if let answer = callMessage.answer {
callEnvelope = .answer(answer)
} else if !callMessage.iceUpdate.isEmpty {
callEnvelope = .iceUpdate(callMessage.iceUpdate)
} else if let hangup = callMessage.hangup {
callEnvelope = .hangup(hangup)
} else if let busy = callMessage.busy {
callEnvelope = .busy(busy)
} else if let opaque = callMessage.opaque {
callEnvelope = .opaque(opaque)
} else {
Logger.warn("Dropping call message with no actionable payload.")
return
}
callMessageHandler.receivedEnvelope(
envelope.envelope,
callEnvelope: callEnvelope,
from: (envelope.sourceAci, envelope.sourceDeviceId),
plaintextData: envelope.plaintextData,
wasReceivedByUD: envelope.wasReceivedByUD,
sentAtTimestamp: envelope.timestamp,
serverReceivedTimestamp: envelope.serverTimestamp,
serverDeliveryTimestamp: request.serverDeliveryTimestamp,
tx: tx
)
}
private func handleIncomingEnvelope(
_ decryptedEnvelope: DecryptedIncomingEnvelope,
withSenderKeyDistributionMessage skdmData: Data,
transaction tx: SDSAnyWriteTransaction
) {
do {
let skdm = try SenderKeyDistributionMessage(bytes: skdmData.map { $0 })
let sourceAci = decryptedEnvelope.sourceAci
let sourceDeviceId = decryptedEnvelope.sourceDeviceId
let protocolAddress = ProtocolAddress(sourceAci, deviceId: sourceDeviceId)
try processSenderKeyDistributionMessage(skdm, from: protocolAddress, store: senderKeyStore, context: tx)
Logger.info("Processed incoming sender key distribution message from \(sourceAci).\(sourceDeviceId)")
} catch {
owsFailDebug("Failed to process incoming sender key \(error)")
}
}
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
typingMessage: SSKProtoTypingMessage,
tx: SDSAnyWriteTransaction
) {
let envelope = request.decryptedEnvelope
guard typingMessage.timestamp == envelope.timestamp else {
owsFailDebug("typingMessage has invalid timestamp")
return
}
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
if envelope.sourceAci == tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aci {
return
}
let thread: TSThread
if let groupId = typingMessage.groupID {
if blockingManager.isGroupIdBlocked(groupId, transaction: tx) {
Logger.warn("Ignoring blocked message from \(envelope.sourceAci) in \(groupId)")
return
}
guard let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: tx) else {
// This isn't necessarily an error. We might not yet know about the thread,
// in which case we don't need to display the typing indicators.
Logger.warn("Ignoring typingMessage for non-existent thread")
return
}
guard groupThread.isLocalUserFullOrInvitedMember else {
Logger.info("Ignoring message for left group")
return
}
if let groupModel = groupThread.groupModel as? TSGroupModelV2, groupModel.isAnnouncementsOnly {
guard groupModel.groupMembership.isFullMemberAndAdministrator(envelope.sourceAci) else {
return
}
}
thread = groupThread
} else {
let sourceAddress = SignalServiceAddress(envelope.sourceAci)
guard let contactThread = TSContactThread.getWithContactAddress(sourceAddress, transaction: tx) else {
// This isn't necessarily an error. We might not yet know about the thread,
// in which case we don't need to display the typing indicators.
Logger.warn("Ignoring typingMessage for non-existent thread")
return
}
thread = contactThread
}
DispatchQueue.main.async {
switch typingMessage.action {
case .started:
Self.typingIndicatorsImpl.didReceiveTypingStartedMessage(
inThread: thread,
senderAci: envelope.sourceAciObjC,
deviceId: envelope.sourceDeviceId
)
case .stopped:
Self.typingIndicatorsImpl.didReceiveTypingStoppedMessage(
inThread: thread,
senderAci: envelope.sourceAciObjC,
deviceId: envelope.sourceDeviceId
)
case .none:
owsFailDebug("typingMessage has unexpected action")
}
}
}
/// This code path is for UD receipts.
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
receiptMessage: SSKProtoReceiptMessage,
context: DeliveryReceiptContext,
tx: SDSAnyWriteTransaction
) {
let envelope = request.decryptedEnvelope
guard let receiptType = receiptMessage.type else {
owsFailDebug("Missing type for receipt message.")
return
}
let sentTimestamps = receiptMessage.timestamp
for sentTimestamp in sentTimestamps {
guard SDS.fitsInInt64(sentTimestamp) else {
owsFailDebug("Invalid timestamp.")
return
}
}
let earlyTimestamps: [UInt64]
switch receiptType {
case .delivery:
earlyTimestamps = receiptManager.processDeliveryReceipts(
from: envelope.sourceAci,
recipientDeviceId: envelope.sourceDeviceId,
sentTimestamps: sentTimestamps,
deliveryTimestamp: envelope.timestamp,
context: context,
tx: tx
)
case .read:
earlyTimestamps = receiptManager.processReadReceipts(
from: envelope.sourceAci,
recipientDeviceId: envelope.sourceDeviceId,
sentTimestamps: sentTimestamps,
readTimestamp: envelope.timestamp,
tx: tx
)
case .viewed:
earlyTimestamps = receiptManager.processViewedReceipts(
from: envelope.sourceAci,
recipientDeviceId: envelope.sourceDeviceId,
sentTimestamps: sentTimestamps,
viewedTimestamp: envelope.timestamp,
tx: tx
)
}
recordEarlyReceipts(
receiptType: receiptType,
senderServiceId: envelope.sourceAci,
senderDeviceId: envelope.sourceDeviceId,
associatedMessageTimestamps: earlyTimestamps,
actionTimestamp: envelope.timestamp,
tx: tx
)
}
/// Records early receipts for a set of message timestamps.
///
/// - Parameter associatedMessageTimestamps: A list of message timestamps
/// that may need to be marked viewed/read/delivered after they are
/// received.
///
/// - Parameter actionTimestamp: The timestamp when the other user
/// viewed/read/received the message.
private func recordEarlyReceipts(
receiptType: SSKProtoReceiptMessageType,
senderServiceId: ServiceId,
senderDeviceId: UInt32,
associatedMessageTimestamps: [UInt64],
actionTimestamp: UInt64,
tx: SDSAnyWriteTransaction
) {
for associatedMessageTimestamp in associatedMessageTimestamps {
earlyMessageManager.recordEarlyReceiptForOutgoingMessage(
type: receiptType,
senderServiceId: senderServiceId,
senderDeviceId: senderDeviceId,
timestamp: actionTimestamp,
associatedMessageTimestamp: associatedMessageTimestamp,
tx: tx
)
}
}
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
decryptionErrorMessage: Data,
tx: SDSAnyWriteTransaction
) {
let envelope = request.decryptedEnvelope
let sourceAci = envelope.sourceAci
let sourceDeviceId = envelope.sourceDeviceId
do {
guard envelope.localIdentity == .aci else {
throw OWSGenericError("Can't receive DEMs at our PNI.")
}
let errorMessage = try DecryptionErrorMessage(bytes: decryptionErrorMessage)
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
guard errorMessage.deviceId == tsAccountManager.storedDeviceId(tx: tx.asV2Read) else {
Logger.info("Received a DecryptionError message targeting a linked device. Ignoring.")
return
}
let protocolAddress = ProtocolAddress(sourceAci, deviceId: sourceDeviceId)
let didPerformSessionReset: Bool
if let ratchetKey = errorMessage.ratchetKey {
// If a ratchet key is included, this was a 1:1 session message
// Archive the session if the current key matches.
let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
let sessionRecord = try sessionStore.loadSession(for: protocolAddress, context: tx)
if try sessionRecord?.currentRatchetKeyMatches(ratchetKey) == true {
Logger.info("Decryption error included ratchet key. Archiving...")
sessionStore.archiveSession(for: sourceAci, deviceId: sourceDeviceId, tx: tx.asV2Write)
didPerformSessionReset = true
} else {
didPerformSessionReset = false
}
} else {
// If we don't have a ratchet key, this was a sender key session message.
// Let's log any info about SKDMs that we had sent to the address requesting resend
senderKeyStore.logSKDMInfo(for: SignalServiceAddress(sourceAci), transaction: tx)
didPerformSessionReset = false
}
Logger.warn("Performing message resend of timestamp \(errorMessage.timestamp)")
let resendResponse = OWSOutgoingResendResponse(
aci: sourceAci,
deviceId: sourceDeviceId,
failedTimestamp: errorMessage.timestamp,
didResetSession: didPerformSessionReset,
tx: tx
)
let sendBlock = { (transaction: SDSAnyWriteTransaction) in
if let resendResponse = resendResponse {
let preparedMessage = PreparedOutgoingMessage.preprepared(
transientMessageWithoutAttachments: resendResponse
)
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
}
}
if DebugFlags.delayedMessageResend.get() {
DispatchQueue.sharedUtility.asyncAfter(deadline: .now() + 10) {
Self.databaseStorage.asyncWrite { tx in
sendBlock(tx)
}
}
} else {
sendBlock(tx)
}
} catch {
owsFailDebug("Failed to process decryption error message \(error)")
}
}
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
storyMessage: SSKProtoStoryMessage,
localIdentifiers: LocalIdentifiers,
tx: SDSAnyWriteTransaction
) {
do {
try StoryManager.processIncomingStoryMessage(
storyMessage,
timestamp: request.decryptedEnvelope.timestamp,
author: request.decryptedEnvelope.sourceAci,
localIdentifiers: localIdentifiers,
transaction: tx
)
} catch {
Logger.warn("Failed to insert story message with error \(error.localizedDescription)")
}
}
private enum EditProcessingResult: Int, Error {
case editedMessageMissing
case invalidEdit
case success
}
private func handleIncomingEnvelope(
_ decryptedEnvelope: DecryptedIncomingEnvelope,
sentMessage: SSKProtoSyncMessageSent,
editMessage: SSKProtoEditMessage,
transaction tx: SDSAnyWriteTransaction
) -> EditProcessingResult {
guard SDS.fitsInInt64(editMessage.targetSentTimestamp) else {
Logger.error("Edit message target was invalid timestamp!")
return .invalidEdit
}
guard let transcript = OWSIncomingSentMessageTranscript.from(
sentProto: sentMessage,
serverTimestamp: decryptedEnvelope.serverTimestamp,
tx: tx.asV2Write
) else {
Logger.warn("Missing edit transcript.")
return .invalidEdit
}
guard let thread = transcript.threadForDataMessage else {
Logger.warn("Missing edit message thread.")
return .invalidEdit
}
// Find the target message to edit. If missing,
// return and enqueue the message to be handled as early delivery
guard
let targetMessage = DependenciesBridge.shared.editMessageStore.editTarget(
timestamp: editMessage.targetSentTimestamp,
authorAci: nil,
tx: tx.asV2Read
)
else {
Logger.warn("Edit cannot find the target message")
return .editedMessageMissing
}
guard let message = try? handleMessageEdit(
envelope: decryptedEnvelope,
thread: thread,
editTarget: targetMessage,
editMessage: editMessage,
transaction: tx
) else {
Logger.info("Failed to insert edit sync message")
return .invalidEdit
}
if let msg = message as? TSOutgoingMessage {
msg.updateRecipientsFromNonLocalDevice(
transcript.recipientStates,
isSentUpdate: false,
transaction: tx
)
}
return .success
}
private func handleIncomingEnvelope(
request: MessageReceiverRequest,
editMessage: SSKProtoEditMessage,
tx: SDSAnyWriteTransaction
) -> EditProcessingResult {
guard SDS.fitsInInt64(editMessage.targetSentTimestamp) else {
Logger.error("Edit message target was invalid timestamp!")
return .invalidEdit
}
guard let dataMessage = editMessage.dataMessage else {
Logger.warn("Missing edit message data.")
return .invalidEdit
}
let decryptedEnvelope = request.decryptedEnvelope
guard let thread = preprocessDataMessage(dataMessage, envelope: decryptedEnvelope, tx: tx) else {
Logger.warn("Missing edit message thread.")
return .invalidEdit
}
// Find the target message to edit. If missing,
// return and enqueue the message to be handled as early delivery
guard let targetMessage = DependenciesBridge.shared.editMessageStore.editTarget(
timestamp: editMessage.targetSentTimestamp,
authorAci: decryptedEnvelope.sourceAci,
tx: tx.asV2Read
) else {
Logger.warn("Edit cannot find the target message")
return .editedMessageMissing
}
guard let message = try? handleMessageEdit(
envelope: decryptedEnvelope,
thread: thread,
editTarget: targetMessage,
editMessage: editMessage,
transaction: tx
) else {
Logger.info("Failed to insert edit message")
return .invalidEdit
}
if request.wasReceivedByUD {
let receiptSender = SSKEnvironment.shared.receiptSenderRef
receiptSender.enqueueDeliveryReceipt(for: decryptedEnvelope, messageUniqueId: message.uniqueId, tx: tx)
}
if
case let .incomingMessage(incoming) = targetMessage,
let message = message as? TSIncomingMessage
{
// Only update notifications for unread/unedited message targets
let targetEditState = incoming.message.editState
if targetEditState == .latestRevisionUnread || targetEditState == .none {
self.notificationPresenter.notifyUser(
forIncomingMessage: message,
editTarget: incoming.message,
thread: thread,
transaction: tx
)
}
}
return .success
}
private func handleMessageEdit(
envelope: DecryptedIncomingEnvelope,
thread: TSThread,
editTarget: EditMessageTarget,
editMessage: SSKProtoEditMessage,
transaction tx: SDSAnyWriteTransaction
) throws -> TSMessage {
guard let dataMessage = editMessage.dataMessage else {
throw OWSAssertionError("Missing dataMessage in edit")
}
let message = try DependenciesBridge.shared.editManager.processIncomingEditMessage(
dataMessage,
thread: thread,
editTarget: editTarget,
serverTimestamp: envelope.serverTimestamp,
tx: tx.asV2Write
)
// Start downloading any new attachments
DependenciesBridge.shared.tsResourceDownloadManager.enqueueDownloadOfAttachmentsForMessage(
message,
tx: tx.asV2Write
)
DispatchQueue.main.async {
self.typingIndicatorsImpl.didReceiveIncomingMessage(
inThread: thread,
senderAci: AciObjC(envelope.sourceAci),
deviceId: envelope.sourceDeviceId
)
}
return message
}
func checkForUnknownLinkedDevice(in envelope: DecryptedIncomingEnvelope, tx: SDSAnyWriteTransaction) {
let aci = envelope.sourceAci
let deviceId = envelope.sourceDeviceId
guard aci == DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aci else {
return
}
// Check if the SignalRecipient (used for sending messages) knows about
// this device.
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let recipient = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx.asV2Write)
if !recipient.deviceIds.contains(deviceId) {
let recipientManager = DependenciesBridge.shared.recipientManager
Logger.info("Message received from unknown linked device; adding to local SignalRecipient: \(deviceId).")
recipientManager.markAsRegisteredAndSave(recipient, deviceId: deviceId, shouldUpdateStorageService: true, tx: tx.asV2Write)
}
// Check if OWSDevice (ie the "Linked Devices" UI) knows about this device.
let hasDevice = OWSDevice.anyFetchAll(transaction: tx).contains(where: { $0.deviceId == deviceId })
if !hasDevice {
Logger.info("Message received from unknown linked device; refreshing device list: \(deviceId).")
OWSDevicesService.refreshDevices()
}
}
private func handleIncomingEndSessionEnvelope(
_ decryptedEnvelope: DecryptedIncomingEnvelope,
withDataMessage dataMessage: SSKProtoDataMessage,
tx: SDSAnyWriteTransaction
) {
guard decryptedEnvelope.localIdentity == .aci else {
owsFailDebug("Can't receive end session messages to our PNI.")
return
}
let thread = TSContactThread.getOrCreateThread(
withContactAddress: SignalServiceAddress(decryptedEnvelope.sourceAci),
transaction: tx
)
TSInfoMessage(thread: thread, messageType: .typeSessionDidEnd).anyInsert(transaction: tx)
let sessionStore = DependenciesBridge.shared.signalProtocolStoreManager.signalProtocolStore(for: .aci).sessionStore
sessionStore.archiveAllSessions(for: decryptedEnvelope.sourceAci, tx: tx.asV2Write)
}
}
// MARK: -
extension SSKProtoEnvelope {
@objc
var formattedAddress: String {
return "\(String(describing: sourceServiceID)).\(sourceDevice)"
}
}
extension SSKProtoContent {
@objc
var contentDescription: String {
var parts = [String]()
if let dataMessage = self.dataMessage {
parts.append("data \(dataMessage.contentDescription)")
}
if let syncMessage = self.syncMessage {
parts.append("sync \(syncMessage.contentDescription)")
}
if let callMessage = self.callMessage {
parts.append("call \(callMessage.contentDescription)")
}
if nullMessage != nil {
parts.append("null")
}
if receiptMessage != nil {
parts.append("receipt")
}
if typingMessage != nil {
parts.append("typing")
}
if storyMessage != nil {
parts.append("story")
}
if pniSignatureMessage != nil {
parts.append("pniSignature")
}
if editMessage != nil {
parts.append("edit")
}
if senderKeyDistributionMessage != nil {
parts.append("senderKeyDistribution")
}
if decryptionErrorMessage != nil {
parts.append("decryptionError")
}
if hasUnknownFields {
parts.append("unknown fields")
}
return "[\(parts.joined(separator: ", "))]"
}
}
extension SSKProtoCallMessage {
@objc
var contentDescription: String {
let messageType: String
let callId: UInt64
if let offer = self.offer {
messageType = "offer"
callId = offer.id
} else if let busy = self.busy {
messageType = "busy"
callId = busy.id
} else if let answer = self.answer {
messageType = "answer"
callId = answer.id
} else if let hangup = self.hangup {
messageType = "hangup"
callId = hangup.id
} else if let firstICEUpdate = iceUpdate.first {
messageType = "ice updates \(iceUpdate.count)"
callId = firstICEUpdate.id
} else if let opaque = self.opaque {
if opaque.hasUrgency {
messageType = "opaque \(opaque.unwrappedUrgency.contentDescription)"
} else {
messageType = "opaque"
}
callId = 0
} else {
owsFailDebug("failure: unexpected call message type: \(self)")
messageType = "unknown"
callId = 0
}
return "[type: \(messageType), id: \(callId)]"
}
}
extension SSKProtoCallMessageOpaqueUrgency {
var contentDescription: String {
switch self {
case .droppable:
return "droppable"
case .handleImmediately:
return "handleImmediately"
default:
return "unknown"
}
}
}
extension SSKProtoDataMessage {
@objc
var contentDescription: String {
var parts = [String]()
if !attachments.isEmpty {
parts.append("attachments")
}
if groupV2 != nil {
parts.append("group")
}
if quote != nil {
parts.append("quote")
}
if !contact.isEmpty {
parts.append("contacts")
}
if !preview.isEmpty {
parts.append("linkPreviews")
}
if sticker != nil {
parts.append("stickers")
}
if reaction != nil {
parts.append("reaction")
}
if delete != nil {
parts.append("delete")
}
if !bodyRanges.isEmpty {
parts.append("bodyRanges")
}
if groupCallUpdate != nil {
parts.append("groupCallUpdate")
}
if payment != nil {
parts.append("payment")
}
if giftBadge != nil {
parts.append("giftBadge")
}
if body != nil {
parts.append("body")
}
if expireTimer > 0 {
parts.append("expireTimer")
}
if profileKey != nil {
parts.append("profileKey")
}
if isViewOnce {
parts.append("viewOnce")
}
if flags > 0 {
parts.append("flags \(flags)")
}
if hasUnknownFields {
parts.append("unknown fields")
}
return "[\(parts.joined(separator: ", "))]"
}
}
extension SSKProtoSyncMessage {
@objc
var contentDescription: String {
var parts = [String]()
if sent != nil {
parts.append("sentTranscript")
}
if contacts != nil {
parts.append("contacts")
}
if let request {
switch request.type {
case .contacts:
parts.append("request: contacts")
case .blocked:
parts.append("request: blocked")
case .configuration:
parts.append("request: configuration")
case .keys:
parts.append("request: keys")
case .unknown, .none: fallthrough
@unknown default:
parts.append("request: unknown")
}
}
if !read.isEmpty {
parts.append("readReceipts")
}
if blocked != nil {
parts.append("blocked")
}
if verified != nil {
parts.append("verified")
}
if configuration != nil {
parts.append("configuration")
}
if !stickerPackOperation.isEmpty {
for packOperationProto in stickerPackOperation {
switch packOperationProto.type {
case .install:
parts.append("stickerPack: install")
case .remove:
parts.append("stickerPack: remove")
case .none: fallthrough
@unknown default:
parts.append("stickerPack: unknown")
}
}
}
if viewOnceOpen != nil {
parts.append("viewOnceOpen")
}
if let fetchLatest {
switch fetchLatest.type {
case .localProfile:
parts.append("fetchLatest: profile")
case .storageManifest:
parts.append("fetchLatest: storageService")
case .subscriptionStatus:
parts.append("fetchLatest: subscription")
case .unknown, .none: fallthrough
@unknown default:
parts.append("fetchLatest: unknown")
}
}
if keys != nil {
parts.append("keys")
}
if messageRequestResponse != nil {
parts.append("messageRequestResponse")
}
if outgoingPayment != nil {
parts.append("outgoingPayment")
}
if !viewed.isEmpty {
parts.append("viewedReceipt")
}
if pniChangeNumber != nil {
parts.append("pniChangeNumber")
}
if callEvent != nil {
parts.append("callEvent")
}
if callLogEvent != nil {
parts.append("callLogEvent")
}
if deleteForMe != nil {
parts.append("deleteForMe")
}
if hasUnknownFields {
parts.append("unknown fields")
}
owsAssertDebug(!parts.isEmpty, "unknown sync message type")
return "[\(parts.joined(separator: ", "))]"
}
}
// MARK: -
enum MessageReceiverMessageType {
case syncMessage(SSKProtoSyncMessage)
case dataMessage(SSKProtoDataMessage)
case callMessage(SSKProtoCallMessage)
case typingMessage(SSKProtoTypingMessage)
case nullMessage
case receiptMessage(SSKProtoReceiptMessage)
case decryptionErrorMessage(Data)
case storyMessage(SSKProtoStoryMessage)
case editMessage(SSKProtoEditMessage)
case handledElsewhere
}
class MessageReceiverRequest {
let decryptedEnvelope: DecryptedIncomingEnvelope
let envelope: SSKProtoEnvelope
let plaintextData: Data
let wasReceivedByUD: Bool
let serverDeliveryTimestamp: UInt64
let shouldDiscardVisibleMessages: Bool
let protoContent: SSKProtoContent
var messageType: MessageReceiverMessageType? {
if let syncMessage = protoContent.syncMessage {
return .syncMessage(syncMessage)
}
if let dataMessage = protoContent.dataMessage {
return .dataMessage(dataMessage)
}
if let callMessage = protoContent.callMessage {
return .callMessage(callMessage)
}
if let typingMessage = protoContent.typingMessage {
return .typingMessage(typingMessage)
}
if protoContent.nullMessage != nil {
return .nullMessage
}
if let receiptMessage = protoContent.receiptMessage {
return .receiptMessage(receiptMessage)
}
if let decryptionErrorMessage = protoContent.decryptionErrorMessage {
return .decryptionErrorMessage(decryptionErrorMessage)
}
if let storyMessage = protoContent.storyMessage {
return .storyMessage(storyMessage)
}
if let editMessage = protoContent.editMessage {
return .editMessage(editMessage)
}
// All mutually-exclusive top-level proto content types should be placed
// above this comment. Below this comment, we return `.handledElsewhere`
// for cases that *might* be combined with another type or sent alone.
if protoContent.hasSenderKeyDistributionMessage {
// Sender key distribution messages are not mutually exclusive. They can be
// included with any message type. However, they're not processed here.
// They're processed in the -preprocess phase that occurs post-decryption.
return .handledElsewhere
}
return nil
}
enum BuildResult {
case discard
case noContent
case request(MessageReceiverRequest)
}
static func buildRequest(
for decryptedEnvelope: DecryptedIncomingEnvelope,
serverDeliveryTimestamp: UInt64,
shouldDiscardVisibleMessages: Bool,
tx: SDSAnyWriteTransaction
) -> BuildResult {
if Self.isDuplicate(decryptedEnvelope, tx: tx) {
Logger.info("Ignoring previously received envelope from \(decryptedEnvelope.sourceAci) with timestamp: \(decryptedEnvelope.timestamp)")
return .discard
}
guard let contentProto = decryptedEnvelope.content else {
return .noContent
}
if contentProto.callMessage != nil && shouldDiscardVisibleMessages {
Logger.info("Discarding message with timestamp \(decryptedEnvelope.timestamp)")
return .discard
}
if decryptedEnvelope.envelope.story && contentProto.dataMessage?.delete == nil {
guard StoryManager.areStoriesEnabled(transaction: tx) else {
Logger.info("Discarding story message received while stories are disabled")
return .discard
}
guard
contentProto.senderKeyDistributionMessage != nil ||
contentProto.storyMessage != nil ||
(contentProto.dataMessage?.storyContext != nil && contentProto.dataMessage?.groupV2 != nil)
else {
owsFailDebug("Discarding story message with invalid content.")
return .discard
}
}
return .request(MessageReceiverRequest(
decryptedEnvelope: decryptedEnvelope,
protoContent: contentProto,
serverDeliveryTimestamp: serverDeliveryTimestamp,
shouldDiscardVisibleMessages: shouldDiscardVisibleMessages
))
}
private static func isDuplicate(_ decryptedEnvelope: DecryptedIncomingEnvelope, tx: SDSAnyReadTransaction) -> Bool {
return InteractionFinder.existsIncomingMessage(
timestamp: decryptedEnvelope.timestamp,
sourceAci: decryptedEnvelope.sourceAci,
transaction: tx
)
}
private init(
decryptedEnvelope: DecryptedIncomingEnvelope,
protoContent: SSKProtoContent,
serverDeliveryTimestamp: UInt64,
shouldDiscardVisibleMessages: Bool
) {
self.decryptedEnvelope = decryptedEnvelope
self.envelope = decryptedEnvelope.envelope
self.plaintextData = decryptedEnvelope.plaintextData
self.wasReceivedByUD = decryptedEnvelope.wasReceivedByUD
self.protoContent = protoContent
self.serverDeliveryTimestamp = serverDeliveryTimestamp
self.shouldDiscardVisibleMessages = shouldDiscardVisibleMessages
}
}