session-ios/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift
Morgan Pretty 0db74ce1e3 Working on the MediaGallery and ClosedGroup handling
Fixed a couple of issues around the duplicate messages handling
Fixed a few issues with ClosedGroup polling and ClosedGroup control message handling
Started working through updating the MediaGallery
2022-05-08 22:01:39 +10:00

187 lines
8.6 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtilitiesKit
public enum MessageReceiver {
private static var lastEncryptionKeyPairRequest: [String: Date] = [:]
public static func parse(
_ db: Database,
data: Data,
serverExpirationTimestamp: TimeInterval?,
openGroupId: String? = nil,
openGroupMessageServerId: UInt64? = nil,
isRetry: Bool = false
) throws -> (Message, SNProtoContent) {
let userPublicKey: String = getUserHexEncodedPublicKey()
let isOpenGroupMessage: Bool = (openGroupMessageServerId != nil)
// Parse the envelope
let envelope = try SNProtoEnvelope.parseData(data)
// Decrypt the contents
guard let ciphertext = envelope.content else { throw MessageReceiverError.noData }
var plaintext: Data
var sender: String
var groupPublicKey: String? = nil
if isOpenGroupMessage {
(plaintext, sender) = (envelope.content!, envelope.source!)
}
else {
switch envelope.type {
case .sessionMessage:
guard let userX25519KeyPair: Box.KeyPair = Identity.fetchUserKeyPair() else {
throw MessageReceiverError.noUserX25519KeyPair
}
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
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)
/*
do {
try decrypt()
} catch {
do {
let now = Date()
// Don't spam encryption key pair requests
let shouldRequestEncryptionKeyPair = given(lastEncryptionKeyPairRequest[groupPublicKey!]) { now.timeIntervalSince($0) > 30 } ?? true
if shouldRequestEncryptionKeyPair {
try MessageSender.requestEncryptionKeyPair(for: groupPublicKey!, using: transaction as! YapDatabaseReadWriteTransaction)
lastEncryptionKeyPairRequest[groupPublicKey!] = now
}
}
throw error // Throw the * decryption * error and not the error generated by requestEncryptionKeyPair (if it generated one)
}
*/
default: throw MessageReceiverError.unknownEnvelopeType
}
}
// 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
let proto: SNProtoContent
do {
proto = try SNProtoContent.parseData((plaintext as NSData).removePadding())
}
catch {
SNLog("Couldn't parse proto due to error: \(error).")
throw error
}
// Parse the message
let message: Message? = {
if let readReceipt = ReadReceipt.fromProto(proto, sender: sender) { return readReceipt }
if let typingIndicator = TypingIndicator.fromProto(proto, sender: sender) { return typingIndicator }
if let closedGroupControlMessage = ClosedGroupControlMessage.fromProto(proto, sender: sender) { return closedGroupControlMessage }
if let dataExtractionNotification = DataExtractionNotification.fromProto(proto, sender: sender) { return dataExtractionNotification }
if let expirationTimerUpdate = ExpirationTimerUpdate.fromProto(proto, sender: sender) { return expirationTimerUpdate }
if let configurationMessage = ConfigurationMessage.fromProto(proto, sender: sender) { return configurationMessage }
if let unsendRequest = UnsendRequest.fromProto(proto, sender: sender) { return unsendRequest }
if let messageRequestResponse = MessageRequestResponse.fromProto(proto, sender: sender) { return messageRequestResponse }
if let visibleMessage = VisibleMessage.fromProto(proto, sender: sender) { return visibleMessage }
return nil
}()
if let message = message {
// Ignore self sends if needed
if !message.isSelfSendValid {
guard sender != userPublicKey else { throw MessageReceiverError.selfSend }
}
// Guard against control messages in open groups
if isOpenGroupMessage {
guard 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
// Validate
var isValid: Bool = message.isValid
if message is VisibleMessage && !isValid && proto.dataMessage?.attachments.isEmpty == false {
isValid = true
}
guard isValid else {
throw MessageReceiverError.invalidMessage
}
// Prevent ControlMessages from being handled multiple times if not supported
try ControlMessageProcessRecord(
threadId: {
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
}
}(),
message: message,
serverExpirationTimestamp: serverExpirationTimestamp,
isRetry: false
)?.insert(db)
// Return
return (message, proto)
}
throw MessageReceiverError.unknownMessage
}
}