mirror of https://github.com/oxen-io/session-ios
WIP
parent
f706e38ef7
commit
15c6784f0f
@ -0,0 +1,39 @@
|
||||
import SessionProtocolKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNNullMessage)
|
||||
public final class NullMessage : ControlMessage {
|
||||
|
||||
// MARK: Initialization
|
||||
public override init() { super.init() }
|
||||
|
||||
// MARK: Coding
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
public override func encode(with coder: NSCoder) {
|
||||
super.encode(with: coder)
|
||||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> NullMessage? {
|
||||
guard proto.nullMessage != nil else { return nil }
|
||||
return NullMessage()
|
||||
}
|
||||
|
||||
public override func toProto() -> SNProtoContent? {
|
||||
let nullMessageProto = SNProtoNullMessage.builder()
|
||||
let paddingSize = UInt.random(in: 0..<512) // random(in:) uses the system's default random generator, which is cryptographically secure
|
||||
let padding = Data.getSecureRandomData(ofSize: paddingSize)!
|
||||
nullMessageProto.setPadding(padding)
|
||||
let contentProto = SNProtoContent.builder()
|
||||
do {
|
||||
contentProto.setNullMessage(try nullMessageProto.build())
|
||||
return try contentProto.build()
|
||||
} catch {
|
||||
SNLog("Couldn't construct null message proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
public extension MessageSender {
|
||||
|
||||
/**
|
||||
* Wrap message sending in a Promise for easier callback chaining.
|
||||
*/
|
||||
func sendPromise(message: TSOutgoingMessage) -> Promise<Void> {
|
||||
let promise: Promise<Void> = Promise { resolver in
|
||||
self.send(message, success: { resolver.fulfill(()) }, failure: resolver.reject)
|
||||
}
|
||||
|
||||
// Ensure sends complete before they're GC'd.
|
||||
// This *should* be redundant, since we should be calling retainUntilComplete
|
||||
// at all call sites where the promise isn't otherwise retained.
|
||||
promise.retainUntilComplete()
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
@ -1,256 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Durably enqueues a message for sending.
|
||||
///
|
||||
/// The queue's operations (`MessageSenderOperation`) uses `MessageSender` to send a message.
|
||||
///
|
||||
/// ## Retry behavior
|
||||
///
|
||||
/// Like all JobQueue's, MessageSenderJobQueue implements retry handling for operation errors.
|
||||
///
|
||||
/// `MessageSender` also includes it's own retry logic necessary to encapsulate business logic around
|
||||
/// a user changing their Registration ID, or adding/removing devices. That is, it is sometimes *normal*
|
||||
/// for MessageSender to have to resend to a recipient multiple times before it is accepted, and doesn't
|
||||
/// represent a "failure" from the application standpoint.
|
||||
///
|
||||
/// So we have an inner non-durable retry (MessageSender) and an outer durable retry (MessageSenderJobQueue).
|
||||
///
|
||||
/// Both respect the `error.isRetryable` convention to be sure we don't keep retrying in some situations
|
||||
/// (e.g. rate limiting)
|
||||
|
||||
@objc(SSKMessageSenderJobQueue)
|
||||
public class MessageSenderJobQueue: NSObject, JobQueue {
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
|
||||
AppReadiness.runNowOrWhenAppWillBecomeReady {
|
||||
self.setup()
|
||||
}
|
||||
}
|
||||
|
||||
@objc(addMessage:transaction:)
|
||||
public func add(message: TSOutgoingMessage, transaction: YapDatabaseReadWriteTransaction) {
|
||||
self.add(message: message, removeMessageAfterSending: false, transaction: transaction)
|
||||
}
|
||||
|
||||
@objc(addMediaMessage:dataSource:contentType:sourceFilename:caption:albumMessageId:isTemporaryAttachment:)
|
||||
public func add(mediaMessage: TSOutgoingMessage, dataSource: DataSource, contentType: String, sourceFilename: String?, caption: String?, albumMessageId: String?, isTemporaryAttachment: Bool) {
|
||||
let attachmentInfo = OutgoingAttachmentInfo(dataSource: dataSource, contentType: contentType, sourceFilename: sourceFilename, caption: caption, albumMessageId: albumMessageId)
|
||||
add(mediaMessage: mediaMessage, attachmentInfos: [attachmentInfo], isTemporaryAttachment: isTemporaryAttachment)
|
||||
}
|
||||
|
||||
@objc(addMediaMessage:attachmentInfos:isTemporaryAttachment:)
|
||||
public func add(mediaMessage: TSOutgoingMessage, attachmentInfos: [OutgoingAttachmentInfo], isTemporaryAttachment: Bool) {
|
||||
OutgoingMessagePreparer.prepareAttachments(attachmentInfos,
|
||||
inMessage: mediaMessage,
|
||||
completionHandler: { error in
|
||||
if let error = error {
|
||||
Storage.writeSync { transaction in
|
||||
mediaMessage.update(sendingError: error, transaction: transaction)
|
||||
}
|
||||
} else {
|
||||
Storage.writeSync { transaction in
|
||||
self.add(message: mediaMessage, removeMessageAfterSending: isTemporaryAttachment, transaction: transaction)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func add(message: TSOutgoingMessage, removeMessageAfterSending: Bool, transaction: YapDatabaseReadWriteTransaction) {
|
||||
assert(AppReadiness.isAppReady() || CurrentAppContext().isRunningTests)
|
||||
|
||||
let jobRecord: SSKMessageSenderJobRecord
|
||||
do {
|
||||
jobRecord = try SSKMessageSenderJobRecord(message: message, removeMessageAfterSending: false, label: self.jobRecordLabel)
|
||||
} catch {
|
||||
owsFailDebug("Failed to build job due to error: \(error).")
|
||||
return
|
||||
}
|
||||
self.add(jobRecord: jobRecord, transaction: transaction)
|
||||
}
|
||||
|
||||
// MARK: JobQueue
|
||||
|
||||
public typealias DurableOperationType = MessageSenderOperation
|
||||
public static let jobRecordLabel: String = "MessageSender"
|
||||
public static let maxRetries: UInt = 1 // Loki: We have our own retrying
|
||||
public let requiresInternet: Bool = true
|
||||
public var runningOperations: [MessageSenderOperation] = []
|
||||
|
||||
public var jobRecordLabel: String {
|
||||
return type(of: self).jobRecordLabel
|
||||
}
|
||||
|
||||
@objc
|
||||
public func setup() {
|
||||
defaultSetup()
|
||||
}
|
||||
|
||||
public var isSetup: Bool = false
|
||||
|
||||
/// Used when the user clears their database to cancel any outstanding jobs.
|
||||
@objc public func clearAllJobs() {
|
||||
Storage.writeSync { transaction in
|
||||
let statuses: [SSKJobRecordStatus] = [ .unknown, .ready, .running, .permanentlyFailed ]
|
||||
var records: [SSKJobRecord] = []
|
||||
statuses.forEach {
|
||||
records += self.finder.allRecords(label: self.jobRecordLabel, status: $0, transaction: transaction)
|
||||
}
|
||||
records.forEach { $0.remove(with: transaction) }
|
||||
}
|
||||
}
|
||||
|
||||
public func didMarkAsReady(oldJobRecord: SSKMessageSenderJobRecord, transaction: YapDatabaseReadWriteTransaction) {
|
||||
if let messageId = oldJobRecord.messageId, let message = TSOutgoingMessage.fetch(uniqueId: messageId, transaction: transaction) {
|
||||
message.updateWithMarkingAllUnsentRecipientsAsSending(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
public func buildOperation(jobRecord: SSKMessageSenderJobRecord, transaction: YapDatabaseReadTransaction) throws -> MessageSenderOperation {
|
||||
let message: TSOutgoingMessage
|
||||
if let invisibleMessage = jobRecord.invisibleMessage {
|
||||
message = invisibleMessage
|
||||
} else if let messageId = jobRecord.messageId, let fetchedMessage = TSOutgoingMessage.fetch(uniqueId: messageId, transaction: transaction) {
|
||||
message = fetchedMessage
|
||||
} else {
|
||||
assert(jobRecord.messageId != nil)
|
||||
throw JobError.obsolete(description: "Message no longer exists.")
|
||||
}
|
||||
|
||||
return MessageSenderOperation(message: message, jobRecord: jobRecord)
|
||||
}
|
||||
|
||||
var senderQueues: [String: OperationQueue] = [:]
|
||||
let defaultQueue: OperationQueue = {
|
||||
let operationQueue = OperationQueue()
|
||||
operationQueue.name = "DefaultSendingQueue"
|
||||
operationQueue.maxConcurrentOperationCount = 1
|
||||
operationQueue.qualityOfService = .userInitiated
|
||||
|
||||
return operationQueue
|
||||
}()
|
||||
|
||||
// We use a per-thread serial OperationQueue to ensure messages are delivered to the
|
||||
// service in the order the user sent them.
|
||||
public func operationQueue(jobRecord: SSKMessageSenderJobRecord) -> OperationQueue {
|
||||
guard let threadId = jobRecord.threadId else {
|
||||
return defaultQueue
|
||||
}
|
||||
|
||||
guard let existingQueue = senderQueues[threadId] else {
|
||||
let operationQueue = OperationQueue()
|
||||
operationQueue.name = "SendingQueue:\(threadId)"
|
||||
operationQueue.maxConcurrentOperationCount = 1
|
||||
operationQueue.qualityOfService = .userInitiated
|
||||
|
||||
senderQueues[threadId] = operationQueue
|
||||
|
||||
return operationQueue
|
||||
}
|
||||
|
||||
return existingQueue
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageSenderOperation: OWSOperation, DurableOperation {
|
||||
|
||||
// MARK: DurableOperation
|
||||
|
||||
public let jobRecord: SSKMessageSenderJobRecord
|
||||
|
||||
weak public var durableOperationDelegate: MessageSenderJobQueue?
|
||||
|
||||
public var operation: OWSOperation {
|
||||
return self
|
||||
}
|
||||
|
||||
// MARK: Init
|
||||
|
||||
let message: TSOutgoingMessage
|
||||
|
||||
init(message: TSOutgoingMessage, jobRecord: SSKMessageSenderJobRecord) {
|
||||
self.message = message
|
||||
self.jobRecord = jobRecord
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
var messageSender: MessageSender {
|
||||
return SSKEnvironment.shared.messageSender
|
||||
}
|
||||
|
||||
// MARK: OWSOperation
|
||||
|
||||
override public func run() {
|
||||
self.messageSender.send(message, success: reportSuccess, failure: reportError)
|
||||
}
|
||||
|
||||
override public func didSucceed() {
|
||||
Storage.writeSync { transaction in
|
||||
self.durableOperationDelegate?.durableOperationDidSucceed(self, transaction: transaction)
|
||||
|
||||
if self.jobRecord.removeMessageAfterSending {
|
||||
self.message.remove(with: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func didReportError(_ error: Error) {
|
||||
let message = self.message
|
||||
var isFailedSessionRequest = false
|
||||
if message is SessionRequestMessage, let publicKey = message.thread.contactIdentifier() {
|
||||
isFailedSessionRequest = (Storage.getSessionRequestSentTimestamp(for: publicKey) == message.timestamp)
|
||||
}
|
||||
Storage.writeSync { transaction in
|
||||
if isFailedSessionRequest, let publicKey = message.thread.contactIdentifier() {
|
||||
Storage.setSessionRequestSentTimestamp(for: publicKey, to: 0, using: transaction)
|
||||
}
|
||||
|
||||
self.durableOperationDelegate?.durableOperation(self, didReportError: error, transaction: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
override public func retryInterval() -> TimeInterval {
|
||||
// Arbitrary backoff factor...
|
||||
// With backOffFactor of 1.9
|
||||
// try 1 delay: 0.00s
|
||||
// try 2 delay: 0.19s
|
||||
// ...
|
||||
// try 5 delay: 1.30s
|
||||
// ...
|
||||
// try 11 delay: 61.31s
|
||||
let backoffFactor = 1.9
|
||||
let maxBackoff = 15 * kMinuteInterval
|
||||
|
||||
let seconds = 0.1 * min(maxBackoff, pow(backoffFactor, Double(self.jobRecord.failureCount)))
|
||||
return seconds
|
||||
}
|
||||
|
||||
override public func didFail(error: Error) {
|
||||
let message = self.message
|
||||
var isFailedSessionRequest = false
|
||||
if message is SessionRequestMessage, let publicKey = message.thread.contactIdentifier() {
|
||||
isFailedSessionRequest = (Storage.getSessionRequestSentTimestamp(for: publicKey) == message.timestamp)
|
||||
}
|
||||
Storage.writeSync { transaction in
|
||||
if isFailedSessionRequest, let publicKey = message.thread.contactIdentifier() {
|
||||
Storage.setSessionRequestSentTimestamp(for: publicKey, to: 0, using: transaction)
|
||||
}
|
||||
|
||||
self.durableOperationDelegate?.durableOperation(self, didFailWithError: error, transaction: transaction)
|
||||
|
||||
self.message.update(sendingError: error, transaction: transaction)
|
||||
|
||||
if self.jobRecord.removeMessageAfterSending {
|
||||
self.message.remove(with: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,274 +0,0 @@
|
||||
import PromiseKit
|
||||
|
||||
// A few notes about making changes in this file:
|
||||
//
|
||||
// • Don't use a database transaction if you can avoid it.
|
||||
// • If you do need to use a database transaction, use a read transaction if possible.
|
||||
// • For write transactions, consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions).
|
||||
// • Think carefully about adding a function; there might already be one for what you need.
|
||||
// • Document the expected cases in which a function will be used
|
||||
// • Express those cases in tests.
|
||||
|
||||
@objc(LKMultiDeviceProtocol)
|
||||
public final class MultiDeviceProtocol : NSObject {
|
||||
|
||||
/// A mapping from hex encoded public key to date updated.
|
||||
///
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
public static var lastDeviceLinkUpdate: [String:Date] = [:]
|
||||
|
||||
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
||||
|
||||
// MARK: Settings
|
||||
public static let deviceLinkUpdateInterval: TimeInterval = 60
|
||||
|
||||
// MARK: Multi Device Destination
|
||||
public struct MultiDeviceDestination : Hashable {
|
||||
public let publicKey: String
|
||||
public let isMaster: Bool
|
||||
}
|
||||
|
||||
// MARK: - General
|
||||
|
||||
@objc(isUnlinkDeviceMessage:)
|
||||
public static func isUnlinkDeviceMessage(_ dataMessage: SSKProtoDataMessage) -> Bool {
|
||||
let unlinkDeviceFlag = SSKProtoDataMessage.SSKProtoDataMessageFlags.unlinkDevice
|
||||
return dataMessage.flags & UInt32(unlinkDeviceFlag.rawValue) != 0
|
||||
}
|
||||
|
||||
public static func getUserLinkedDevices() -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
storage.dbReadConnection.read { transaction in
|
||||
result = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public static func isSlaveThread(_ thread: TSThread) -> Bool {
|
||||
guard let thread = thread as? TSContactThread else { return false }
|
||||
var isSlaveThread = false
|
||||
storage.dbReadConnection.read { transaction in
|
||||
isSlaveThread = storage.getMasterHexEncodedPublicKey(for: thread.contactIdentifier(), in: transaction) != nil
|
||||
}
|
||||
return isSlaveThread
|
||||
}
|
||||
|
||||
// MARK: - Sending (Part 1)
|
||||
|
||||
@objc(isMultiDeviceRequiredForMessage:toPublicKey:)
|
||||
public static func isMultiDeviceRequired(for message: TSOutgoingMessage, to publicKey: String) -> Bool {
|
||||
return !(message is DeviceLinkMessage) && !(message is UnlinkDeviceMessage) && (message.thread as? TSGroupThread)?.groupModel.groupType != .openGroup
|
||||
&& !Storage.getUserClosedGroupPublicKeys().contains(publicKey)
|
||||
}
|
||||
|
||||
private static func copy(_ messageSend: OWSMessageSend, for destination: MultiDeviceDestination, with seal: Resolver<Void>) -> OWSMessageSend {
|
||||
var recipient: SignalRecipient!
|
||||
storage.dbReadConnection.read { transaction in
|
||||
recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: destination.publicKey, transaction: transaction)
|
||||
}
|
||||
// TODO: Why is it okay that the thread, sender certificate, etc. don't get changed?
|
||||
return OWSMessageSend(message: messageSend.message, thread: messageSend.thread, recipient: recipient,
|
||||
senderCertificate: messageSend.senderCertificate, udAccess: messageSend.udAccess, localNumber: messageSend.localNumber, success: {
|
||||
seal.fulfill(())
|
||||
}, failure: { error in
|
||||
seal.reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
private static func sendMessage(_ messageSend: OWSMessageSend, to destination: MultiDeviceDestination, in transaction: YapDatabaseReadTransaction) -> Promise<Void> {
|
||||
let (threadPromise, threadPromiseSeal) = Promise<TSThread>.pending()
|
||||
if messageSend.message.thread.isGroupThread() {
|
||||
threadPromiseSeal.fulfill(messageSend.message.thread)
|
||||
} else if let thread = TSContactThread.getWithContactId(destination.publicKey, transaction: transaction) {
|
||||
threadPromiseSeal.fulfill(thread)
|
||||
} else {
|
||||
Storage.write { transaction in
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: destination.publicKey, transaction: transaction)
|
||||
threadPromiseSeal.fulfill(thread)
|
||||
}
|
||||
}
|
||||
return threadPromise.then2 { thread -> Promise<Void> in
|
||||
let message = messageSend.message
|
||||
let messageSender = SSKEnvironment.shared.messageSender
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
let messageSendCopy = copy(messageSend, for: destination, with: seal)
|
||||
OWSDispatch.sendingQueue().async {
|
||||
messageSender.sendMessage(messageSendCopy)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
/// See [Multi Device Message Sending](https://github.com/loki-project/session-protocol-docs/wiki/Multi-Device-Message-Sending) for more information.
|
||||
@objc(sendMessageToDestinationAndLinkedDevices:transaction:)
|
||||
public static func sendMessageToDestinationAndLinkedDevices(_ messageSend: OWSMessageSend, in transaction: YapDatabaseReadTransaction) {
|
||||
// if !messageSend.isUDSend && messageSend.recipient.recipientId() != getUserHexEncodedPublicKey() {
|
||||
// #if DEBUG
|
||||
// preconditionFailure()
|
||||
// #endif
|
||||
// }
|
||||
let message = messageSend.message
|
||||
let messageSender = SSKEnvironment.shared.messageSender
|
||||
if !isMultiDeviceRequired(for: message, to: messageSend.recipient.recipientId()) {
|
||||
print("[Loki] sendMessageToDestinationAndLinkedDevices(_:in:) invoked for a message that doesn't require multi device routing.")
|
||||
OWSDispatch.sendingQueue().async {
|
||||
messageSender.sendMessage(messageSend)
|
||||
}
|
||||
return
|
||||
}
|
||||
print("[Loki] Sending \(type(of: message)) message using multi device routing.")
|
||||
let publicKey = messageSend.recipient.recipientId()
|
||||
getMultiDeviceDestinations(for: publicKey, in: transaction).done2 { destinations in
|
||||
var promises: [Promise<Void>] = []
|
||||
let masterDestination = destinations.first { $0.isMaster }
|
||||
if let masterDestination = masterDestination {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
promises.append(sendMessage(messageSend, to: masterDestination, in: transaction))
|
||||
}
|
||||
}
|
||||
let slaveDestinations = destinations.filter { !$0.isMaster }
|
||||
slaveDestinations.forEach { slaveDestination in
|
||||
storage.dbReadConnection.read { transaction in
|
||||
promises.append(sendMessage(messageSend, to: slaveDestination, in: transaction))
|
||||
}
|
||||
}
|
||||
when(resolved: promises).done(on: OWSDispatch.sendingQueue()) { results in
|
||||
let errors = results.compactMap { result -> Error? in
|
||||
if case PromiseKit.Result.rejected(let error) = result {
|
||||
return error
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if errors.isEmpty {
|
||||
messageSend.success()
|
||||
} else {
|
||||
messageSend.failure(errors.first!)
|
||||
}
|
||||
}
|
||||
}.catch2 { error in
|
||||
// Proceed even if updating the recipient's device links failed, so that message sending
|
||||
// is independent of whether the file server is online
|
||||
OWSDispatch.sendingQueue().async {
|
||||
messageSender.sendMessage(messageSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(updateDeviceLinksIfNeededForPublicKey:transaction:)
|
||||
public static func updateDeviceLinksIfNeeded(for publicKey: String, in transaction: YapDatabaseReadTransaction) -> AnyPromise {
|
||||
return AnyPromise.from(getMultiDeviceDestinations(for: publicKey, in: transaction))
|
||||
}
|
||||
|
||||
// MARK: - Receiving
|
||||
|
||||
@objc(handleDeviceLinkMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleDeviceLinkMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
guard let deviceLinkMessage = protoContent.lokiDeviceLinkMessage, let master = deviceLinkMessage.masterPublicKey,
|
||||
let slave = deviceLinkMessage.slavePublicKey, let slaveSignature = deviceLinkMessage.slaveSignature else {
|
||||
return print("[Loki] Received an invalid device link message.")
|
||||
}
|
||||
let deviceLinkingSession = DeviceLinkingSession.current
|
||||
if let masterSignature = deviceLinkMessage.masterSignature { // Authorization
|
||||
print("[Loki] Received a device link authorization from: \(publicKey).") // Intentionally not `master`
|
||||
if let deviceLinkingSession = deviceLinkingSession {
|
||||
deviceLinkingSession.processLinkingAuthorization(from: master, for: slave, masterSignature: masterSignature, slaveSignature: slaveSignature)
|
||||
} else {
|
||||
print("[Loki] Received a device link authorization without a session; ignoring.")
|
||||
}
|
||||
// Set any profile info (the device link authorization also includes the master device's profile info)
|
||||
if let dataMessage = protoContent.dataMessage {
|
||||
SessionMetaProtocol.updateDisplayNameIfNeeded(for: master, using: dataMessage, in: transaction)
|
||||
SessionMetaProtocol.updateProfileKeyIfNeeded(for: master, using: dataMessage)
|
||||
}
|
||||
} else { // Request
|
||||
print("[Loki] Received a device link request from: \(publicKey).") // Intentionally not `slave`
|
||||
if let deviceLinkingSession = deviceLinkingSession {
|
||||
deviceLinkingSession.processLinkingRequest(from: slave, to: master, with: slaveSignature)
|
||||
} else {
|
||||
NotificationCenter.default.post(name: .unexpectedDeviceLinkRequestReceived, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(handleUnlinkDeviceMessage:wrappedIn:transaction:)
|
||||
public static func handleUnlinkDeviceMessage(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
// Check that the request was sent by our master device
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
guard let userMasterPublicKey = storage.getMasterHexEncodedPublicKey(for: userPublicKey, in: transaction) else { return }
|
||||
let wasSentByMasterDevice = (userMasterPublicKey == publicKey)
|
||||
guard wasSentByMasterDevice else { return }
|
||||
// Ignore the request if we don't know about the device link in question
|
||||
let masterDeviceLinks = storage.getDeviceLinks(for: userMasterPublicKey, in: transaction)
|
||||
if !masterDeviceLinks.contains(where: {
|
||||
$0.master.publicKey == userMasterPublicKey && $0.slave.publicKey == userPublicKey
|
||||
}) {
|
||||
return
|
||||
}
|
||||
FileServerAPI.getDeviceLinks(associatedWith: userPublicKey).done2 { slaveDeviceLinks in
|
||||
// Check that the device link IS present on the file server.
|
||||
// Note that the device link as seen from the master device's perspective has been deleted at this point, but the
|
||||
// device link as seen from the slave perspective hasn't.
|
||||
if slaveDeviceLinks.contains(where: {
|
||||
$0.master.publicKey == userMasterPublicKey && $0.slave.publicKey == userPublicKey
|
||||
}) {
|
||||
for deviceLink in slaveDeviceLinks { // In theory there should only be one
|
||||
FileServerAPI.removeDeviceLink(deviceLink) // Attempt to clean up on the file server
|
||||
}
|
||||
UserDefaults.standard[.wasUnlinked] = true
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .dataNukeRequested, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sending (Part 2)
|
||||
|
||||
// Here (in a non-@objc extension) because it doesn't interoperate well with Obj-C
|
||||
public extension MultiDeviceProtocol {
|
||||
|
||||
fileprivate static func getMultiDeviceDestinations(for publicKey: String, in transaction: YapDatabaseReadTransaction) -> Promise<Set<MultiDeviceDestination>> {
|
||||
let (promise, seal) = Promise<Set<MultiDeviceDestination>>.pending()
|
||||
func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
var destinations: Set<MultiDeviceDestination> = []
|
||||
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
|
||||
let masterDestination = MultiDeviceDestination(publicKey: masterPublicKey, isMaster: true)
|
||||
destinations.insert(masterDestination)
|
||||
let deviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction)
|
||||
let slaveDestinations = deviceLinks.map { MultiDeviceDestination(publicKey: $0.slave.publicKey, isMaster: false) }
|
||||
destinations.formUnion(slaveDestinations)
|
||||
seal.fulfill(destinations)
|
||||
}
|
||||
}
|
||||
let timeSinceLastUpdate: TimeInterval
|
||||
if let lastDeviceLinkUpdate = lastDeviceLinkUpdate[publicKey] {
|
||||
timeSinceLastUpdate = Date().timeIntervalSince(lastDeviceLinkUpdate)
|
||||
} else {
|
||||
timeSinceLastUpdate = .infinity
|
||||
}
|
||||
if timeSinceLastUpdate > deviceLinkUpdateInterval {
|
||||
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
|
||||
FileServerAPI.getDeviceLinks(associatedWith: masterPublicKey).done2 { _ in
|
||||
getDestinations()
|
||||
lastDeviceLinkUpdate[publicKey] = Date()
|
||||
}.catch2 { error in
|
||||
if (error as? DotNetAPI.Error) == DotNetAPI.Error.parsingFailed {
|
||||
// Don't immediately re-fetch in case of failure due to a parsing error
|
||||
lastDeviceLinkUpdate[publicKey] = Date()
|
||||
getDestinations()
|
||||
} else {
|
||||
print("[Loki] Failed to get device links due to error: \(error).")
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getDestinations()
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSMessageHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSPrimaryStorage;
|
||||
@class SSKProtoEnvelope;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
|
||||
@interface OWSMessageDecryptResult : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSData *envelopeData;
|
||||
@property (nonatomic, readonly, nullable) NSData *plaintextData;
|
||||
@property (nonatomic, readonly) NSString *source;
|
||||
@property (nonatomic, readonly) UInt32 sourceDevice;
|
||||
@property (nonatomic, readonly) BOOL isUDMessage;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// Decryption result includes the envelope since the envelope
|
||||
// may be altered by the decryption process.
|
||||
typedef void (^DecryptSuccessBlock)(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction);
|
||||
typedef void (^DecryptFailureBlock)(void);
|
||||
|
||||
@interface OWSMessageDecrypter : OWSMessageHandler
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
// decryptEnvelope: can be called from any thread.
|
||||
// successBlock & failureBlock will be called an arbitrary thread.
|
||||
//
|
||||
// Exactly one of successBlock & failureBlock will be called,
|
||||
// once.
|
||||
- (void)decryptEnvelope:(SSKProtoEnvelope *)envelope
|
||||
envelopeData:(NSData *)envelopeData
|
||||
successBlock:(DecryptSuccessBlock)successBlock
|
||||
failureBlock:(DecryptFailureBlock)failureBlock;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,686 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageDecrypter.h"
|
||||
#import "NSData+messagePadding.h"
|
||||
#import "NSString+SSK.h"
|
||||
#import "NotificationsProtocol.h"
|
||||
#import "OWSAnalytics.h"
|
||||
#import "OWSBlockingManager.h"
|
||||
#import "OWSDevice.h"
|
||||
#import "OWSError.h"
|
||||
#import "OWSIdentityManager.h"
|
||||
#import "OWSPrimaryStorage+PreKeyStore.h"
|
||||
#import "OWSPrimaryStorage+SessionStore.h"
|
||||
#import "OWSPrimaryStorage+SignedPreKeyStore.h"
|
||||
#import "OWSPrimaryStorage.h"
|
||||
#import "SSKEnvironment.h"
|
||||
#import "SignalRecipient.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import "TSContactThread.h"
|
||||
#import "TSErrorMessage.h"
|
||||
#import "TSPreKeyManager.h"
|
||||
#import <SessionProtocolKit/AxolotlExceptions.h>
|
||||
#import <SessionProtocolKit/NSData+keyVersionByte.h>
|
||||
#import <SessionProtocolKit/SessionCipher.h>
|
||||
#import <SignalCoreKit/NSData+OWS.h>
|
||||
#import <SignalCoreKit/Randomness.h>
|
||||
#import <SignalCoreKit/SCKExceptionWrapper.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import "SSKAsserts.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDescription)
|
||||
{
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
OWSCFailDebug(@"Caller should provide specific error");
|
||||
return OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptUDMessage, fallbackErrorDescription);
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessageDecryptResult ()
|
||||
|
||||
@property (nonatomic) NSData *envelopeData;
|
||||
@property (nonatomic, nullable) NSData *plaintextData;
|
||||
@property (nonatomic) NSString *source;
|
||||
@property (nonatomic) UInt32 sourceDevice;
|
||||
@property (nonatomic) BOOL isUDMessage;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageDecryptResult
|
||||
|
||||
+ (OWSMessageDecryptResult *)resultWithEnvelopeData:(NSData *)envelopeData
|
||||
plaintextData:(nullable NSData *)plaintextData
|
||||
source:(NSString *)source
|
||||
sourceDevice:(UInt32)sourceDevice
|
||||
isUDMessage:(BOOL)isUDMessage
|
||||
{
|
||||
OWSAssertDebug(envelopeData);
|
||||
OWSAssertDebug(source.length > 0);
|
||||
OWSAssertDebug(sourceDevice > 0);
|
||||
|
||||
OWSMessageDecryptResult *result = [OWSMessageDecryptResult new];
|
||||
result.envelopeData = envelopeData;
|
||||
result.plaintextData = plaintextData;
|
||||
result.source = source;
|
||||
result.sourceDevice = sourceDevice;
|
||||
result.isUDMessage = isUDMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessageDecrypter ()
|
||||
|
||||
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
|
||||
@property (nonatomic, readonly) SNSessionRestorationImplementation *sessionResetImplementation;
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageDecrypter
|
||||
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_primaryStorage = primaryStorage;
|
||||
_sessionResetImplementation = [SNSessionRestorationImplementation new];
|
||||
_dbConnection = primaryStorage.newDatabaseConnection;
|
||||
|
||||
OWSSingletonAssert();
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.blockingManager);
|
||||
|
||||
return SSKEnvironment.shared.blockingManager;
|
||||
}
|
||||
|
||||
- (OWSIdentityManager *)identityManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.identityManager);
|
||||
|
||||
return SSKEnvironment.shared.identityManager;
|
||||
}
|
||||
|
||||
- (id<OWSUDManager>)udManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.udManager);
|
||||
|
||||
return SSKEnvironment.shared.udManager;
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
||||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
#pragma mark - Blocking
|
||||
|
||||
- (BOOL)isEnvelopeSenderBlocked:(SSKProtoEnvelope *)envelope
|
||||
{
|
||||
OWSAssertDebug(envelope);
|
||||
|
||||
return [self.blockingManager.blockedPhoneNumbers containsObject:envelope.source];
|
||||
}
|
||||
|
||||
#pragma mark - Decryption
|
||||
|
||||
- (void)decryptEnvelope:(SSKProtoEnvelope *)envelope
|
||||
envelopeData:(NSData *)envelopeData
|
||||
successBlock:(DecryptSuccessBlock)successBlockParameter
|
||||
failureBlock:(DecryptFailureBlock)failureBlockParameter
|
||||
{
|
||||
OWSAssertDebug(envelope);
|
||||
OWSAssertDebug(envelopeData);
|
||||
OWSAssertDebug(successBlockParameter);
|
||||
OWSAssertDebug(failureBlockParameter);
|
||||
OWSAssertDebug([self.tsAccountManager isRegistered]);
|
||||
|
||||
// successBlock is called synchronously so that we can avail ourselves of
|
||||
// the transaction.
|
||||
//
|
||||
// Ensure that failureBlock is called on a worker queue.
|
||||
DecryptFailureBlock failureBlock = ^() {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
failureBlockParameter();
|
||||
});
|
||||
};
|
||||
|
||||
NSString *localRecipientId = self.tsAccountManager.localNumber;
|
||||
uint32_t localDeviceId = OWSDevicePrimaryDeviceId;
|
||||
DecryptSuccessBlock successBlock = ^(
|
||||
OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
|
||||
// Ensure all blocked messages are discarded.
|
||||
if ([self isEnvelopeSenderBlocked:envelope]) {
|
||||
OWSLogInfo(@"Ignoring blocked envelope from: %@.", envelope.source);
|
||||
return failureBlock();
|
||||
}
|
||||
|
||||
if ([result.source isEqualToString:localRecipientId] && result.sourceDevice == localDeviceId) {
|
||||
// Self-sent messages should be discarded during the decryption process.
|
||||
OWSFailDebug(@"Unexpected self-sent sync message.");
|
||||
return failureBlock();
|
||||
}
|
||||
|
||||
// Having received a valid (decryptable) message from this user,
|
||||
// make note of the fact that they have a valid Signal account.
|
||||
[SignalRecipient markRecipientAsRegistered:result.source deviceId:result.sourceDevice transaction:transaction];
|
||||
|
||||
successBlockParameter(result, transaction);
|
||||
};
|
||||
|
||||
@try {
|
||||
OWSLogInfo(@"Decrypting envelope: %@.", [self descriptionForEnvelope:envelope]);
|
||||
|
||||
if (envelope.type != SSKProtoEnvelopeTypeUnidentifiedSender) {
|
||||
if (!envelope.hasSource || envelope.source.length < 1 || ![ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source]) {
|
||||
OWSFailDebug(@"Incoming envelope with invalid source.");
|
||||
return failureBlock();
|
||||
}
|
||||
if (!envelope.hasSourceDevice || envelope.sourceDevice < 1) {
|
||||
OWSFailDebug(@"Incoming envelope with invalid source device.");
|
||||
return failureBlock();
|
||||
}
|
||||
|
||||
// We block UD messages later, after they are decrypted.
|
||||
if ([self isEnvelopeSenderBlocked:envelope]) {
|
||||
OWSLogInfo(@"Ignoring blocked envelope from: %@.", envelope.source);
|
||||
return failureBlock();
|
||||
}
|
||||
}
|
||||
|
||||
switch (envelope.type) {
|
||||
case SSKProtoEnvelopeTypeCiphertext: {
|
||||
[self throws_decryptSecureMessage:envelope
|
||||
envelopeData:envelopeData
|
||||
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
|
||||
OWSLogDebug(@"Decrypted secure message.");
|
||||
successBlock(result, transaction);
|
||||
}
|
||||
failureBlock:^(NSError *_Nullable error) {
|
||||
OWSLogError(@"Decrypting secure message from: %@ failed with error: %@.",
|
||||
envelopeAddress(envelope),
|
||||
error);
|
||||
OWSProdError([OWSAnalyticsEvents messageManagerErrorCouldNotHandleSecureMessage]);
|
||||
failureBlock();
|
||||
}];
|
||||
// Return to avoid double-acknowledging.
|
||||
return;
|
||||
}
|
||||
case SSKProtoEnvelopeTypePrekeyBundle: {
|
||||
[self throws_decryptPreKeyBundle:envelope
|
||||
envelopeData:envelopeData
|
||||
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
|
||||
OWSLogDebug(@"Decrypted pre key bundle message.");
|
||||
successBlock(result, transaction);
|
||||
}
|
||||
failureBlock:^(NSError *_Nullable error) {
|
||||
OWSLogError(@"Decrypting pre key bundle message from: %@ failed with error: %@.",
|
||||
envelopeAddress(envelope),
|
||||
error);
|
||||
OWSProdError([OWSAnalyticsEvents messageManagerErrorCouldNotHandlePrekeyBundle]);
|
||||
failureBlock();
|
||||
}];
|
||||
// Return to avoid double-acknowledging.
|
||||
return;
|
||||
}
|
||||
// These message types don't have a payload to decrypt.
|
||||
case SSKProtoEnvelopeTypeReceipt:
|
||||
case SSKProtoEnvelopeTypeKeyExchange:
|
||||
case SSKProtoEnvelopeTypeUnknown: {
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
OWSMessageDecryptResult *result =
|
||||
[OWSMessageDecryptResult resultWithEnvelopeData:envelopeData
|
||||
plaintextData:nil
|
||||
source:envelope.source
|
||||
sourceDevice:envelope.sourceDevice
|
||||
isUDMessage:NO];
|
||||
successBlock(result, transaction);
|
||||
}];
|
||||
// Return to avoid double-acknowledging.
|
||||
return;
|
||||
}
|
||||
case SSKProtoEnvelopeTypeClosedGroupCiphertext: {
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSError *error = nil;
|
||||
NSArray *plaintextAndSenderPublicKey = [LKClosedGroupUtilities decryptEnvelope:envelope transaction:transaction error:&error];
|
||||
if (error != nil) { return failureBlock(); }
|
||||
NSData *plaintext = plaintextAndSenderPublicKey[0];
|
||||
NSString *senderPublicKey = plaintextAndSenderPublicKey[1];
|
||||
SSKProtoEnvelopeBuilder *newEnvelope = [envelope asBuilder];
|
||||
[newEnvelope setSource:senderPublicKey];
|
||||
NSData *newEnvelopeAsData = [newEnvelope buildSerializedDataAndReturnError:&error];
|
||||
if (error != nil) { return failureBlock(); }
|
||||
NSString *userPublicKey = [OWSIdentityManager.sharedManager.identityKeyPair hexEncodedPublicKey];
|
||||
if ([senderPublicKey isEqual:userPublicKey]) { return failureBlock(); }
|
||||
OWSMessageDecryptResult *result = [OWSMessageDecryptResult resultWithEnvelopeData:newEnvelopeAsData
|
||||
plaintextData:[plaintext removePadding]
|
||||
source:senderPublicKey
|
||||
sourceDevice:OWSDevicePrimaryDeviceId
|
||||
isUDMessage:NO];
|
||||
successBlock(result, transaction);
|
||||
}];
|
||||
return;
|
||||
}
|
||||
case SSKProtoEnvelopeTypeUnidentifiedSender: {
|
||||
[self decryptUnidentifiedSender:envelope
|
||||
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
|
||||
OWSLogDebug(@"Decrypted unidentified sender message.");
|
||||
successBlock(result, transaction);
|
||||
}
|
||||
failureBlock:^(NSError *_Nullable error) {
|
||||
OWSLogError(@"Decrypting unidentified sender message from: %@ failed with error: %@.",
|
||||
envelopeAddress(envelope),
|
||||
error);
|
||||
OWSProdError([OWSAnalyticsEvents messageManagerErrorCouldNotHandleUnidentifiedSenderMessage]);
|
||||
failureBlock();
|
||||
}];
|
||||
// Return to avoid double-acknowledging.
|
||||
return;
|
||||
}
|
||||
default:
|
||||
OWSLogWarn(@"Received unhandled envelope type: %d.", (int)envelope.type);
|
||||
break;
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
OWSFailDebug(@"Received an invalid envelope: %@.", exception.debugDescription);
|
||||
OWSProdFail([OWSAnalyticsEvents messageManagerErrorInvalidProtocolMessage]);
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
TSErrorMessage *errorMessage = [TSErrorMessage corruptedMessageInUnknownThread];
|
||||
[SSKEnvironment.shared.notificationsManager notifyUserForThreadlessErrorMessage:errorMessage
|
||||
transaction:transaction];
|
||||
}];
|
||||
}
|
||||
|
||||
failureBlock();
|
||||
}
|
||||
|
||||
- (void)throws_decryptSecureMessage:(SSKProtoEnvelope *)envelope
|
||||
envelopeData:(NSData *)envelopeData
|
||||
successBlock:(DecryptSuccessBlock)successBlock
|
||||
failureBlock:(void (^)(NSError *_Nullable error))failureBlock
|
||||
{
|
||||
OWSAssertDebug(envelope);
|
||||
OWSAssertDebug(envelopeData);
|
||||
OWSAssertDebug(successBlock);
|
||||
OWSAssertDebug(failureBlock);
|
||||
|
||||
[self decryptEnvelope:envelope
|
||||
envelopeData:envelopeData
|
||||
cipherTypeName:@"Secure Message"
|
||||
cipherMessageBlock:^(NSData *encryptedData) {
|
||||
return [[WhisperMessage alloc] init_throws_withData:encryptedData];
|
||||
}
|
||||
successBlock:successBlock
|
||||
failureBlock:failureBlock];
|
||||
}
|
||||
|
||||
- (void)throws_decryptPreKeyBundle:(SSKProtoEnvelope *)envelope
|
||||
envelopeData:(NSData *)envelopeData
|
||||
successBlock:(DecryptSuccessBlock)successBlock
|
||||
failureBlock:(void (^)(NSError *_Nullable error))failureBlock
|
||||
{
|
||||
OWSAssertDebug(envelope);
|
||||
OWSAssertDebug(envelopeData);
|
||||
OWSAssertDebug(successBlock);
|
||||
OWSAssertDebug(failureBlock);
|
||||
|
||||
// Check whether we need to refresh our PreKeys every time we receive a PreKeyWhisperMessage.
|
||||
[TSPreKeyManager checkPreKeys];
|
||||
|
||||
[self decryptEnvelope:envelope
|
||||
envelopeData:envelopeData
|
||||
cipherTypeName:@"PreKey Bundle"
|
||||
cipherMessageBlock:^(NSData *encryptedData) {
|
||||
return [[PreKeyWhisperMessage alloc] init_throws_withData:encryptedData];
|
||||
}
|
||||
successBlock:successBlock
|
||||
failureBlock:failureBlock];
|
||||
}
|
||||
|
||||
- (void)decryptEnvelope:(SSKProtoEnvelope *)envelope
|
||||
envelopeData:(NSData *)envelopeData
|
||||
cipherTypeName:(NSString *)cipherTypeName
|
||||
cipherMessageBlock:(id<CipherMessage> (^_Nonnull)(NSData *))cipherMessageBlock
|
||||
successBlock:(DecryptSuccessBlock)successBlock
|
||||
failureBlock:(void (^)(NSError *_Nullable error))failureBlock
|
||||
{
|
||||
OWSAssertDebug(envelope);
|
||||
OWSAssertDebug(envelopeData);
|
||||
OWSAssertDebug(cipherTypeName.length > 0);
|
||||
OWSAssertDebug(cipherMessageBlock);
|
||||
OWSAssertDebug(successBlock);
|
||||
OWSAssertDebug(failureBlock);
|
||||
|
||||
NSString *recipientId = envelope.source;
|
||||
int deviceId = envelope.sourceDevice;
|
||||
|
||||
// DEPRECATED - Remove `legacyMessage` after all clients have been upgraded.
|
||||
NSData *encryptedData = envelope.content ?: envelope.legacyMessage;
|
||||
if (!encryptedData) {
|
||||
OWSProdFail([OWSAnalyticsEvents messageManagerErrorMessageEnvelopeHasNoContent]);
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, @"Envelope has no content.");
|
||||
return failureBlock(error);
|
||||
}
|
||||
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
@try {
|
||||
id<CipherMessage> cipherMessage = cipherMessageBlock(encryptedData);
|
||||
SNSessionCipher *cipher = [[SNSessionCipher alloc]
|
||||
initWithSessionResetImplementation:self.sessionResetImplementation
|
||||
sessionStore:self.primaryStorage
|
||||
preKeyStore:self.primaryStorage
|
||||
signedPreKeyStore:self.primaryStorage
|
||||
identityKeyStore:self.identityManager
|
||||
recipientID:recipientId
|
||||
deviceID:deviceId];
|
||||
|
||||
// plaintextData may be nil for some envelope types.
|
||||
NSError *error = nil;
|
||||
NSData *_Nullable decryptedData = [cipher decrypt:cipherMessage protocolContext:transaction error:&error];
|
||||
// Throw if we got an error
|
||||
SCKRaiseIfExceptionWrapperError(error);
|
||||
NSData *_Nullable plaintextData = decryptedData != nil ? [decryptedData removePadding] : nil;
|
||||
|
||||
OWSMessageDecryptResult *result = [OWSMessageDecryptResult resultWithEnvelopeData:envelopeData
|
||||
plaintextData:plaintextData
|
||||
source:envelope.source
|
||||
sourceDevice:envelope.sourceDevice
|
||||
isUDMessage:NO];
|
||||
successBlock(result, transaction);
|
||||
} @catch (NSException *exception) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self processException:exception envelope:envelope];
|
||||
NSString *errorDescription = [NSString
|
||||
stringWithFormat:@"Exception while decrypting %@: %@.", cipherTypeName, exception.description];
|
||||
OWSLogError(@"%@", errorDescription);
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, errorDescription);
|
||||
failureBlock(error);
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)decryptUnidentifiedSender:(SSKProtoEnvelope *)envelope
|
||||
successBlock:(DecryptSuccessBlock)successBlock
|
||||
failureBlock:(void (^)(NSError *_Nullable error))failureBlock
|
||||
{
|
||||
OWSAssertDebug(envelope);
|
||||
OWSAssertDebug(successBlock);
|
||||
OWSAssertDebug(failureBlock);
|
||||
|
||||
// NOTE: We don't need to bother with `legacyMessage` for UD messages.
|
||||
NSData *encryptedData = envelope.content;
|
||||
if (!encryptedData) {
|
||||
NSString *errorDescription = @"UD Envelope is missing content.";
|
||||
OWSFailDebug(@"%@", errorDescription);
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptUDMessage, errorDescription);
|
||||
return failureBlock(error);
|
||||
}
|
||||
|
||||
UInt64 serverTimestamp = envelope.timestamp;
|
||||
|
||||
id<SMKCertificateValidator> certificateValidator =
|
||||
[[SMKCertificateDefaultValidator alloc] initWithTrustRoot:self.udManager.trustRoot];
|
||||
|
||||
NSString *localRecipientId = self.tsAccountManager.localNumber;
|
||||
uint32_t localDeviceId = OWSDevicePrimaryDeviceId;
|
||||
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSError *cipherError;
|
||||
SMKSecretSessionCipher *_Nullable cipher =
|
||||
[[SMKSecretSessionCipher alloc] initWithSessionResetImplementation:self.sessionResetImplementation
|
||||
sessionStore:self.primaryStorage
|
||||
preKeyStore:self.primaryStorage
|
||||
signedPreKeyStore:self.primaryStorage
|
||||
identityStore:self.identityManager
|
||||
error:&cipherError];
|
||||
|
||||
if (cipherError || !cipher) {
|
||||
OWSFailDebug(@"Could not create secret session cipher: %@.", cipherError);
|
||||
cipherError = EnsureDecryptError(cipherError, @"Could not create secret session cipher.");
|
||||
return failureBlock(cipherError);
|
||||
}
|
||||
|
||||
NSError *decryptError;
|
||||
SMKDecryptResult *_Nullable decryptResult =
|
||||
[cipher throwswrapped_decryptMessageWithCertificateValidator:certificateValidator
|
||||
cipherTextData:encryptedData
|
||||
timestamp:serverTimestamp
|
||||
localRecipientId:localRecipientId
|
||||
localDeviceId:localDeviceId
|
||||
protocolContext:transaction
|
||||
error:&decryptError];
|
||||
|
||||
if (!decryptResult) {
|
||||
if (!decryptError) {
|
||||
OWSFailDebug(@"Caller should provide specific error.");
|
||||
NSError *error = OWSErrorWithCodeDescription(
|
||||
OWSErrorCodeFailedToDecryptUDMessage, @"Could not decrypt UD message.");
|
||||
return failureBlock(error);
|
||||
}
|
||||
|
||||
// Decrypt Failure Part 1: Unwrap failure details
|
||||
|
||||
NSError *_Nullable underlyingError;
|
||||
SSKProtoEnvelope *_Nullable identifiedEnvelope;
|
||||
|
||||
if (![decryptError.domain isEqualToString:@"SessionMetadataKit.SecretSessionKnownSenderError"]) {
|
||||
underlyingError = decryptError;
|
||||
identifiedEnvelope = envelope;
|
||||
} else {
|
||||
underlyingError = decryptError.userInfo[NSUnderlyingErrorKey];
|
||||
|
||||
NSString *senderRecipientId
|
||||
= decryptError.userInfo[SecretSessionKnownSenderError.kSenderRecipientIdKey];
|
||||
OWSAssert(senderRecipientId);
|
||||
|
||||
NSNumber *senderDeviceId = decryptError.userInfo[SecretSessionKnownSenderError.kSenderDeviceIdKey];
|
||||
OWSAssert(senderDeviceId);
|
||||
|
||||
SSKProtoEnvelopeBuilder *identifiedEnvelopeBuilder = envelope.asBuilder;
|
||||
identifiedEnvelopeBuilder.source = senderRecipientId;
|
||||
identifiedEnvelopeBuilder.sourceDevice = senderDeviceId.unsignedIntValue;
|
||||
NSError *identifiedEnvelopeBuilderError;
|
||||
|
||||
identifiedEnvelope = [identifiedEnvelopeBuilder buildAndReturnError:&identifiedEnvelopeBuilderError];
|
||||
if (identifiedEnvelopeBuilderError) {
|
||||
OWSFailDebug(@"identifiedEnvelopeBuilderError: %@", identifiedEnvelopeBuilderError);
|
||||
}
|
||||
}
|
||||
OWSAssert(underlyingError);
|
||||
OWSAssert(identifiedEnvelope);
|
||||
|
||||
NSException *_Nullable underlyingException;
|
||||
if ([underlyingError.domain isEqualToString:SCKExceptionWrapperErrorDomain]
|
||||
&& underlyingError.code == SCKExceptionWrapperErrorThrown) {
|
||||
|
||||
underlyingException = underlyingError.userInfo[SCKExceptionWrapperUnderlyingExceptionKey];
|
||||
OWSAssert(underlyingException);
|
||||
}
|
||||
|
||||
// Decrypt Failure Part 2: Handle unwrapped failure details
|
||||
|
||||
if (underlyingException) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self processException:underlyingException envelope:identifiedEnvelope];
|
||||
NSString *errorDescription = [NSString
|
||||
stringWithFormat:@"Exception while decrypting UD message: %@.", underlyingException.description];
|
||||
OWSLogError(@"%@", errorDescription);
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, errorDescription);
|
||||
failureBlock(error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ([underlyingError.domain isEqualToString:@"SessionMetadataKit.SMKSecretSessionCipherError"]
|
||||
&& underlyingError.code == SMKSecretSessionCipherErrorSelfSentMessage) {
|
||||
// Self-sent messages can be safely discarded.
|
||||
failureBlock(underlyingError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to recover automatically
|
||||
if ([decryptError userInfo][NSUnderlyingErrorKey] != nil) {
|
||||
NSDictionary *underlyingErrorUserInfo = [[decryptError userInfo][NSUnderlyingErrorKey] userInfo];
|
||||
if (underlyingErrorUserInfo[SCKExceptionWrapperUnderlyingExceptionKey] != nil) {
|
||||
NSException *underlyingUnderlyingError = underlyingErrorUserInfo[SCKExceptionWrapperUnderlyingExceptionKey];
|
||||
if ([[underlyingUnderlyingError reason] hasPrefix:@"Bad Mac!"]) {
|
||||
if ([underlyingError userInfo][@"kSenderRecipientIdKey"] != nil) {
|
||||
NSString *senderPublicKey = [underlyingError userInfo][@"kSenderRecipientIdKey"];
|
||||
TSContactThread *thread = [TSContactThread getThreadWithContactId:senderPublicKey transaction:transaction];
|
||||
if (thread != nil) {
|
||||
[thread addSessionRestoreDevice:senderPublicKey transaction:transaction];
|
||||
[LKSessionManagementProtocol startSessionResetInThread:thread transaction:transaction];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failureBlock(underlyingError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (decryptResult.messageType == SMKMessageTypePrekey) {
|
||||
[TSPreKeyManager checkPreKeys];
|
||||
}
|
||||
|
||||
NSString *source = decryptResult.senderRecipientId;
|
||||
if (source.length < 1) {
|
||||
NSString *errorDescription = @"Invalid UD sender.";
|
||||
OWSFailDebug(@"%@", errorDescription);
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptUDMessage, errorDescription);
|
||||
return failureBlock(error);
|
||||
}
|
||||
|
||||
long sourceDeviceId = decryptResult.senderDeviceId;
|
||||
if (sourceDeviceId < 1 || sourceDeviceId > UINT32_MAX) {
|
||||
NSString *errorDescription = @"Invalid UD sender device ID.";
|
||||
OWSFailDebug(@"%@", errorDescription);
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptUDMessage, errorDescription);
|
||||
return failureBlock(error);
|
||||
}
|
||||
NSData *plaintextData = [decryptResult.paddedPayload removePadding];
|
||||
|
||||
SSKProtoEnvelopeBuilder *envelopeBuilder = [envelope asBuilder];
|
||||
[envelopeBuilder setSource:source];
|
||||
[envelopeBuilder setSourceDevice:(uint32_t)sourceDeviceId];
|
||||
if (decryptResult.messageType == SMKMessageTypeFallback) {
|
||||
[envelopeBuilder setType:SSKProtoEnvelopeTypeFallbackMessage];
|
||||
OWSLogInfo(@"SMKMessageTypeFallback");
|
||||
}
|
||||
NSError *envelopeBuilderError;
|
||||
NSData *_Nullable newEnvelopeData = [envelopeBuilder buildSerializedDataAndReturnError:&envelopeBuilderError];
|
||||
if (envelopeBuilderError || !newEnvelopeData) {
|
||||
OWSFailDebug(@"Could not update UD envelope data: %@", envelopeBuilderError);
|
||||
NSError *error = EnsureDecryptError(envelopeBuilderError, @"Could not update UD envelope data");
|
||||
return failureBlock(error);
|
||||
}
|
||||
|
||||
OWSMessageDecryptResult *result = [OWSMessageDecryptResult resultWithEnvelopeData:newEnvelopeData
|
||||
plaintextData:plaintextData
|
||||
source:source
|
||||
sourceDevice:(uint32_t)sourceDeviceId
|
||||
isUDMessage:YES];
|
||||
successBlock(result, transaction);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)processException:(NSException *)exception envelope:(SSKProtoEnvelope *)envelope
|
||||
{
|
||||
OWSLogError(
|
||||
@"Got exception: %@ of type: %@ with reason: %@", exception.description, exception.name, exception.reason);
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
TSErrorMessage *errorMessage;
|
||||
|
||||
if (envelope.source.length == 0) {
|
||||
TSErrorMessage *errorMessage = [TSErrorMessage corruptedMessageInUnknownThread];
|
||||
[SSKEnvironment.shared.notificationsManager notifyUserForThreadlessErrorMessage:errorMessage
|
||||
transaction:transaction];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([exception.name isEqualToString:NoSessionException]) {
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorNoSession], envelope);
|
||||
errorMessage = [TSErrorMessage missingSessionWithEnvelope:envelope withTransaction:transaction];
|
||||
} else if ([exception.name isEqualToString:InvalidKeyException]) {
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorInvalidKey], envelope);
|
||||
errorMessage = [TSErrorMessage invalidKeyExceptionWithEnvelope:envelope withTransaction:transaction];
|
||||
} else if ([exception.name isEqualToString:InvalidKeyIdException]) {
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorInvalidKeyId], envelope);
|
||||
errorMessage = [TSErrorMessage invalidKeyExceptionWithEnvelope:envelope withTransaction:transaction];
|
||||
} else if ([exception.name isEqualToString:DuplicateMessageException]) {
|
||||
// Duplicate messages are silently discarded.
|
||||
return;
|
||||
} else if ([exception.name isEqualToString:InvalidVersionException]) {
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorInvalidMessageVersion], envelope);
|
||||
errorMessage = [TSErrorMessage invalidVersionWithEnvelope:envelope withTransaction:transaction];
|
||||
} else if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
|
||||
// Should no longer get here, since we now record the new identity for incoming messages.
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorUntrustedIdentityKeyException], envelope);
|
||||
OWSFailDebug(@"Failed to trust identity on incoming message from: %@", envelopeAddress(envelope));
|
||||
return;
|
||||
} else {
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorCorruptMessage], envelope);
|
||||
errorMessage = [TSErrorMessage corruptedMessageWithEnvelope:envelope withTransaction:transaction];
|
||||
}
|
||||
|
||||
OWSAssertDebug(errorMessage);
|
||||
if (errorMessage != nil) {
|
||||
[LKSessionManagementProtocol handleDecryptionError:errorMessage forPublicKey:envelope.source transaction:transaction];
|
||||
if (![LKSessionMetaProtocol isErrorMessageFromBeforeRestoration:errorMessage]) {
|
||||
[errorMessage saveWithTransaction:transaction];
|
||||
[self notifyUserForErrorMessage:errorMessage envelope:envelope transaction:transaction];
|
||||
} else {
|
||||
// Show the thread if it exists before restoration
|
||||
NSString *masterPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source;
|
||||
TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:masterPublicKey transaction:transaction];
|
||||
contactThread.shouldThreadBeVisible = true;
|
||||
[contactThread saveWithTransaction:transaction];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)notifyUserForErrorMessage:(TSErrorMessage *)errorMessage
|
||||
envelope:(SSKProtoEnvelope *)envelope
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
NSString *masterPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source;
|
||||
TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:masterPublicKey transaction:transaction];
|
||||
[SSKEnvironment.shared.notificationsManager notifyUserForErrorMessage:errorMessage
|
||||
thread:contactThread
|
||||
transaction:transaction];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,24 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class SSKProtoContent;
|
||||
@class SSKProtoDataMessage;
|
||||
@class SSKProtoEnvelope;
|
||||
|
||||
NSString *envelopeAddress(SSKProtoEnvelope *envelope);
|
||||
|
||||
@interface OWSMessageHandler : NSObject
|
||||
|
||||
- (NSString *)descriptionForEnvelopeType:(SSKProtoEnvelope *)envelope;
|
||||
- (NSString *)descriptionForEnvelope:(SSKProtoEnvelope *)envelope;
|
||||
- (NSString *)descriptionForContent:(SSKProtoContent *)content;
|
||||
- (NSString *)descriptionForDataMessage:(SSKProtoDataMessage *)dataMessage;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,183 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageHandler.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// used in log formatting
|
||||
NSString *envelopeAddress(SSKProtoEnvelope *envelope)
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@.%d", envelope.source, (unsigned int)envelope.sourceDevice];
|
||||
}
|
||||
|
||||
@implementation OWSMessageHandler
|
||||
|
||||
- (NSString *)descriptionForEnvelopeType:(SSKProtoEnvelope *)envelope
|
||||
{
|
||||
OWSAssertDebug(envelope != nil);
|
||||
|
||||
switch (envelope.type) {
|
||||
case SSKProtoEnvelopeTypeReceipt:
|
||||
return @"DeliveryReceipt";
|
||||
case SSKProtoEnvelopeTypeUnknown:
|
||||
// Shouldn't happen
|
||||
return @"Unknown";
|
||||
case SSKProtoEnvelopeTypeCiphertext:
|
||||
return @"SignalEncryptedMessage";
|
||||
case SSKProtoEnvelopeTypeKeyExchange:
|
||||
// Unsupported
|
||||
return @"KeyExchange";
|
||||
case SSKProtoEnvelopeTypePrekeyBundle:
|
||||
return @"PreKeyEncryptedMessage";
|
||||
case SSKProtoEnvelopeTypeUnidentifiedSender:
|
||||
return @"UnidentifiedSender";
|
||||
case SSKProtoEnvelopeTypeFallbackMessage:
|
||||
return @"FallbackMessage";
|
||||
case SSKProtoEnvelopeTypeClosedGroupCiphertext:
|
||||
return @"ClosedGroupCiphertext";
|
||||
default:
|
||||
// Shouldn't happen
|
||||
return @"Other";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)descriptionForEnvelope:(SSKProtoEnvelope *)envelope
|
||||
{
|
||||
OWSAssertDebug(envelope != nil);
|
||||
|
||||
return [NSString stringWithFormat:@"<Envelope type: %@, source: %@, timestamp: %llu content.length: %lu />",
|
||||
[self descriptionForEnvelopeType:envelope],
|
||||
envelopeAddress(envelope),
|
||||
envelope.timestamp,
|
||||
(unsigned long)envelope.content.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't want to just log `content.description` because we'd potentially log message bodies for dataMesssages and
|
||||
* sync transcripts
|
||||
*/
|
||||
- (NSString *)descriptionForContent:(SSKProtoContent *)content
|
||||
{
|
||||
if (content.syncMessage) {
|
||||
return [NSString stringWithFormat:@"<SyncMessage: %@ />", [self descriptionForSyncMessage:content.syncMessage]];
|
||||
} else if (content.dataMessage) {
|
||||
return [NSString stringWithFormat:@"<DataMessage: %@ />", [self descriptionForDataMessage:content.dataMessage]];
|
||||
} else if (content.callMessage) {
|
||||
NSString *callMessageDescription = [self descriptionForCallMessage:content.callMessage];
|
||||
return [NSString stringWithFormat:@"<CallMessage %@ />", callMessageDescription];
|
||||
} else if (content.nullMessage) {
|
||||
return [NSString stringWithFormat:@"<NullMessage: %@ />", content.nullMessage];
|
||||
} else if (content.receiptMessage) {
|
||||
return [NSString stringWithFormat:@"<ReceiptMessage: %@ />", content.receiptMessage];
|
||||
} else if (content.typingMessage) {
|
||||
return [NSString stringWithFormat:@"<TypingMessage: %@ />", content.typingMessage];
|
||||
} else {
|
||||
// Don't fire an analytics event; if we ever add a new content type, we'd generate a ton of
|
||||
// analytics traffic.
|
||||
return @"UnknownContent";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)descriptionForCallMessage:(SSKProtoCallMessage *)callMessage
|
||||
{
|
||||
NSString *messageType;
|
||||
UInt64 callId;
|
||||
|
||||
if (callMessage.offer) {
|
||||
messageType = @"Offer";
|
||||
callId = callMessage.offer.id;
|
||||
} else if (callMessage.busy) {
|
||||
messageType = @"Busy";
|
||||
callId = callMessage.busy.id;
|
||||
} else if (callMessage.answer) {
|
||||
messageType = @"Answer";
|
||||
callId = callMessage.answer.id;
|
||||
} else if (callMessage.hangup) {
|
||||
messageType = @"Hangup";
|
||||
callId = callMessage.hangup.id;
|
||||
} else if (callMessage.iceUpdate.count > 0) {
|
||||
messageType = [NSString stringWithFormat:@"Ice Updates (%lu)", (unsigned long)callMessage.iceUpdate.count];
|
||||
callId = callMessage.iceUpdate.firstObject.id;
|
||||
} else {
|
||||
OWSFailDebug(@"failure: unexpected call message type: %@", callMessage);
|
||||
messageType = @"Unknown";
|
||||
callId = 0;
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"type: %@, id: %llu", messageType, callId];
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't want to just log `dataMessage.description` because we'd potentially log message contents
|
||||
*/
|
||||
- (NSString *)descriptionForDataMessage:(SSKProtoDataMessage *)dataMessage
|
||||
{
|
||||
NSMutableString *description = [NSMutableString new];
|
||||
|
||||
if (dataMessage.group) {
|
||||
[description appendString:@"(Group:YES) "];
|
||||
}
|
||||
|
||||
if ((dataMessage.flags & SSKProtoDataMessageFlagsEndSession) != 0) {
|
||||
[description appendString:@"EndSession"];
|
||||
} else if ((dataMessage.flags & SSKProtoDataMessageFlagsExpirationTimerUpdate) != 0) {
|
||||
[description appendString:@"ExpirationTimerUpdate"];
|
||||
} else if ((dataMessage.flags & SSKProtoDataMessageFlagsProfileKeyUpdate) != 0) {
|
||||
[description appendString:@"ProfileKey"];
|
||||
} else if (dataMessage.attachments.count > 0) {
|
||||
[description appendString:@"MessageWithAttachment"];
|
||||
} else {
|
||||
[description appendString:@"Plain"];
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"<%@ />", description];
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't want to just log `syncMessage.description` because we'd potentially log message contents in sent transcripts
|
||||
*/
|
||||
- (NSString *)descriptionForSyncMessage:(SSKProtoSyncMessage *)syncMessage
|
||||
{
|
||||
NSMutableString *description = [NSMutableString new];
|
||||
if (syncMessage.sent) {
|
||||
[description appendString:@"SentTranscript"];
|
||||
} else if (syncMessage.request) {
|
||||
if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeContacts) {
|
||||
[description appendString:@"ContactRequest"];
|
||||
} else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeGroups) {
|
||||
[description appendString:@"GroupRequest"];
|
||||
} else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeBlocked) {
|
||||
[description appendString:@"BlockedRequest"];
|
||||
} else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeConfiguration) {
|
||||
[description appendString:@"ConfigurationRequest"];
|
||||
} else {
|
||||
OWSFailDebug(@"Unknown sync message request type");
|
||||
[description appendString:@"UnknownRequest"];
|
||||
}
|
||||
} else if (syncMessage.blocked) {
|
||||
[description appendString:@"Blocked"];
|
||||
} else if (syncMessage.read.count > 0) {
|
||||
[description appendString:@"ReadReceipt"];
|
||||
} else if (syncMessage.verified) {
|
||||
NSString *verifiedString =
|
||||
[NSString stringWithFormat:@"Verification for: %@", syncMessage.verified.destination];
|
||||
[description appendString:verifiedString];
|
||||
} else if (syncMessage.contacts) {
|
||||
[description appendString:@"Contacts"];
|
||||
} else if (syncMessage.groups) {
|
||||
[description appendString:@"ClosedGroups"];
|
||||
} else if (syncMessage.openGroups) {
|
||||
[description appendString:@"OpenGroups"];
|
||||
} else {
|
||||
[description appendString:@"Unknown"];
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,33 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSMessageHandler.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSPrimaryStorage;
|
||||
@class SSKProtoEnvelope;
|
||||
@class TSThread;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
|
||||
@interface OWSMessageManager : OWSMessageHandler
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
// processEnvelope: can be called from any thread.
|
||||
- (void)throws_processEnvelope:(SSKProtoEnvelope *)envelope
|
||||
plaintextData:(NSData *_Nullable)plaintextData
|
||||
wasReceivedByUD:(BOOL)wasReceivedByUD
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
serverID:(uint64_t)serverID;
|
||||
|
||||
// This should be invoked by the main app when the app is ready.
|
||||
- (void)startObserving;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSPrimaryStorage;
|
||||
@class OWSStorage;
|
||||
|
||||
// This class is used to write incoming (encrypted, unprocessed)
|
||||
// messages to a durable queue and then decrypt them in the order
|
||||
// in which they were received. Successfully decrypted messages
|
||||
// are forwarded to OWSBatchMessageProcessor.
|
||||
@interface OWSMessageReceiver : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
+ (NSString *)databaseExtensionName;
|
||||
+ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage;
|
||||
|
||||
- (void)handleReceivedEnvelopeData:(NSData *)envelopeData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,513 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageReceiver.h"
|
||||
#import "AppContext.h"
|
||||
#import "AppReadiness.h"
|
||||
#import "NSArray+OWS.h"
|
||||
#import "NotificationsProtocol.h"
|
||||
#import "OWSBackgroundTask.h"
|
||||
#import "OWSBatchMessageProcessor.h"
|
||||
#import "OWSMessageDecrypter.h"
|
||||
#import "OWSPrimaryStorage+Loki.h"
|
||||
#import "OWSQueues.h"
|
||||
#import "OWSStorage.h"
|
||||
#import "OWSIdentityManager.h"
|
||||
#import "SSKEnvironment.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import "TSDatabaseView.h"
|
||||
#import "TSErrorMessage.h"
|
||||
#import "TSYapDatabaseObject.h"
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <YapDatabase/YapDatabaseAutoView.h>
|
||||
#import <YapDatabase/YapDatabaseConnection.h>
|
||||
#import <YapDatabase/YapDatabaseTransaction.h>
|
||||
#import <YapDatabase/YapDatabaseViewTypes.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
|
||||
#import "SSKAsserts.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessageDecryptJob : TSYapDatabaseObject
|
||||
|
||||
@property (nonatomic, readonly) NSDate *createdAt;
|
||||
@property (nonatomic, readonly) NSData *envelopeData;
|
||||
@property (nonatomic, readonly, nullable) SSKProtoEnvelope *envelopeProto;
|
||||
|
||||
- (instancetype)initWithEnvelopeData:(NSData *)envelopeData NS_DESIGNATED_INITIALIZER;
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageDecryptJob
|
||||
|
||||
+ (NSString *)collection
|
||||
{
|
||||
return @"OWSMessageProcessingJob";
|
||||
}
|
||||
|
||||
- (instancetype)initWithEnvelopeData:(NSData *)envelopeData
|
||||
{
|
||||
OWSAssertDebug(envelopeData);
|
||||
|
||||
self = [super initWithUniqueId:[NSUUID new].UUIDString];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_envelopeData = envelopeData;
|
||||
_createdAt = [NSDate new];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
return [super initWithCoder:coder];
|
||||
}
|
||||
|
||||
- (nullable SSKProtoEnvelope *)envelopeProto
|
||||
{
|
||||
NSError *error;
|
||||
SSKProtoEnvelope *_Nullable envelope = [SSKProtoEnvelope parseData:self.envelopeData error:&error];
|
||||
if (error || envelope == nil) {
|
||||
OWSFailDebug(@"failed to parse envelope with error: %@", error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Finder
|
||||
|
||||
NSString *const OWSMessageDecryptJobFinderExtensionName = @"OWSMessageProcessingJobFinderExtensionName2";
|
||||
NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessingJobFinderExtensionGroup2";
|
||||
|
||||
@interface OWSMessageDecryptJobFinder : NSObject
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessageDecryptJobFinder ()
|
||||
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageDecryptJobFinder
|
||||
|
||||
- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection
|
||||
{
|
||||
OWSSingletonAssert();
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_dbConnection = dbConnection;
|
||||
|
||||
[OWSMessageDecryptJobFinder registerLegacyClasses];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (OWSMessageDecryptJob *_Nullable)nextJob
|
||||
{
|
||||
__block OWSMessageDecryptJob *_Nullable job = nil;
|
||||
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
||||
YapDatabaseViewTransaction *viewTransaction = [transaction ext:OWSMessageDecryptJobFinderExtensionName];
|
||||
OWSAssertDebug(viewTransaction != nil);
|
||||
job = [viewTransaction firstObjectInGroup:OWSMessageDecryptJobFinderExtensionGroup];
|
||||
}];
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
- (void)addJobForEnvelopeData:(NSData *)envelopeData
|
||||
{
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
OWSMessageDecryptJob *job = [[OWSMessageDecryptJob alloc] initWithEnvelopeData:envelopeData];
|
||||
[job saveWithTransaction:transaction];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)removeJobWithId:(NSString *)uniqueId
|
||||
{
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
[transaction removeObjectForKey:uniqueId inCollection:[OWSMessageDecryptJob collection]];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (YapDatabaseView *)databaseExtension
|
||||
{
|
||||
YapDatabaseViewSorting *sorting =
|
||||
[YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *transaction,
|
||||
NSString *group,
|
||||
NSString *collection1,
|
||||
NSString *key1,
|
||||
id object1,
|
||||
NSString *collection2,
|
||||
NSString *key2,
|
||||
id object2) {
|
||||
|
||||
if (![object1 isKindOfClass:[OWSMessageDecryptJob class]]) {
|
||||
OWSFailDebug(@"Unexpected object: %@ in collection: %@", [object1 class], collection1);
|
||||
return NSOrderedSame;
|
||||
}
|
||||
OWSMessageDecryptJob *job1 = (OWSMessageDecryptJob *)object1;
|
||||
|
||||
if (![object2 isKindOfClass:[OWSMessageDecryptJob class]]) {
|
||||
OWSFailDebug(@"Unexpected object: %@ in collection: %@", [object2 class], collection2);
|
||||
return NSOrderedSame;
|
||||
}
|
||||
OWSMessageDecryptJob *job2 = (OWSMessageDecryptJob *)object2;
|
||||
|
||||
return [job1.createdAt compare:job2.createdAt];
|
||||
}];
|
||||
|
||||
YapDatabaseViewGrouping *grouping =
|
||||
[YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable(YapDatabaseReadTransaction *_Nonnull transaction,
|
||||
NSString *_Nonnull collection,
|
||||
NSString *_Nonnull key,
|
||||
id _Nonnull object) {
|
||||
if (![object isKindOfClass:[OWSMessageDecryptJob class]]) {
|
||||
OWSFailDebug(@"Unexpected object: %@ in collection: %@", object, collection);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Arbitrary string - all in the same group. We're only using the view for sorting.
|
||||
return OWSMessageDecryptJobFinderExtensionGroup;
|
||||
}];
|
||||
|
||||
YapDatabaseViewOptions *options = [YapDatabaseViewOptions new];
|
||||
options.allowedCollections =
|
||||
[[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[OWSMessageDecryptJob collection]]];
|
||||
|
||||
return [[YapDatabaseAutoView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"1" options:options];
|
||||
}
|
||||
|
||||
+ (void)registerLegacyClasses
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
// We've renamed OWSMessageProcessingJob to OWSMessageDecryptJob.
|
||||
[NSKeyedUnarchiver setClass:[OWSMessageDecryptJob class] forClassName:[OWSMessageDecryptJob collection]];
|
||||
});
|
||||
}
|
||||
|
||||
+ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage
|
||||
{
|
||||
[self registerLegacyClasses];
|
||||
|
||||
YapDatabaseView *existingView = [storage registeredExtension:OWSMessageDecryptJobFinderExtensionName];
|
||||
if (existingView) {
|
||||
OWSFailDebug(@"%@ was already initialized.", OWSMessageDecryptJobFinderExtensionName);
|
||||
// already initialized
|
||||
return;
|
||||
}
|
||||
[storage asyncRegisterExtension:[self databaseExtension] withName:OWSMessageDecryptJobFinderExtensionName];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Queue Processing
|
||||
|
||||
@interface OWSMessageDecryptQueue : NSObject
|
||||
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
@property (nonatomic, readonly) OWSMessageDecryptJobFinder *finder;
|
||||
@property (nonatomic) BOOL isDrainingQueue;
|
||||
|
||||
- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection
|
||||
finder:(OWSMessageDecryptJobFinder *)finder NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageDecryptQueue
|
||||
|
||||
- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection finder:(OWSMessageDecryptJobFinder *)finder
|
||||
{
|
||||
OWSSingletonAssert();
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_dbConnection = dbConnection;
|
||||
_finder = finder;
|
||||
_isDrainingQueue = NO;
|
||||
|
||||
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
[self drainQueue];
|
||||
}
|
||||
}];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(registrationStateDidChange:)
|
||||
name:RegistrationStateDidChangeNotification
|
||||
object:nil];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Singletons
|
||||
|
||||
- (OWSMessageDecrypter *)messageDecrypter
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.messageDecrypter);
|
||||
|
||||
return SSKEnvironment.shared.messageDecrypter;
|
||||
}
|
||||
|
||||
- (OWSBatchMessageProcessor *)batchMessageProcessor
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.batchMessageProcessor);
|
||||
|
||||
return SSKEnvironment.shared.batchMessageProcessor;
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
||||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)registrationStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
[self drainQueue];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Instance methods
|
||||
|
||||
- (dispatch_queue_t)serialQueue
|
||||
{
|
||||
static dispatch_queue_t queue = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
queue = dispatch_queue_create("org.whispersystems.message.decrypt", DISPATCH_QUEUE_SERIAL);
|
||||
});
|
||||
return queue;
|
||||
}
|
||||
|
||||
- (void)enqueueEnvelopeData:(NSData *)envelopeData
|
||||
{
|
||||
[self.finder addJobForEnvelopeData:envelopeData];
|
||||
}
|
||||
|
||||
- (void)drainQueue
|
||||
{
|
||||
OWSAssertDebug(AppReadiness.isAppReady);
|
||||
|
||||
if (!CurrentAppContext().isMainApp) { return; }
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) { return; }
|
||||
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
if (self.isDrainingQueue) { return; }
|
||||
self.isDrainingQueue = YES;
|
||||
[self drainQueueWorkStep];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)drainQueueWorkStep
|
||||
{
|
||||
AssertOnDispatchQueue(self.serialQueue);
|
||||
|
||||
OWSMessageDecryptJob *_Nullable job = [self.finder nextJob];
|
||||
|
||||
if (!job) {
|
||||
self.isDrainingQueue = NO;
|
||||
OWSLogVerbose(@"Queue is drained.");
|
||||
return;
|
||||
}
|
||||
|
||||
__block OWSBackgroundTask *_Nullable backgroundTask =
|
||||
[OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
|
||||
|
||||
[self processJob:job
|
||||
completion:^(BOOL success) {
|
||||
[self.finder removeJobWithId:job.uniqueId];
|
||||
OWSLogVerbose(@"%@ job. %lu jobs left.",
|
||||
success ? @"decrypted" : @"failed to decrypt",
|
||||
(unsigned long)[OWSMessageDecryptJob numberOfKeysInCollection]);
|
||||
[self drainQueueWorkStep];
|
||||
OWSAssertDebug(backgroundTask);
|
||||
backgroundTask = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)wasReceivedByUD:(SSKProtoEnvelope *)envelope
|
||||
{
|
||||
return (envelope.type == SSKProtoEnvelopeTypeUnidentifiedSender && (!envelope.hasSource || envelope.source.length < 1));
|
||||
}
|
||||
|
||||
- (void)processJob:(OWSMessageDecryptJob *)job completion:(void (^)(BOOL))completion
|
||||
{
|
||||
AssertOnDispatchQueue(self.serialQueue);
|
||||
OWSAssertDebug(job);
|
||||
|
||||
SSKProtoEnvelope *_Nullable envelope = job.envelopeProto;
|
||||
|
||||
if (!envelope) {
|
||||
OWSFailDebug(@"Couldn't parse proto.");
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
TSErrorMessage *errorMessage = [TSErrorMessage corruptedMessageInUnknownThread];
|
||||
[SSKEnvironment.shared.notificationsManager notifyUserForThreadlessErrorMessage:errorMessage
|
||||
transaction:transaction];
|
||||
}];
|
||||
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
completion(NO);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We use the original envelope for this check;
|
||||
// the decryption process might rewrite the envelope.
|
||||
BOOL wasReceivedByUD = [self wasReceivedByUD:envelope];
|
||||
|
||||
[self.messageDecrypter decryptEnvelope:envelope
|
||||
envelopeData:job.envelopeData
|
||||
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
|
||||
OWSAssertDebug(transaction);
|
||||
|
||||
if ([LKSessionMetaProtocol shouldSkipMessageDecryptResult:result wrappedIn:envelope]) {
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
completion(YES);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// We persist the decrypted envelope data in the same transaction within which
|
||||
// it was decrypted to prevent data loss. If the new job isn't persisted,
|
||||
// the session state side effects of its decryption are also rolled back.
|
||||
//
|
||||
// NOTE: We use envelopeData from the decrypt result, not job.envelopeData,
|
||||
// since the envelope may be altered by the decryption process in the UD case.
|
||||
[self.batchMessageProcessor enqueueEnvelopeData:result.envelopeData
|
||||
plaintextData:result.plaintextData
|
||||
wasReceivedByUD:wasReceivedByUD
|
||||
transaction:transaction];
|
||||
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
completion(YES);
|
||||
});
|
||||
}
|
||||
failureBlock:^{
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
completion(NO);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - OWSMessageReceiver
|
||||
|
||||
@interface OWSMessageReceiver ()
|
||||
|
||||
@property (nonatomic, readonly) OWSMessageDecryptQueue *processingQueue;
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageReceiver
|
||||
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
OWSSingletonAssert();
|
||||
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// For coherency we use the same dbConnection to persist and read the unprocessed envelopes
|
||||
YapDatabaseConnection *dbConnection = [primaryStorage newDatabaseConnection];
|
||||
OWSMessageDecryptJobFinder *finder = [[OWSMessageDecryptJobFinder alloc] initWithDBConnection:dbConnection];
|
||||
OWSMessageDecryptQueue *processingQueue = [[OWSMessageDecryptQueue alloc] initWithDBConnection:dbConnection finder:finder];
|
||||
|
||||
_processingQueue = processingQueue;
|
||||
|
||||
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
[self.processingQueue drainQueue];
|
||||
}
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - class methods
|
||||
|
||||
+ (NSString *)databaseExtensionName
|
||||
{
|
||||
return OWSMessageDecryptJobFinderExtensionName;
|
||||
}
|
||||
|
||||
+ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage
|
||||
{
|
||||
[OWSMessageDecryptJobFinder asyncRegisterDatabaseExtension:storage];
|
||||
}
|
||||
|
||||
#pragma mark - instance methods
|
||||
|
||||
- (void)handleReceivedEnvelopeData:(NSData *)envelopeData
|
||||
{
|
||||
if (envelopeData.length < 1) {
|
||||
OWSFailDebug(@"Received an empty envelope.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop any too-large messages on the floor. Well behaving clients should never send them.
|
||||
NSUInteger kMaxEnvelopeByteCount = 250 * 1024;
|
||||
if (envelopeData.length > kMaxEnvelopeByteCount) {
|
||||
OWSFailDebug(@"Received an oversized message.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Take note of any messages larger than we expect, but still process them.
|
||||
// This likely indicates a misbehaving sending client.
|
||||
NSUInteger kLargeEnvelopeWarningByteCount = 25 * 1024;
|
||||
if (envelopeData.length > kLargeEnvelopeWarningByteCount) {
|
||||
OWSFailDebug(@"Received an unexpectedly large message.");
|
||||
}
|
||||
|
||||
[self.processingQueue enqueueEnvelopeData:envelopeData];
|
||||
[self.processingQueue drainQueue];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,96 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// Corresponds to a single effort to send a message to a given recipient,
|
||||
// which may span multiple attempts. Note that group messages may be sent
|
||||
// to multiple recipients and therefore require multiple instances of
|
||||
// OWSMessageSend.
|
||||
@objc
|
||||
public class OWSMessageSend: NSObject {
|
||||
@objc
|
||||
public let message: TSOutgoingMessage
|
||||
|
||||
// thread may be nil if message is an OWSOutgoingSyncMessage.
|
||||
@objc
|
||||
public let thread: TSThread?
|
||||
|
||||
@objc
|
||||
public let recipient: SignalRecipient
|
||||
|
||||
private static let kMaxRetriesPerRecipient: Int = 1 // Loki: We have our own retrying
|
||||
|
||||
@objc
|
||||
public var remainingAttempts = OWSMessageSend.kMaxRetriesPerRecipient
|
||||
|
||||
// We "fail over" to REST sends after _any_ error sending
|
||||
// via the web socket.
|
||||
@objc
|
||||
public var hasWebsocketSendFailed = false
|
||||
|
||||
@objc
|
||||
public var udAccess: OWSUDAccess?
|
||||
|
||||
@objc
|
||||
public var senderCertificate: SMKSenderCertificate?
|
||||
|
||||
@objc
|
||||
public let localNumber: String
|
||||
|
||||
@objc
|
||||
public let isLocalNumber: Bool
|
||||
|
||||
@objc
|
||||
public let success: () -> Void
|
||||
|
||||
@objc
|
||||
public let failure: (Error) -> Void
|
||||
|
||||
@objc
|
||||
public init(message: TSOutgoingMessage,
|
||||
thread: TSThread?,
|
||||
recipient: SignalRecipient,
|
||||
senderCertificate: SMKSenderCertificate?,
|
||||
udAccess: OWSUDAccess?,
|
||||
localNumber: String,
|
||||
success: @escaping () -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
self.message = message
|
||||
self.thread = thread
|
||||
self.recipient = recipient
|
||||
self.localNumber = localNumber
|
||||
self.senderCertificate = senderCertificate
|
||||
self.udAccess = udAccess
|
||||
|
||||
if let recipientId = recipient.uniqueId {
|
||||
self.isLocalNumber = localNumber == recipientId
|
||||
} else {
|
||||
owsFailDebug("SignalRecipient missing recipientId")
|
||||
self.isLocalNumber = false
|
||||
}
|
||||
|
||||
self.success = success
|
||||
self.failure = failure
|
||||
}
|
||||
|
||||
@objc
|
||||
public var isUDSend: Bool {
|
||||
return udAccess != nil && senderCertificate != nil
|
||||
}
|
||||
|
||||
@objc
|
||||
public func disableUD() {
|
||||
Logger.verbose("\(recipient.recipientId)")
|
||||
udAccess = nil
|
||||
}
|
||||
|
||||
@objc
|
||||
public func setHasUDAuthFailed() {
|
||||
Logger.verbose("\(recipient.recipientId)")
|
||||
// We "fail over" to non-UD sends after auth errors sending via UD.
|
||||
disableUD()
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/DataSource.h>
|
||||
#import <SignalUtilitiesKit/TSContactThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern const NSUInteger kOversizeTextMessageSizeThreshold;
|
||||
|
||||
@class OWSBlockingManager;
|
||||
@class OWSPrimaryStorage;
|
||||
@class TSAttachmentStream;
|
||||
@class TSInvalidIdentityKeySendingErrorMessage;
|
||||
@class TSNetworkManager;
|
||||
@class TSOutgoingMessage;
|
||||
@class TSThread;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
@class OWSMessageSend;
|
||||
|
||||
@protocol ContactsManagerProtocol;
|
||||
|
||||
/**
|
||||
* Useful for when you *sometimes* want to retry before giving up and calling the failure handler
|
||||
* but *sometimes* we don't want to retry when we know it's a terminal failure, so we allow the
|
||||
* caller to indicate this with isRetryable=NO.
|
||||
*/
|
||||
typedef void (^RetryableFailureHandler)(NSError *_Nonnull error);
|
||||
|
||||
// Message send error handling is slightly different for contact and group messages.
|
||||
//
|
||||
// For example, If one member of a group deletes their account, the group should
|
||||
// ignore errors when trying to send messages to this ex-member.
|
||||
|
||||
#pragma mark -
|
||||
|
||||
NS_SWIFT_NAME(OutgoingAttachmentInfo)
|
||||
@interface OWSOutgoingAttachmentInfo : NSObject
|
||||
|
||||
@property (nonatomic, readonly) DataSource *dataSource;
|
||||
@property (nonatomic, readonly) NSString *contentType;
|
||||
@property (nonatomic, readonly, nullable) NSString *sourceFilename;
|
||||
@property (nonatomic, readonly, nullable) NSString *caption;
|
||||
@property (nonatomic, readonly, nullable) NSString *albumMessageId;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithDataSource:(DataSource *)dataSource
|
||||
contentType:(NSString *)contentType
|
||||
sourceFilename:(nullable NSString *)sourceFilename
|
||||
caption:(nullable NSString *)caption
|
||||
albumMessageId:(nullable NSString *)albumMessageId NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
NS_SWIFT_NAME(MessageSender)
|
||||
@interface OWSMessageSender : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
/**
|
||||
* Send and resend text messages or resend messages with existing attachments.
|
||||
* If you haven't yet created the attachment, see the `sendAttachment:` variants.
|
||||
*/
|
||||
- (void)sendMessage:(TSOutgoingMessage *)message
|
||||
success:(void (^)(void))successHandler
|
||||
failure:(void (^)(NSError *error))failureHandler;
|
||||
|
||||
/**
|
||||
* Takes care of allocating and uploading the attachment, then sends the message.
|
||||
* Only necessary to call once. If sending fails, retry with `sendMessage:`.
|
||||
*/
|
||||
- (void)sendAttachment:(DataSource *)dataSource
|
||||
contentType:(NSString *)contentType
|
||||
sourceFilename:(nullable NSString *)sourceFilename
|
||||
albumMessageId:(nullable NSString *)albumMessageId
|
||||
inMessage:(TSOutgoingMessage *)outgoingMessage
|
||||
success:(void (^)(void))successHandler
|
||||
failure:(void (^)(NSError *error))failureHandler;
|
||||
|
||||
- (void)sendAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
|
||||
inMessage:(TSOutgoingMessage *)message
|
||||
success:(void (^)(void))successHandler
|
||||
failure:(void (^)(NSError *error))failureHandler;
|
||||
|
||||
/**
|
||||
* Same as `sendAttachment:`, but deletes the local copy of the attachment after sending.
|
||||
* Used for sending sync request data, not for user visible attachments.
|
||||
*/
|
||||
- (void)sendTemporaryAttachment:(DataSource *)dataSource
|
||||
contentType:(NSString *)contentType
|
||||
inMessage:(TSOutgoingMessage *)outgoingMessage
|
||||
success:(void (^)(void))successHandler
|
||||
failure:(void (^)(NSError *error))failureHandler;
|
||||
|
||||
- (void)sendMessage:(OWSMessageSend *)messageSend;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OutgoingMessagePreparer : NSObject
|
||||
|
||||
/// Persists all necessary data to disk before sending, e.g. generate thumbnails
|
||||
+ (NSArray<NSString *> *)prepareMessageForSending:(TSOutgoingMessage *)message
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
|
||||
/// Writes attachment to disk and applies original filename to message attributes
|
||||
+ (void)prepareAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
|
||||
inMessage:(TSOutgoingMessage *)outgoingMessage
|
||||
completionHandler:(void (^)(NSError *_Nullable error))completionHandler
|
||||
NS_SWIFT_NAME(prepareAttachments(_:inMessage:completionHandler:));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
|
||||
public extension Message.Destination {
|
||||
|
||||
static func from(_ thread: TSThread) -> Message.Destination {
|
||||
if let thread = thread as? TSContactThread {
|
||||
return .contact(publicKey: thread.uniqueId!)
|
||||
} else if let thread = thread as? TSGroupThread, thread.usesSharedSenderKeys {
|
||||
let groupID = thread.groupModel.groupId
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
return .closedGroup(groupPublicKey: groupPublicKey)
|
||||
} else if let thread = thread as? TSGroupThread, thread.isPublicChat {
|
||||
var openGroup: OpenGroup!
|
||||
Storage.read { transaction in
|
||||
openGroup = LokiDatabaseUtilities.getPublicChat(for: thread.uniqueId!, in: transaction)
|
||||
}
|
||||
return .openGroup(channel: openGroup.channel, server: openGroup.server)
|
||||
} else {
|
||||
preconditionFailure("TODO: Handle legacy closed groups.")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import PromiseKit
|
||||
|
||||
public extension MessageSender {
|
||||
|
||||
static func send(_ message: Message, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
message.threadID = thread.uniqueId!
|
||||
let destination = Message.Destination.from(thread)
|
||||
let job = MessageSendJob(message: message, destination: destination)
|
||||
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
|
||||
}
|
||||
|
||||
static func sendNonDurably(_ message: Message, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
|
||||
message.threadID = thread.uniqueId!
|
||||
let destination = Message.Destination.from(thread)
|
||||
return MessageSender.send(message, to: destination, using: transaction)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue