session-ios/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift

385 lines
16 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SignalCoreKit
2020-11-09 00:58:47 +01:00
import SessionUtilitiesKit
2020-11-05 23:17:05 +01:00
public enum MessageReceiver {
private static var lastEncryptionKeyPairRequest: [String: Date] = [:]
public static func parse(
_ db: Database,
envelope: SNProtoEnvelope,
serverExpirationTimestamp: TimeInterval?,
openGroupId: String?,
openGroupMessageServerId: Int64?,
openGroupServerPublicKey: String?,
isOutgoing: Bool? = nil,
otherBlindedPublicKey: String? = nil,
dependencies: SMKDependencies = SMKDependencies()
) throws -> (Message, SNProtoContent, String) {
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let isOpenGroupMessage: Bool = (openGroupId != nil)
// Decrypt the contents
guard let ciphertext = envelope.content else { throw MessageReceiverError.noData }
var plaintext: Data
var sender: String
2020-11-18 05:53:45 +01:00
var groupPublicKey: String? = nil
2020-11-30 01:00:28 +01:00
if isOpenGroupMessage {
(plaintext, sender) = (envelope.content!, envelope.source!)
}
else {
2020-11-30 01:00:28 +01:00
switch envelope.type {
case .sessionMessage:
// Default to 'standard' as the old code didn't seem to require an `envelope.source`
switch (SessionId.Prefix(from: envelope.source) ?? .standard) {
case .standard, .unblinded:
guard let userX25519KeyPair: Box.KeyPair = Identity.fetchUserKeyPair(db) else {
throw MessageReceiverError.noUserX25519KeyPair
}
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
case .blinded:
guard let otherBlindedPublicKey: String = otherBlindedPublicKey else {
throw MessageReceiverError.noData
}
guard let openGroupServerPublicKey: String = openGroupServerPublicKey else {
throw MessageReceiverError.invalidGroupPublicKey
}
guard let userEd25519KeyPair: Box.KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw MessageReceiverError.noUserED25519KeyPair
}
(plaintext, sender) = try decryptWithSessionBlindingProtocol(
data: ciphertext,
isOutgoing: (isOutgoing == true),
otherBlindedPublicKey: otherBlindedPublicKey,
with: openGroupServerPublicKey,
userEd25519KeyPair: userEd25519KeyPair,
using: dependencies
)
}
case .closedGroupMessage:
guard
let hexEncodedGroupPublicKey = envelope.source,
let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: hexEncodedGroupPublicKey)
else {
throw MessageReceiverError.invalidGroupPublicKey
}
guard
let encryptionKeyPairs: [ClosedGroupKeyPair] = try? closedGroup.keyPairs.order(ClosedGroupKeyPair.Columns.receivedTimestamp.desc).fetchAll(db),
!encryptionKeyPairs.isEmpty
else {
throw MessageReceiverError.noGroupKeyPair
}
// Loop through all known group key pairs in reverse order (i.e. try the latest key
// pair first (which'll more than likely be the one we want) but try older ones in
// case that didn't work)
func decrypt(keyPairs: [ClosedGroupKeyPair], lastError: Error? = nil) throws -> (Data, String) {
guard let keyPair: ClosedGroupKeyPair = keyPairs.first else {
throw (lastError ?? MessageReceiverError.decryptionFailed)
}
do {
return try decryptWithSessionProtocol(
ciphertext: ciphertext,
using: Box.KeyPair(
publicKey: keyPair.publicKey.bytes,
secretKey: keyPair.secretKey.bytes
)
)
}
catch {
return try decrypt(keyPairs: Array(keyPairs.suffix(from: 1)), lastError: error)
}
}
groupPublicKey = hexEncodedGroupPublicKey
(plaintext, sender) = try decrypt(keyPairs: encryptionKeyPairs)
Merge branch 'feature/session-id-blinding-part-2' into feature/database-refactor # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Closed Groups/EditClosedGroupVC.swift # Session/Closed Groups/NewClosedGroupVC.swift # Session/Conversations/Context Menu/ContextMenuVC+Action.swift # Session/Conversations/Context Menu/ContextMenuVC.swift # Session/Conversations/ConversationMessageMapping.swift # Session/Conversations/ConversationSearch.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewItem.h # Session/Conversations/ConversationViewItem.m # Session/Conversations/ConversationViewModel.m # Session/Conversations/Input View/InputView.swift # Session/Conversations/Input View/MentionSelectionView.swift # Session/Conversations/LongTextViewController.swift # Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift # Session/Conversations/Message Cells/MessageCell.swift # Session/Conversations/Message Cells/VisibleMessageCell.swift # Session/Conversations/Settings/OWSConversationSettingsViewController.m # Session/Conversations/Views & Modals/ConversationTitleView.swift # Session/Conversations/Views & Modals/DownloadAttachmentModal.swift # Session/Conversations/Views & Modals/JoinOpenGroupModal.swift # Session/Conversations/Views & Modals/LinkPreviewModal.swift # Session/Conversations/Views & Modals/MessagesTableView.swift # Session/Conversations/Views & Modals/URLModal.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/Message Requests/MessageRequestsViewController.swift # Session/Media Viewing & Editing/MediaDetailViewController.m # Session/Media Viewing & Editing/MediaPageViewController.swift # Session/Meta/AppDelegate.m # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/Signal-Bridging-Header.h # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Notifications/AppNotifications.swift # Session/Open Groups/JoinOpenGroupVC.swift # Session/Settings/NukeDataModal.swift # Session/Settings/SeedModal.swift # Session/Settings/SettingsVC.swift # Session/Settings/ShareLogsModal.swift # Session/Shared/ConversationCell.swift # Session/Shared/UserSelectionVC.swift # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MentionUtilities.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Database/OWSPrimaryStorage.m # SessionMessagingKit/Database/SSKPreferences.swift # SessionMessagingKit/Database/Storage+Contacts.swift # SessionMessagingKit/Database/Storage+Jobs.swift # SessionMessagingKit/Database/Storage+Messaging.swift # SessionMessagingKit/Database/Storage+OpenGroups.swift # SessionMessagingKit/Database/TSDatabaseView.m # SessionMessagingKit/File Server/FileServerAPIV2.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/AttachmentUploadJob.swift # SessionMessagingKit/Jobs/JobQueue.swift # SessionMessagingKit/Jobs/MessageReceiveJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/NotifyPNServerJob.swift # SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift # SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift # SessionMessagingKit/Messages/Message+Destination.swift # SessionMessagingKit/Messages/Signal/TSIncomingMessage.h # SessionMessagingKit/Messages/Signal/TSIncomingMessage.m # SessionMessagingKit/Messages/Signal/TSInfoMessage.h # SessionMessagingKit/Messages/Signal/TSInfoMessage.m # SessionMessagingKit/Messages/Signal/TSInteraction.h # SessionMessagingKit/Messages/Signal/TSInteraction.m # SessionMessagingKit/Messages/Signal/TSMessage.h # SessionMessagingKit/Messages/Signal/TSMessage.m # SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift # SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift # SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift # SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift # SessionMessagingKit/Sending & Receiving/Mentions/MentionsManager.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Decryption.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Encryption.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h # SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Storage.swift # SessionMessagingKit/Threads/Notification+Thread.swift # SessionMessagingKit/Threads/TSContactThread.h # SessionMessagingKit/Threads/TSContactThread.m # SessionMessagingKit/Threads/TSGroupModel.h # SessionMessagingKit/Threads/TSGroupModel.m # SessionMessagingKit/Threads/TSGroupThread.m # SessionMessagingKit/Utilities/General.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionSnodeKit/OnionRequestAPI+Encryption.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionSnodeKit/SnodeAPI.swift # SessionSnodeKit/SnodeMessage.swift # SessionSnodeKit/Storage+SnodeAPI.swift # SessionSnodeKit/Storage.swift # SessionUtilitiesKit/General/Array+Utilities.swift # SessionUtilitiesKit/General/Dictionary+Utilities.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/Set+Utilities.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Utilities/Optional+Utilities.swift # SessionUtilitiesKit/Utilities/Sodium+Conversion.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Database/Migrations/OpenGroupServerIdLookupMigration.swift # SignalUtilitiesKit/Messaging/FullTextSearcher.swift # SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift # SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift # SignalUtilitiesKit/To Do/OWSProfileManager.m # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # SignalUtilitiesKit/Utilities/UIView+OWS.swift
2022-06-08 06:29:51 +02:00
default: throw MessageReceiverError.unknownEnvelopeType
2020-11-30 01:00:28 +01:00
}
}
2020-11-17 06:23:13 +01:00
// Don't process the envelope any further if the sender is blocked
guard (try? Contact.fetchOne(db, id: sender))?.isBlocked != true else {
throw MessageReceiverError.senderBlocked
}
// Parse the proto
2020-11-06 04:05:45 +01:00
let proto: SNProtoContent
2020-11-06 04:05:45 +01:00
do {
proto = try SNProtoContent.parseData(plaintext.removePadding())
}
catch {
2020-11-06 04:05:45 +01:00
SNLog("Couldn't parse proto due to error: \(error).")
throw error
2020-11-06 04:05:45 +01:00
}
// Parse the message
guard let message: Message = Message.createMessageFrom(proto, sender: sender) else {
throw MessageReceiverError.unknownMessage
}
// Ignore self sends if needed
guard message.isSelfSendValid || sender != userPublicKey else {
throw MessageReceiverError.selfSend
}
// Guard against control messages in open groups
guard !isOpenGroupMessage || message is VisibleMessage else {
throw MessageReceiverError.invalidMessage
}
// Finish parsing
message.sender = sender
message.recipient = userPublicKey
message.sentTimestamp = envelope.timestamp
message.receivedTimestamp = UInt64((Date().timeIntervalSince1970) * 1000)
message.groupPublicKey = groupPublicKey
message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) }
// Validate
var isValid: Bool = message.isValid
if message is VisibleMessage && !isValid && proto.dataMessage?.attachments.isEmpty == false {
isValid = true
}
guard isValid else {
throw MessageReceiverError.invalidMessage
}
// Extract the proper threadId for the message
let threadId: String = {
if let groupPublicKey: String = groupPublicKey { return groupPublicKey }
if let openGroupId: String = openGroupId { return openGroupId }
switch message {
case let message as VisibleMessage: return (message.syncTarget ?? sender)
case let message as ExpirationTimerUpdate: return (message.syncTarget ?? sender)
default: return sender
}
}()
return (message, proto, threadId)
2020-11-06 04:05:45 +01:00
}
// MARK: - Handling
public static func handle(
_ db: Database,
message: Message,
associatedWithProto proto: SNProtoContent,
openGroupId: String?,
dependencies: SMKDependencies = SMKDependencies()
) throws {
switch message {
case let message as ReadReceipt:
try MessageReceiver.handleReadReceipt(db, message: message)
case let message as TypingIndicator:
try MessageReceiver.handleTypingIndicator(db, message: message)
case let message as ClosedGroupControlMessage:
try MessageReceiver.handleClosedGroupControlMessage(db, message)
case let message as DataExtractionNotification:
try MessageReceiver.handleDataExtractionNotification(db, message: message)
case let message as ExpirationTimerUpdate:
try MessageReceiver.handleExpirationTimerUpdate(db, message: message)
case let message as ConfigurationMessage:
try MessageReceiver.handleConfigurationMessage(db, message: message)
case let message as UnsendRequest:
try MessageReceiver.handleUnsendRequest(db, message: message)
case let message as CallMessage:
try MessageReceiver.handleCallMessage(db, message: message)
case let message as MessageRequestResponse:
try MessageReceiver.handleMessageRequestResponse(db, message: message, dependencies: dependencies)
case let message as VisibleMessage:
try MessageReceiver.handleVisibleMessage(
db,
message: message,
associatedWithProto: proto,
openGroupId: openGroupId
)
default: fatalError()
}
Fixed a number of reported bugs, some cleanup, added animated profile support Added support for animated profile images (no ability to crop/resize) Updated the message trimming to only remove messages if the open group has 2000 messages or more Updated the message trimming setting to default to be on Updated the ContextMenu to fade out the snapshot as well (looked buggy if the device had even minor lag) Updated the ProfileManager to delete and re-download invalid avatar images (and updated the conversation screen to reload when avatars complete downloading) Updated the message request notification logic so it will show notifications when receiving a new message request as long as the user has read all the old ones (previously the user had to accept/reject all the old ones) Fixed a bug where the "trim open group messages" toggle was accessing UI off the main thread Fixed a bug where the "Chats" settings screen had a close button instead of a back button Fixed a bug where the 'viewsToMove' for the reply UI was inconsistent in some places Fixed an issue where the ProfileManager was doing all of it's validation (and writing to disk) within the database write closure which would block database writes unnecessarily Fixed a bug where a message request wouldn't be identified as such just because it wasn't visible in the conversations list Fixed a bug where opening a message request notification would result in the message request being in the wrong state (also wouldn't insert the 'MessageRequestsViewController' into the hierarchy) Fixed a bug where the avatar image wouldn't appear beside incoming closed group message in some situations cases Removed an error log that was essentially just spam Remove the logic to delete old profile images when calling save on a Profile (wouldn't get called if the row was modified directly and duplicates GarbageCollection logic) Remove the logic to send a notification when calling save on a Profile (wouldn't get called if the row was modified directly) Tweaked the message trimming description to be more accurate Cleaned up some duplicate logic used to determine if a notification should be shown Cleaned up some onion request logic (was passing the version info in some cases when not needed) Moved the push notification notify API call into the PushNotificationAPI class for consistency
2022-07-08 09:53:48 +02:00
// Perform any required post-handling logic
try MessageReceiver.postHandleMessage(db, message: message, openGroupId: openGroupId)
}
public static func postHandleMessage(
_ db: Database,
message: Message,
openGroupId: String?
) throws {
// When handling any non-typing indicator message we want to make sure the thread becomes
// visible (the only other spot this flag gets set is when sending messages)
switch message {
case is TypingIndicator: break
default:
guard let threadInfo: (id: String, variant: SessionThread.Variant) = threadInfo(db, message: message, openGroupId: openGroupId) else {
return
}
_ = try SessionThread
.fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant)
.with(shouldBeVisible: true)
.saved(db)
}
}
2022-08-04 09:10:24 +02:00
public static func handleOpenGroupReactions(
_ db: Database,
threadId: String,
2022-08-04 09:10:24 +02:00
openGroupMessageServerId: Int64,
openGroupReactions: [Reaction]
) throws {
guard let interactionId: Int64 = try? Interaction
.select(.id)
.filter(Interaction.Columns.threadId == threadId)
2022-08-04 09:10:24 +02:00
.filter(Interaction.Columns.openGroupServerMessageId == openGroupMessageServerId)
.asRequest(of: Int64.self)
.fetchOne(db)
else {
throw MessageReceiverError.invalidMessage
}
_ = try Reaction
.filter(Reaction.Columns.interactionId == interactionId)
.deleteAll(db)
for reaction in openGroupReactions {
try reaction.with(interactionId: interactionId).insert(db)
}
}
// MARK: - Convenience
internal static func threadInfo(_ db: Database, message: Message, openGroupId: String?) -> (id: String, variant: SessionThread.Variant)? {
if let openGroupId: String = openGroupId {
// Note: We don't want to create a thread for an open group if it doesn't exist
if (try? SessionThread.exists(db, id: openGroupId)) != true { return nil }
return (openGroupId, .openGroup)
}
if let groupPublicKey: String = message.groupPublicKey {
// Note: We don't want to create a thread for a closed group if it doesn't exist
if (try? SessionThread.exists(db, id: groupPublicKey)) != true { return nil }
return (groupPublicKey, .closedGroup)
}
// Extract the 'syncTarget' value if there is one
let maybeSyncTarget: String?
switch message {
case let message as VisibleMessage: maybeSyncTarget = message.syncTarget
case let message as ExpirationTimerUpdate: maybeSyncTarget = message.syncTarget
default: maybeSyncTarget = nil
}
// Note: We don't want to create a thread for a closed group if it doesn't exist
guard let contactId: String = (maybeSyncTarget ?? message.sender) else { return nil }
return (contactId, .contact)
}
internal static func updateProfileIfNeeded(
_ db: Database,
publicKey: String,
name: String?,
profilePictureUrl: String?,
profileKey: OWSAES256Key?,
sentTimestamp: TimeInterval,
dependencies: Dependencies = Dependencies()
) throws {
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey(db, dependencies: dependencies))
let profile: Profile = Profile.fetchOrCreate(id: publicKey)
var updatedProfile: Profile = profile
// Name
if let name = name, name != profile.name {
let shouldUpdate: Bool
if isCurrentUser {
shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) {
sentTimestamp > $0.timeIntervalSince1970
}
.defaulting(to: true)
}
else {
shouldUpdate = true
}
if shouldUpdate {
if isCurrentUser {
UserDefaults.standard[.lastDisplayNameUpdate] = Date(timeIntervalSince1970: sentTimestamp)
}
updatedProfile = updatedProfile.with(name: name)
}
}
// Profile picture & profile key
if
let profileKey: OWSAES256Key = profileKey,
let profilePictureUrl: String = profilePictureUrl,
profileKey.keyData.count == kAES256_KeyByteLength,
profileKey != profile.profileEncryptionKey
{
let shouldUpdate: Bool
if isCurrentUser {
shouldUpdate = given(UserDefaults.standard[.lastProfilePictureUpdate]) {
sentTimestamp > $0.timeIntervalSince1970
}
.defaulting(to: true)
}
else {
shouldUpdate = true
}
if shouldUpdate {
if isCurrentUser {
UserDefaults.standard[.lastProfilePictureUpdate] = Date(timeIntervalSince1970: sentTimestamp)
}
updatedProfile = updatedProfile.with(
profilePictureUrl: .update(profilePictureUrl),
profileEncryptionKey: .update(profileKey)
)
}
}
// Persist any changes
if updatedProfile != profile {
try updatedProfile.save(db)
}
// Download the profile picture if needed
if updatedProfile.profilePictureUrl != profile.profilePictureUrl || updatedProfile.profileEncryptionKey != profile.profileEncryptionKey {
db.afterNextTransaction { _ in
ProfileManager.downloadAvatar(for: updatedProfile)
}
}
}
2020-11-05 23:17:05 +01:00
}