diff --git a/Podfile b/Podfile index 67e3e8a37..43126a2da 100644 --- a/Podfile +++ b/Podfile @@ -98,6 +98,26 @@ target 'SignalMessaging' do shared_pods end +target 'SignalUtilitiesKit' do + pod 'AFNetworking', inhibit_warnings: true + pod 'CryptoSwift', :inhibit_warnings => true + pod 'Curve25519Kit', :inhibit_warnings => true + pod 'GRKOpenSSLFramework', :inhibit_warnings => true + pod 'HKDFKit', :inhibit_warnings => true + pod 'libPhoneNumber-iOS', :inhibit_warnings => true + pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true + pod 'PromiseKit', :inhibit_warnings => true + pod 'Reachability', :inhibit_warnings => true + pod 'SAMKeychain', :inhibit_warnings => true + pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release', :inhibit_warnings => true + pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true + pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true +end + +target 'SessionUIKit' do + +end + target 'SessionMessagingKit' do pod 'AFNetworking', inhibit_warnings: true pod 'CryptoSwift', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index 096a79587..dc9ac5f8a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -206,12 +206,14 @@ DEPENDENCIES: - FeedKit (~> 8.1) - GRKOpenSSLFramework - HKDFKit + - libPhoneNumber-iOS - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) - NVActivityIndicatorView (~> 4.7) - PromiseKit - PromiseKit (= 6.5.3) - PureLayout (~> 3.1.4) - Reachability + - SAMKeychain - SessionAxolotlKit (from `https://github.com/loki-project/session-ios-protocol-kit.git`, branch `master`) - SessionAxolotlKit/Tests (from `https://github.com/loki-project/session-ios-protocol-kit.git`, branch `master`) - SessionCoreKit (from `https://github.com/loki-project/session-ios-core-kit.git`) @@ -333,6 +335,6 @@ SPEC CHECKSUMS: YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 8fc5917e97576b902a46b328af80664381ede889 +PODFILE CHECKSUM: d78dc9a752cd3ce8f01fa327b8518dff3f5236d5 COCOAPODS: 1.10.0.rc.1 diff --git a/Pods b/Pods index 0c79ca436..e28da414f 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 0c79ca436b633fdf1b0daf90e86fd323dcc60c55 +Subproject commit e28da414f77b9cba508c92e90b16b815847cde7e diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 020a21d3a..649cc8654 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -13,7 +13,7 @@ public struct Configuration { internal static var shared: Configuration! } -public enum SessionMessagingKit { // Just to make the external API nice +public enum SessionMessagingKitX { // Just to make the external API nice public static func configure( storage: SessionMessagingKitStorageProtocol, diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index abd9799d8..93f7328c6 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -2,15 +2,15 @@ import PromiseKit import SessionSnodeKit import SessionUtilitiesKit -internal enum MessageSender { +public enum MessageSender { - internal enum Error : LocalizedError { + public enum Error : LocalizedError { case invalidMessage case protoConversionFailed case proofOfWorkCalculationFailed case noUserPublicKey - internal var errorDescription: String? { + public var errorDescription: String? { switch self { case .invalidMessage: return "Invalid message." case .protoConversionFailed: return "Couldn't convert message to proto." diff --git a/SessionProtocolKit/Meta/SessionProtocolKit.h b/SessionProtocolKit/Meta/SessionProtocolKit.h index 82c84e815..99f92f97c 100644 --- a/SessionProtocolKit/Meta/SessionProtocolKit.h +++ b/SessionProtocolKit/Meta/SessionProtocolKit.h @@ -4,14 +4,20 @@ FOUNDATION_EXPORT double SessionProtocolKitVersionNumber; FOUNDATION_EXPORT const unsigned char SessionProtocolKitVersionString[]; #import +#import #import #import #import +#import #import #import +#import #import #import +#import #import #import #import +#import #import +#import diff --git a/SessionProtocolKit/Shared Sender Keys/ClosedGroupSenderKey.swift b/SessionProtocolKit/Shared Sender Keys/ClosedGroupSenderKey.swift index 05e81f3f0..da16b34ec 100644 --- a/SessionProtocolKit/Shared Sender Keys/ClosedGroupSenderKey.swift +++ b/SessionProtocolKit/Shared Sender Keys/ClosedGroupSenderKey.swift @@ -5,7 +5,7 @@ public final class ClosedGroupSenderKey : NSObject, NSCoding { // NSObject/NSCod public let publicKey: Data // MARK: Initialization - init(chainKey: Data, keyIndex: UInt, publicKey: Data) { + public init(chainKey: Data, keyIndex: UInt, publicKey: Data) { self.chainKey = chainKey self.keyIndex = keyIndex self.publicKey = publicKey diff --git a/SessionProtocolKit/Signal/Utility/Cryptography.m b/SessionProtocolKit/Signal/Utility/Cryptography.m index b6a84350e..5d9a5c402 100644 --- a/SessionProtocolKit/Signal/Utility/Cryptography.m +++ b/SessionProtocolKit/Signal/Utility/Cryptography.m @@ -7,7 +7,7 @@ #import "NSData+OWS.h" #import #import -#import +#import #import #import diff --git a/SessionProtocolKit/Signal/Utility/Randomness.h b/SessionProtocolKit/Signal/Utility/Randomness.h deleted file mode 100644 index f74b525ee..000000000 --- a/SessionProtocolKit/Signal/Utility/Randomness.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import - -@interface Randomness : NSObject - -/** - * Generates a given number of cryptographically secure bytes using SecRandomCopyBytes. - * - * @param numberBytes The number of bytes to be generated. - * - * @return Random Bytes. - */ - -+ (NSData *)generateRandomBytes:(int)numberBytes; - - -@end diff --git a/SessionProtocolKit/Signal/Utility/Randomness.m b/SessionProtocolKit/Signal/Utility/Randomness.m deleted file mode 100644 index 27fbdf562..000000000 --- a/SessionProtocolKit/Signal/Utility/Randomness.m +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "Randomness.h" -#import - -@implementation Randomness - -+ (NSData *)generateRandomBytes:(int)numberBytes -{ - NSMutableData *_Nullable randomBytes = [NSMutableData dataWithLength:numberBytes]; - if (!randomBytes) { - OWSFail(@"Could not allocate buffer for random bytes."); - } - int err = 0; - err = SecRandomCopyBytes(kSecRandomDefault, numberBytes, [randomBytes mutableBytes]); - if (err != noErr || randomBytes.length != numberBytes) { - OWSFail(@"Could not generate random bytes."); - } - return [randomBytes copy]; -} - -@end diff --git a/SessionSnodeKit/Snode.swift b/SessionSnodeKit/Snode.swift index 6392787a3..9b185edbb 100644 --- a/SessionSnodeKit/Snode.swift +++ b/SessionSnodeKit/Snode.swift @@ -3,7 +3,7 @@ import Foundation public final class Snode : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility public let address: String public let port: UInt16 - internal let publicKeySet: KeySet + public let publicKeySet: KeySet public var ip: String { address.removingPrefix("https://") @@ -16,9 +16,9 @@ public final class Snode : NSObject, NSCoding { // NSObject/NSCoding conformance case sendMessage = "store" } - internal struct KeySet { - let ed25519Key: String - let x25519Key: String + public struct KeySet { + public let ed25519Key: String + public let x25519Key: String } // MARK: Initialization diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 9b59c153c..58aa04fdd 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -1,15 +1,18 @@ import PromiseKit import SessionUtilitiesKit -public enum SnodeAPI { +@objc(SNSnodeAPI) +public final class SnodeAPI : NSObject { /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. internal static var snodeFailureCount: [Snode:UInt] = [:] /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. internal static var snodePool: Set = [] + /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - internal static var swarmCache: [String:Set] = [:] - + public static var swarmCache: [String:Set] = [:] + public static var workQueue: DispatchQueue { Threading.workQueue } + // MARK: Settings private static let maxRetryCount: UInt = 4 private static let minimumSnodePoolCount = 64 @@ -25,11 +28,13 @@ public enum SnodeAPI { // MARK: Error public enum Error : LocalizedError { + case generic case clockOutOfSync case randomSnodePoolUpdatingFailed public var errorDescription: String? { switch self { + case .generic: return "An error occurred." case .clockOutOfSync: return "Your clock is out of sync with the service node network." case .randomSnodePoolUpdatingFailed: return "Failed to update random service node pool." } @@ -41,8 +46,8 @@ public enum SnodeAPI { public typealias RawResponse = Any public typealias RawResponsePromise = Promise - // MARK: Core - internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise { + // MARK: Internal API + public static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise { if useOnionRequests { return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any } } else { @@ -109,7 +114,40 @@ public enum SnodeAPI { } } - internal static func getSwarm(for publicKey: String, isForcedReload: Bool = false) -> Promise> { + internal static func dropSnodeFromSnodePool(_ snode: Snode) { + var snodePool = SnodeAPI.snodePool + snodePool.remove(snode) + SnodeAPI.snodePool = snodePool + Configuration.shared.storage.with { transaction in + Configuration.shared.storage.setSnodePool(to: snodePool, using: transaction) + } + } + + public static func clearSnodePool() { + snodePool.removeAll() + Configuration.shared.storage.with { transaction in + Configuration.shared.storage.setSnodePool(to: [], using: transaction) + } + } + + // MARK: Public API + public static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) { + let swarm = SnodeAPI.swarmCache[publicKey] + if var swarm = swarm, let index = swarm.firstIndex(of: snode) { + swarm.remove(at: index) + SnodeAPI.swarmCache[publicKey] = swarm + Configuration.shared.storage.with { transaction in + Configuration.shared.storage.setSwarm(to: swarm, for: publicKey, using: transaction) + } + } + } + + public static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> { + // shuffled() uses the system's default random generator, which is cryptographically secure + return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) } + } + + public static func getSwarm(for publicKey: String, isForcedReload: Bool = false) -> Promise> { if swarmCache[publicKey] == nil { swarmCache[publicKey] = Configuration.shared.storage.getSwarm(for: publicKey) } @@ -133,48 +171,26 @@ public enum SnodeAPI { } } - internal static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> { - // shuffled() uses the system's default random generator, which is cryptographically secure - return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) } - } - - internal static func dropSnodeFromSnodePool(_ snode: Snode) { - var snodePool = SnodeAPI.snodePool - snodePool.remove(snode) - SnodeAPI.snodePool = snodePool - Configuration.shared.storage.with { transaction in - Configuration.shared.storage.setSnodePool(to: snodePool, using: transaction) + public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise { + let storage = Configuration.shared.storage + storage.with { transaction in + storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey, using: transaction) } + let lastHash = storage.getLastMessageHash(for: snode, associatedWith: publicKey) ?? "" + let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ] + return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters) } - public static func clearSnodePool() { - snodePool.removeAll() - Configuration.shared.storage.with { transaction in - Configuration.shared.storage.setSnodePool(to: [], using: transaction) - } - } - - internal static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) { - let swarm = SnodeAPI.swarmCache[publicKey] - if var swarm = swarm, let index = swarm.firstIndex(of: snode) { - swarm.remove(at: index) - SnodeAPI.swarmCache[publicKey] = swarm - Configuration.shared.storage.with { transaction in - Configuration.shared.storage.setSwarm(to: swarm, for: publicKey, using: transaction) - } - } - } - - // MARK: Receiving public static func getMessages(for publicKey: String) -> Promise> { let (promise, seal) = Promise>.pending() + let storage = Configuration.shared.storage Threading.workQueue.async { attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { getTargetSnodes(for: publicKey).mapValues2 { targetSnode in - Configuration.shared.storage.with { transaction in - Configuration.shared.storage.pruneLastMessageHashInfoIfExpired(for: targetSnode, associatedWith: publicKey, using: transaction) + storage.with { transaction in + storage.pruneLastMessageHashInfoIfExpired(for: targetSnode, associatedWith: publicKey, using: transaction) } - let lastHash = Configuration.shared.storage.getLastMessageHash(for: targetSnode, associatedWith: publicKey) ?? "" + let lastHash = storage.getLastMessageHash(for: targetSnode, associatedWith: publicKey) ?? "" let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ] return invoke(.getMessages, on: targetSnode, associatedWith: publicKey, parameters: parameters).map2 { rawResponse in parseRawMessagesResponse(rawResponse, from: targetSnode, associatedWith: publicKey) @@ -185,7 +201,6 @@ public enum SnodeAPI { return promise } - // MARK: Sending public static func sendMessage(_ message: SnodeMessage) -> Promise> { let (promise, seal) = Promise>.pending() let publicKey = message.recipient @@ -230,7 +245,7 @@ public enum SnodeAPI { }) } - internal static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [JSON] { + public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [JSON] { guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] } updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages) return removeDuplicates(from: rawMessages, associatedWith: publicKey) diff --git a/SessionSnodeKit/Utilities/Promise+Threading.swift b/SessionSnodeKit/Utilities/Promise+Threading.swift index 7b4936794..ce7d5ab7a 100644 --- a/SessionSnodeKit/Utilities/Promise+Threading.swift +++ b/SessionSnodeKit/Utilities/Promise+Threading.swift @@ -1,6 +1,6 @@ import PromiseKit -internal extension Thenable { +public extension Thenable { @discardableResult func then2(_ body: @escaping (T) throws -> U) -> Promise where U : Thenable { @@ -23,7 +23,7 @@ internal extension Thenable { } } -internal extension Thenable where T: Sequence { +public extension Thenable where T: Sequence { @discardableResult func mapValues2(_ transform: @escaping (T.Iterator.Element) throws -> U) -> Promise<[U]> { @@ -31,7 +31,7 @@ internal extension Thenable where T: Sequence { } } -internal extension Guarantee { +public extension Guarantee { @discardableResult func then2(_ body: @escaping (T) -> Guarantee) -> Guarantee { @@ -54,7 +54,7 @@ internal extension Guarantee { } } -internal extension CatchMixin { +public extension CatchMixin { @discardableResult func catch2(_ body: @escaping (Error) -> Void) -> PMKFinalizer { @@ -77,7 +77,7 @@ internal extension CatchMixin { } } -internal extension CatchMixin where T == Void { +public extension CatchMixin where T == Void { @discardableResult func recover2(_ body: @escaping(Error) -> Void) -> Guarantee { diff --git a/SessionSnodeKit/Utilities/Promise+Delaying.swift b/SessionUtilitiesKit/Promise+Delaying.swift similarity index 77% rename from SessionSnodeKit/Utilities/Promise+Delaying.swift rename to SessionUtilitiesKit/Promise+Delaying.swift index 9bb1ee1d2..7e02f4e88 100644 --- a/SessionSnodeKit/Utilities/Promise+Delaying.swift +++ b/SessionUtilitiesKit/Promise+Delaying.swift @@ -1,7 +1,7 @@ import PromiseKit /// Delay the execution of the promise constructed in `body` by `delay` seconds. -internal func withDelay(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise) -> Promise { +public func withDelay(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise) -> Promise { #if DEBUG assert(Thread.current.isMainThread) // Timers don't do well on background queues #endif diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b87914bce..aa6d3e741 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 10AC6C7D50A0C865C5E4779B /* Pods_SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71CFEDD2D3C54277731012DF /* Pods_SessionUIKit.framework */; }; 2400888E239F30A600305217 /* SessionRestorationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2400888D239F30A600305217 /* SessionRestorationView.swift */; }; 2AE2882E4C2B96BFFF9EE27C /* Pods_SignalShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F94C85CB0B235DA37F68ED0 /* Pods_SignalShareExtension.framework */; }; 3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */; }; @@ -496,6 +497,7 @@ B10C9B601A7049EC00ECA2BF /* pause_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B10C9B5C1A7049EC00ECA2BF /* pause_icon@2x.png */; }; B10C9B611A7049EC00ECA2BF /* play_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = B10C9B5D1A7049EC00ECA2BF /* play_icon.png */; }; B10C9B621A7049EC00ECA2BF /* play_icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B10C9B5E1A7049EC00ECA2BF /* play_icon@2x.png */; }; + B3E0C9C6F1633B1ABCE5AD0B /* Pods_SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D547348A367C8A14D37FC0 /* Pods_SignalUtilitiesKit.framework */; }; B60EDE041A05A01700D73516 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B60EDE031A05A01700D73516 /* AudioToolbox.framework */; }; B633C5861A1D190B0059AC12 /* call@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C5041A1D190B0059AC12 /* call@2x.png */; }; B633C58D1A1D190B0059AC12 /* contact_default_feed.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C50B1A1D190B0059AC12 /* contact_default_feed.png */; }; @@ -599,6 +601,434 @@ C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C353F8F8244809150011121A /* PNOptionView.swift */; }; C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* ConversationCell.swift */; }; C33FD4E9255A149100E217F9 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; }; + C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FD9B2255A548A00E217F9 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; + C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; + C33FD9C3255A54EF00E217F9 /* SessionProtocolKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A8622553B41A00C340D1 /* SessionProtocolKit.framework */; }; + C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; + C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; + C33FDC20255A581F00E217F9 /* OWSOutgoingCallMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA66255A57F900E217F9 /* OWSOutgoingCallMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC21255A581F00E217F9 /* OWSPrimaryStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA67255A57F900E217F9 /* OWSPrimaryStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC22255A581F00E217F9 /* OWSBlockingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA68255A57F900E217F9 /* OWSBlockingManager.m */; }; + C33FDC23255A581F00E217F9 /* SSKPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA69255A57F900E217F9 /* SSKPreferences.swift */; }; + C33FDC24255A581F00E217F9 /* OWSMessageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6A255A57F900E217F9 /* OWSMessageManager.m */; }; + C33FDC25255A581F00E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6B255A57FA00E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.m */; }; + C33FDC26255A581F00E217F9 /* ProtoUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */; }; + C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6D255A57FA00E217F9 /* YapDatabase+Promise.swift */; }; + C33FDC28255A581F00E217F9 /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6E255A57FA00E217F9 /* Array+Description.swift */; }; + C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA6F255A57FA00E217F9 /* ReachabilityManager.swift */; }; + C33FDC2A255A581F00E217F9 /* TSMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA70255A57FA00E217F9 /* TSMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC2B255A581F00E217F9 /* OWSReadReceiptManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA71255A57FA00E217F9 /* OWSReadReceiptManager.m */; }; + C33FDC2C255A581F00E217F9 /* OWSFailedAttachmentDownloadsJob.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA72255A57FA00E217F9 /* OWSFailedAttachmentDownloadsJob.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC2D255A581F00E217F9 /* ECKeyPair+Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA73255A57FA00E217F9 /* ECKeyPair+Hexadecimal.swift */; }; + C33FDC2E255A581F00E217F9 /* ClosedGroupsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA74255A57FB00E217F9 /* ClosedGroupsProtocol.swift */; }; + C33FDC2F255A581F00E217F9 /* OWSSyncManagerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA75255A57FB00E217F9 /* OWSSyncManagerProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC30255A581F00E217F9 /* OWSSyncGroupsRequestMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA76255A57FB00E217F9 /* OWSSyncGroupsRequestMessage.m */; }; + C33FDC31255A581F00E217F9 /* Contact.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA77255A57FB00E217F9 /* Contact.m */; }; + C33FDC32255A581F00E217F9 /* SSKWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA78255A57FB00E217F9 /* SSKWebSocket.swift */; }; + C33FDC33255A581F00E217F9 /* TSGroupThread.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA79255A57FB00E217F9 /* TSGroupThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC34255A581F00E217F9 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; }; + C33FDC35255A581F00E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA7B255A57FB00E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC36255A581F00E217F9 /* Debugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7C255A57FB00E217F9 /* Debugging.swift */; }; + C33FDC37255A581F00E217F9 /* OWSCensorshipConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7D255A57FB00E217F9 /* OWSCensorshipConfiguration.m */; }; + C33FDC38255A581F00E217F9 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7E255A57FB00E217F9 /* Mention.swift */; }; + C33FDC39255A581F00E217F9 /* OWSRecordTranscriptJob.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7F255A57FC00E217F9 /* OWSRecordTranscriptJob.m */; }; + C33FDC3A255A581F00E217F9 /* OWSDisappearingMessagesJob.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA80255A57FC00E217F9 /* OWSDisappearingMessagesJob.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC3B255A581F00E217F9 /* MentionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA81255A57FC00E217F9 /* MentionsManager.swift */; }; + C33FDC3D255A581F00E217F9 /* Promise+retainUntilComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA83255A57FC00E217F9 /* Promise+retainUntilComplete.swift */; }; + C33FDC3E255A581F00E217F9 /* ContactsUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA84255A57FC00E217F9 /* ContactsUpdater.m */; }; + C33FDC3F255A581F00E217F9 /* OWSPrimaryStorage+Loki.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA85255A57FC00E217F9 /* OWSPrimaryStorage+Loki.swift */; }; + C33FDC40255A581F00E217F9 /* OWSDisappearingMessagesFinder.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA86255A57FC00E217F9 /* OWSDisappearingMessagesFinder.m */; }; + C33FDC41255A581F00E217F9 /* TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */; }; + C33FDC42255A581F00E217F9 /* YapDatabaseTransaction+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA88255A57FD00E217F9 /* YapDatabaseTransaction+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC43255A581F00E217F9 /* OWSAnalytics.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA89255A57FD00E217F9 /* OWSAnalytics.m */; }; + C33FDC44255A581F00E217F9 /* PhoneNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8A255A57FD00E217F9 /* PhoneNumber.m */; }; + C33FDC45255A581F00E217F9 /* AppVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8B255A57FD00E217F9 /* AppVersion.m */; }; + C33FDC46255A581F00E217F9 /* PublicChatPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8C255A57FD00E217F9 /* PublicChatPoller.swift */; }; + C33FDC47255A581F00E217F9 /* OWSReceiptsForSenderMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA8D255A57FD00E217F9 /* OWSReceiptsForSenderMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC48255A581F00E217F9 /* OWSFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */; }; + C33FDC49255A581F00E217F9 /* NSTimer+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8F255A57FD00E217F9 /* NSTimer+OWS.m */; }; + C33FDC4A255A582000E217F9 /* TSYapDatabaseObject.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA90255A57FD00E217F9 /* TSYapDatabaseObject.m */; }; + C33FDC4B255A582000E217F9 /* LKSyncOpenGroupsMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA91255A57FD00E217F9 /* LKSyncOpenGroupsMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC4C255A582000E217F9 /* OWSMessageSender.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA92255A57FE00E217F9 /* OWSMessageSender.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC4E255A582000E217F9 /* Data+Streaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA94255A57FE00E217F9 /* Data+Streaming.swift */; }; + C33FDC4F255A582000E217F9 /* OWSChunkedOutputStream.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA95255A57FE00E217F9 /* OWSChunkedOutputStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA96255A57FE00E217F9 /* OWSDispatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC51255A582000E217F9 /* TSIncomingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA97255A57FE00E217F9 /* TSIncomingMessage.m */; }; + C33FDC52255A582000E217F9 /* RotateSignedKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA98255A57FE00E217F9 /* RotateSignedKeyOperation.swift */; }; + C33FDC53255A582000E217F9 /* OutageDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA99255A57FE00E217F9 /* OutageDetection.swift */; }; + C33FDC54255A582000E217F9 /* OWSLinkedDeviceReadReceipt.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9A255A57FE00E217F9 /* OWSLinkedDeviceReadReceipt.m */; }; + C33FDC55255A582000E217F9 /* OWSProvisioningMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9B255A57FE00E217F9 /* OWSProvisioningMessage.m */; }; + C33FDC56255A582000E217F9 /* OWSSyncContactsMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9C255A57FE00E217F9 /* OWSSyncContactsMessage.m */; }; + C33FDC57255A582000E217F9 /* OWSContactsOutputStream.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA9D255A57FF00E217F9 /* OWSContactsOutputStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; }; + C33FDC59255A582000E217F9 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9F255A57FF00E217F9 /* NetworkManager.swift */; }; + C33FDC5A255A582000E217F9 /* OWSRecipientIdentity.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAA0255A57FF00E217F9 /* OWSRecipientIdentity.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC5B255A582000E217F9 /* TSYapDatabaseObject.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAA1255A57FF00E217F9 /* TSYapDatabaseObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC5C255A582000E217F9 /* OWSAddToContactsOfferMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAA2255A57FF00E217F9 /* OWSAddToContactsOfferMessage.m */; }; + C33FDC5D255A582000E217F9 /* OWSAddToContactsOfferMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAA3255A57FF00E217F9 /* OWSAddToContactsOfferMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC5E255A582000E217F9 /* SSKProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAA4255A57FF00E217F9 /* SSKProto.swift */; }; + C33FDC5F255A582000E217F9 /* OWSRequestMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAA5255A57FF00E217F9 /* OWSRequestMaker.swift */; }; + C33FDC60255A582000E217F9 /* OWSLinkedDeviceReadReceipt.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAA6255A57FF00E217F9 /* OWSLinkedDeviceReadReceipt.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC61255A582000E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAA7255A57FF00E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC62255A582000E217F9 /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */; }; + C33FDC63255A582000E217F9 /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAA9255A580000E217F9 /* Mnemonic.swift */; }; + C33FDC64255A582000E217F9 /* NSObject+Casting.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAAA255A580000E217F9 /* NSObject+Casting.m */; }; + C33FDC65255A582000E217F9 /* OWSWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAAB255A580000E217F9 /* OWSWebSocket.m */; }; + C33FDC66255A582000E217F9 /* OWSMessageHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAAC255A580000E217F9 /* OWSMessageHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC67255A582000E217F9 /* OWSDeviceProvisioner.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAAD255A580000E217F9 /* OWSDeviceProvisioner.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC68255A582000E217F9 /* OWSReceiptsForSenderMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAAE255A580000E217F9 /* OWSReceiptsForSenderMessage.m */; }; + C33FDC69255A582000E217F9 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAAF255A580000E217F9 /* String+Trimming.swift */; }; + C33FDC6A255A582000E217F9 /* ProvisioningProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB0255A580000E217F9 /* ProvisioningProto.swift */; }; + C33FDC6B255A582000E217F9 /* OWSStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB1255A580000E217F9 /* OWSStorage.m */; }; + C33FDC6C255A582000E217F9 /* TSNetworkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB2255A580000E217F9 /* TSNetworkManager.m */; }; + C33FDC6D255A582000E217F9 /* TSContactThread.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAB3255A580000E217F9 /* TSContactThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC6E255A582000E217F9 /* OWSMessageReceiver.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB4255A580000E217F9 /* OWSMessageReceiver.m */; }; + C33FDC6F255A582000E217F9 /* TSNetworkManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAB5255A580000E217F9 /* TSNetworkManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC70255A582000E217F9 /* SyncMessagesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB6255A580100E217F9 /* SyncMessagesProtocol.swift */; }; + C33FDC71255A582000E217F9 /* OWSFailedMessagesJob.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB7255A580100E217F9 /* OWSFailedMessagesJob.m */; }; + C33FDC72255A582000E217F9 /* NSArray+Functional.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAB8255A580100E217F9 /* NSArray+Functional.m */; }; + C33FDC73255A582000E217F9 /* OWSStorage+Subclass.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAB9255A580100E217F9 /* OWSStorage+Subclass.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC74255A582000E217F9 /* OWSWebSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDABA255A580100E217F9 /* OWSWebSocket.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC75255A582000E217F9 /* OWSGroupsOutputStream.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDABB255A580100E217F9 /* OWSGroupsOutputStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC77255A582000E217F9 /* OWSOutgoingReceiptManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDABD255A580100E217F9 /* OWSOutgoingReceiptManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC78255A582000E217F9 /* TSConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDABE255A580100E217F9 /* TSConstants.m */; }; + C33FDC7A255A582000E217F9 /* OWSIncomingMessageFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAC0255A580100E217F9 /* OWSIncomingMessageFinder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC7B255A582000E217F9 /* NSSet+Functional.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC1255A580100E217F9 /* NSSet+Functional.m */; }; + C33FDC7C255A582000E217F9 /* TSAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC2255A580200E217F9 /* TSAttachment.m */; }; + C33FDC7D255A582000E217F9 /* OWSDispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC3255A580200E217F9 /* OWSDispatch.m */; }; + C33FDC7E255A582000E217F9 /* TSAttachmentStream.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC4255A580200E217F9 /* TSAttachmentStream.m */; }; + C33FDC7F255A582000E217F9 /* OWSCountryMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC5255A580200E217F9 /* OWSCountryMetadata.m */; }; + C33FDC80255A582000E217F9 /* Fingerprint.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC6255A580200E217F9 /* Fingerprint.pb.swift */; }; + C33FDC81255A582000E217F9 /* OWSSignalService.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAC7255A580200E217F9 /* OWSSignalService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC82255A582000E217F9 /* OWSCensorshipConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAC8255A580200E217F9 /* OWSCensorshipConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC83255A582000E217F9 /* OWSContact.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAC9255A580200E217F9 /* OWSContact.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC84255A582000E217F9 /* LokiMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDACA255A580200E217F9 /* LokiMessage.swift */; }; + C33FDC85255A582000E217F9 /* CDSSigningCertificate.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDACB255A580200E217F9 /* CDSSigningCertificate.m */; }; + C33FDC86255A582000E217F9 /* OWSMessageSend.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDACC255A580200E217F9 /* OWSMessageSend.swift */; }; + C33FDC87255A582000E217F9 /* SSKJobRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDACD255A580200E217F9 /* SSKJobRecord.m */; }; + C33FDC88255A582000E217F9 /* OWSVerificationStateSyncMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDACE255A580300E217F9 /* OWSVerificationStateSyncMessage.m */; }; + C33FDC89255A582000E217F9 /* OWSAttachmentDownloads.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDACF255A580300E217F9 /* OWSAttachmentDownloads.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC8A255A582000E217F9 /* CDSQuote.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAD0255A580300E217F9 /* CDSQuote.m */; }; + C33FDC8B255A582000E217F9 /* OWSDynamicOutgoingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAD1255A580300E217F9 /* OWSDynamicOutgoingMessage.m */; }; + C33FDC8C255A582000E217F9 /* Contact.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAD2255A580300E217F9 /* Contact.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC8D255A582000E217F9 /* TSThread.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAD3255A580300E217F9 /* TSThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC8E255A582000E217F9 /* EncryptionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAD4255A580300E217F9 /* EncryptionUtilities.swift */; }; + C33FDC8F255A582000E217F9 /* TSQuotedMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAD5255A580300E217F9 /* TSQuotedMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC90255A582000E217F9 /* OWSAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAD6255A580300E217F9 /* OWSAnalytics.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC91255A582000E217F9 /* OWSDeviceProvisioner.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAD7255A580300E217F9 /* OWSDeviceProvisioner.m */; }; + C33FDC92255A582000E217F9 /* OWSGroupsOutputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAD8255A580300E217F9 /* OWSGroupsOutputStream.m */; }; + C33FDC93255A582000E217F9 /* OWSDisappearingMessagesConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAD9255A580300E217F9 /* OWSDisappearingMessagesConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC94255A582000E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDADA255A580400E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC95255A582000E217F9 /* OWSFailedMessagesJob.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDADB255A580400E217F9 /* OWSFailedMessagesJob.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDADC255A580400E217F9 /* NSObject+Casting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC97255A582000E217F9 /* TSInfoMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDADD255A580400E217F9 /* TSInfoMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADE255A580400E217F9 /* SwiftSingletons.swift */; }; + C33FDC99255A582000E217F9 /* PublicChatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADF255A580400E217F9 /* PublicChatManager.swift */; }; + C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE0255A580400E217F9 /* ByteParser.m */; }; + C33FDC9B255A582000E217F9 /* OWSReadTracking.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAE1255A580400E217F9 /* OWSReadTracking.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC9C255A582000E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAE2255A580400E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC9D255A582000E217F9 /* TSPrefix.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAE3255A580400E217F9 /* TSPrefix.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC9E255A582000E217F9 /* TSAttachmentStream.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAE4255A580400E217F9 /* TSAttachmentStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDC9F255A582000E217F9 /* OWSAddToProfileWhitelistOfferMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE5255A580400E217F9 /* OWSAddToProfileWhitelistOfferMessage.m */; }; + C33FDCA0255A582000E217F9 /* TSInteraction.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAE6255A580400E217F9 /* TSInteraction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCA1255A582000E217F9 /* TSErrorMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE7255A580500E217F9 /* TSErrorMessage.m */; }; + C33FDCA2255A582000E217F9 /* OWSMessageUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCA3255A582000E217F9 /* SSKMessageSenderJobRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAE9255A580500E217F9 /* SSKMessageSenderJobRecord.m */; }; + C33FDCA4255A582000E217F9 /* OWSBackupFragment.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCA5255A582000E217F9 /* OWSDeviceProvisioningCodeService.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAEB255A580500E217F9 /* OWSDeviceProvisioningCodeService.m */; }; + C33FDCA6255A582000E217F9 /* SignalRecipient.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAEC255A580500E217F9 /* SignalRecipient.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCA7255A582000E217F9 /* SSKMessageSenderJobRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAED255A580500E217F9 /* SSKMessageSenderJobRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCA8255A582000E217F9 /* OWSFingerprintBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAEE255A580500E217F9 /* OWSFingerprintBuilder.m */; }; + C33FDCA9255A582000E217F9 /* NSData+Image.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAEF255A580500E217F9 /* NSData+Image.m */; }; + C33FDCAA255A582000E217F9 /* ContactsUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAF0255A580500E217F9 /* ContactsUpdater.h */; }; + C33FDCAB255A582000E217F9 /* OWSThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */; }; + C33FDCAC255A582000E217F9 /* ProxiedContentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */; }; + C33FDCAD255A582000E217F9 /* OWSPrimaryStorage+SessionStore.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF3255A580500E217F9 /* OWSPrimaryStorage+SessionStore.m */; }; + C33FDCAE255A582000E217F9 /* SSKEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF4255A580600E217F9 /* SSKEnvironment.m */; }; + C33FDCAF255A582000E217F9 /* OWSAnalyticsEvents.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAF5255A580600E217F9 /* OWSAnalyticsEvents.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCB0255A582000E217F9 /* OWSIncomingSentMessageTranscript.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAF6255A580600E217F9 /* OWSIncomingSentMessageTranscript.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCB1255A582000E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF7255A580600E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.m */; }; + C33FDCB2255A582000E217F9 /* OWSSyncGroupsRequestMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAF8255A580600E217F9 /* OWSSyncGroupsRequestMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCB3255A582000E217F9 /* TSContactThread.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF9255A580600E217F9 /* TSContactThread.m */; }; + C33FDCB4255A582000E217F9 /* LKDeviceLinkMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFA255A580600E217F9 /* LKDeviceLinkMessage.m */; }; + C33FDCB5255A582000E217F9 /* SessionMetaProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFB255A580600E217F9 /* SessionMetaProtocol.swift */; }; + C33FDCB6255A582000E217F9 /* MIMETypeUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAFC255A580600E217F9 /* MIMETypeUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCB7255A582000E217F9 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.swift */; }; + C33FDCB8255A582000E217F9 /* OWSStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAFE255A580600E217F9 /* OWSStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCB9255A582000E217F9 /* DisplayNameUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFF255A580600E217F9 /* DisplayNameUtilities.swift */; }; + C33FDCBA255A582000E217F9 /* OWSRequestBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB00255A580600E217F9 /* OWSRequestBuilder.m */; }; + C33FDCBB255A582000E217F9 /* AppReadiness.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB01255A580700E217F9 /* AppReadiness.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCBC255A582000E217F9 /* TSCall.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB02255A580700E217F9 /* TSCall.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCBD255A582000E217F9 /* OWSPrimaryStorage+SessionStore.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB03255A580700E217F9 /* OWSPrimaryStorage+SessionStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCBF255A582000E217F9 /* OWSFingerprint.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB05255A580700E217F9 /* OWSFingerprint.m */; }; + C33FDCC0255A582000E217F9 /* OWSRequestBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB06255A580700E217F9 /* OWSRequestBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCC1255A582000E217F9 /* OWSBackupFragment.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB07255A580700E217F9 /* OWSBackupFragment.m */; }; + C33FDCC2255A582000E217F9 /* OWSProfileKeyMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB08255A580700E217F9 /* OWSProfileKeyMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCC3255A582000E217F9 /* NSError+MessageSending.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB09255A580700E217F9 /* NSError+MessageSending.m */; }; + C33FDCC4255A582000E217F9 /* TSGroupModel.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB0A255A580700E217F9 /* TSGroupModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCC5255A582000E217F9 /* OWSVerificationStateChangeMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB0B255A580700E217F9 /* OWSVerificationStateChangeMessage.m */; }; + C33FDCC6255A582000E217F9 /* CDSQuote.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB0C255A580700E217F9 /* CDSQuote.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCC7255A582000E217F9 /* NSArray+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB0D255A580800E217F9 /* NSArray+OWS.m */; }; + C33FDCC8255A582000E217F9 /* NSError+MessageSending.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB0E255A580800E217F9 /* NSError+MessageSending.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCC9255A582000E217F9 /* DeviceLinkingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB0F255A580800E217F9 /* DeviceLinkingSession.swift */; }; + C33FDCCA255A582000E217F9 /* OWSBatchMessageProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB10255A580800E217F9 /* OWSBatchMessageProcessor.m */; }; + C33FDCCC255A582000E217F9 /* NSString+SSK.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB12255A580800E217F9 /* NSString+SSK.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCCD255A582000E217F9 /* LKUnlinkDeviceMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB13255A580800E217F9 /* LKUnlinkDeviceMessage.m */; }; + C33FDCCE255A582000E217F9 /* OWSMath.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB14255A580800E217F9 /* OWSMath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCD0255A582000E217F9 /* OWSDisappearingMessagesConfigurationMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB16255A580800E217F9 /* OWSDisappearingMessagesConfigurationMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB17255A580800E217F9 /* FunctionalUtil.m */; }; + C33FDCD2255A582000E217F9 /* OWSSignalService.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB18255A580800E217F9 /* OWSSignalService.m */; }; + C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB19255A580900E217F9 /* GroupUtilities.swift */; }; + C33FDCD4255A582000E217F9 /* OWSPrimaryStorage+Calling.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB1A255A580900E217F9 /* OWSPrimaryStorage+Calling.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCD5255A582000E217F9 /* OWSDeviceProvisioningCodeService.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB1B255A580900E217F9 /* OWSDeviceProvisioningCodeService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCD6255A582000E217F9 /* UIImage+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB1C255A580900E217F9 /* UIImage+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCD7255A582000E217F9 /* OWSReadReceiptManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB1D255A580900E217F9 /* OWSReadReceiptManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCD8255A582000E217F9 /* OWSIncomingMessageFinder.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB1E255A580900E217F9 /* OWSIncomingMessageFinder.m */; }; + C33FDCD9255A582000E217F9 /* DeviceLinkingUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB1F255A580900E217F9 /* DeviceLinkingUtilities.swift */; }; + C33FDCDA255A582000E217F9 /* TSDatabaseSecondaryIndexes.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB20255A580900E217F9 /* TSDatabaseSecondaryIndexes.m */; }; + C33FDCDB255A582000E217F9 /* OWS2FAManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB21255A580900E217F9 /* OWS2FAManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCDC255A582000E217F9 /* OWSMediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB22255A580900E217F9 /* OWSMediaUtils.swift */; }; + C33FDCDE255A582000E217F9 /* OWSOutgoingSentMessageTranscript.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB24255A580900E217F9 /* OWSOutgoingSentMessageTranscript.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCDF255A582000E217F9 /* TSDatabaseSecondaryIndexes.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB25255A580900E217F9 /* TSDatabaseSecondaryIndexes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCE0255A582000E217F9 /* FingerprintProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB26255A580A00E217F9 /* FingerprintProto.swift */; }; + C33FDCE1255A582000E217F9 /* OWSEndSessionMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB27255A580A00E217F9 /* OWSEndSessionMessage.m */; }; + C33FDCE2255A582000E217F9 /* OWSOutgoingCallMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB28255A580A00E217F9 /* OWSOutgoingCallMessage.m */; }; + C33FDCE3255A582000E217F9 /* NSData+Image.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB29255A580A00E217F9 /* NSData+Image.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCE4255A582000E217F9 /* OWSIncompleteCallsJob.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB2A255A580A00E217F9 /* OWSIncompleteCallsJob.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCE5255A582000E217F9 /* OWSProvisioningCipher.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB2B255A580A00E217F9 /* OWSProvisioningCipher.m */; }; + C33FDCE6255A582000E217F9 /* TSDatabaseView.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB2C255A580A00E217F9 /* TSDatabaseView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCE7255A582000E217F9 /* OWSDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB2D255A580A00E217F9 /* OWSDevice.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCE8255A582000E217F9 /* OWSEndSessionMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB2E255A580A00E217F9 /* OWSEndSessionMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCE9255A582000E217F9 /* ContactsManagerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB2F255A580A00E217F9 /* ContactsManagerProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCEA255A582000E217F9 /* OWSDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB30255A580A00E217F9 /* OWSDevice.m */; }; + C33FDCEB255A582000E217F9 /* SSKEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB31255A580A00E217F9 /* SSKEnvironment.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCEC255A582000E217F9 /* SSKIncrementingIdFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB32255A580A00E217F9 /* SSKIncrementingIdFinder.swift */; }; + C33FDCED255A582000E217F9 /* Provisioning.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB33255A580B00E217F9 /* Provisioning.pb.swift */; }; + C33FDCEE255A582000E217F9 /* ClosedGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */; }; + C33FDCEF255A582000E217F9 /* OWSContactDiscoveryOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB35255A580B00E217F9 /* OWSContactDiscoveryOperation.swift */; }; + C33FDCF0255A582000E217F9 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB36255A580B00E217F9 /* Storage.swift */; }; + C33FDCF1255A582000E217F9 /* Storage+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB37255A580B00E217F9 /* Storage+SnodeAPI.swift */; }; + C33FDCF2255A582000E217F9 /* OWSBackgroundTask.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCF4255A582000E217F9 /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3A255A580B00E217F9 /* Poller.swift */; }; + C33FDCF5255A582000E217F9 /* NSNotificationCenter+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB3B255A580B00E217F9 /* NSNotificationCenter+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCF7255A582000E217F9 /* OWSProfileKeyMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3D255A580B00E217F9 /* OWSProfileKeyMessage.m */; }; + C33FDCF8255A582000E217F9 /* OWSMessageSender.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3E255A580B00E217F9 /* OWSMessageSender.m */; }; + C33FDCF9255A582000E217F9 /* String+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3F255A580C00E217F9 /* String+SSK.swift */; }; + C33FDCFA255A582000E217F9 /* SignalIOSProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */; }; + C33FDCFB255A582000E217F9 /* MIMETypeUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB41255A580C00E217F9 /* MIMETypeUtil.m */; }; + C33FDCFC255A582000E217F9 /* OWSCountryMetadata.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB42255A580C00E217F9 /* OWSCountryMetadata.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDCFD255A582000E217F9 /* YapDatabaseConnection+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB43255A580C00E217F9 /* YapDatabaseConnection+OWS.m */; }; + C33FDCFE255A582000E217F9 /* OWSContactsOutputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB44255A580C00E217F9 /* OWSContactsOutputStream.m */; }; + C33FDCFF255A582000E217F9 /* NSString+SSK.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB45255A580C00E217F9 /* NSString+SSK.m */; }; + C33FDD00255A582000E217F9 /* TSDatabaseView.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB46255A580C00E217F9 /* TSDatabaseView.m */; }; + C33FDD01255A582000E217F9 /* OWSPrimaryStorage+PreKeyStore.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB47255A580C00E217F9 /* OWSPrimaryStorage+PreKeyStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD02255A582000E217F9 /* TSOutgoingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB48255A580C00E217F9 /* TSOutgoingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD03255A582000E217F9 /* WeakTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB49255A580C00E217F9 /* WeakTimer.swift */; }; + C33FDD04255A582000E217F9 /* SignalServiceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB4A255A580C00E217F9 /* SignalServiceClient.swift */; }; + C33FDD05255A582000E217F9 /* OWSChunkedOutputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB4B255A580C00E217F9 /* OWSChunkedOutputStream.m */; }; + C33FDD06255A582000E217F9 /* AppVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB4C255A580D00E217F9 /* AppVersion.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD09255A582000E217F9 /* SSKJobRecord.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB4F255A580D00E217F9 /* SSKJobRecord.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD0A255A582000E217F9 /* OWSPrimaryStorage+PreKeyStore.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB50255A580D00E217F9 /* OWSPrimaryStorage+PreKeyStore.m */; }; + C33FDD0B255A582000E217F9 /* NSUserDefaults+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD0C255A582000E217F9 /* OWSHTTPSecurityPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB52255A580D00E217F9 /* OWSHTTPSecurityPolicy.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD0D255A582000E217F9 /* PreKeyBundle+jsonDict.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB53255A580D00E217F9 /* PreKeyBundle+jsonDict.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD0E255A582000E217F9 /* DataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB54255A580D00E217F9 /* DataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD0F255A582000E217F9 /* TSInvalidIdentityKeySendingErrorMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB55255A580D00E217F9 /* TSInvalidIdentityKeySendingErrorMessage.m */; }; + C33FDD10255A582000E217F9 /* TSOutgoingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB56255A580D00E217F9 /* TSOutgoingMessage.m */; }; + C33FDD11255A582000E217F9 /* OWSSyncContactsMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB57255A580D00E217F9 /* OWSSyncContactsMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD12255A582000E217F9 /* OWSPrimaryStorage+Loki.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB58255A580E00E217F9 /* OWSPrimaryStorage+Loki.m */; }; + C33FDD13255A582000E217F9 /* OWSFailedAttachmentDownloadsJob.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB59255A580E00E217F9 /* OWSFailedAttachmentDownloadsJob.m */; }; + C33FDD14255A582000E217F9 /* OWSUDManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB5A255A580E00E217F9 /* OWSUDManager.swift */; }; + C33FDD15255A582000E217F9 /* YapDatabaseTransaction+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB5B255A580E00E217F9 /* YapDatabaseTransaction+OWS.m */; }; + C33FDD16255A582000E217F9 /* NSArray+Functional.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD17255A582000E217F9 /* TSErrorMessage_privateConstructor.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB5D255A580E00E217F9 /* TSErrorMessage_privateConstructor.h */; }; + C33FDD18255A582000E217F9 /* ContactParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB5E255A580E00E217F9 /* ContactParser.swift */; }; + C33FDD19255A582000E217F9 /* YapDatabaseConnection+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB5F255A580E00E217F9 /* YapDatabaseConnection+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD1A255A582000E217F9 /* TSMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB60255A580E00E217F9 /* TSMessage.m */; }; + C33FDD1B255A582000E217F9 /* LKUnlinkDeviceMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB61255A580E00E217F9 /* LKUnlinkDeviceMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD1C255A582000E217F9 /* OWSProvisioningCipher.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB62255A580E00E217F9 /* OWSProvisioningCipher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD1D255A582000E217F9 /* OWSDynamicOutgoingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB63255A580E00E217F9 /* OWSDynamicOutgoingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD1E255A582000E217F9 /* PreKeyRefreshOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB64255A580E00E217F9 /* PreKeyRefreshOperation.swift */; }; + C33FDD1F255A582000E217F9 /* OWSSyncConfigurationMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB65255A580F00E217F9 /* OWSSyncConfigurationMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD20255A582000E217F9 /* ContactDiscoveryService.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB66255A580F00E217F9 /* ContactDiscoveryService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD21255A582000E217F9 /* OWSMediaGalleryFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB67255A580F00E217F9 /* OWSMediaGalleryFinder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD22255A582000E217F9 /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB68255A580F00E217F9 /* ContentProxy.swift */; }; + C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB69255A580F00E217F9 /* FeatureFlags.swift */; }; + C33FDD24255A582000E217F9 /* SignalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6A255A580F00E217F9 /* SignalMessage.swift */; }; + C33FDD25255A582000E217F9 /* LKUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6B255A580F00E217F9 /* LKUserDefaults.swift */; }; + C33FDD26255A582000E217F9 /* NSNotificationCenter+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6C255A580F00E217F9 /* NSNotificationCenter+OWS.m */; }; + C33FDD27255A582000E217F9 /* TSPreKeyManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB6D255A580F00E217F9 /* TSPreKeyManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD28255A582000E217F9 /* SSKProtoPrekeyBundleMessage+Loki.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6E255A580F00E217F9 /* SSKProtoPrekeyBundleMessage+Loki.swift */; }; + C33FDD29255A582000E217F9 /* OWSOutgoingReceiptManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6F255A580F00E217F9 /* OWSOutgoingReceiptManager.m */; }; + C33FDD2A255A582000E217F9 /* OWSMessageServiceParams.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB70255A580F00E217F9 /* OWSMessageServiceParams.m */; }; + C33FDD2B255A582000E217F9 /* OWSMediaGalleryFinder.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB71255A581000E217F9 /* OWSMediaGalleryFinder.m */; }; + C33FDD2C255A582000E217F9 /* DeviceLinkIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB72255A581000E217F9 /* DeviceLinkIndex.swift */; }; + C33FDD2D255A582000E217F9 /* TSGroupModel.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB73255A581000E217F9 /* TSGroupModel.m */; }; + C33FDD2E255A582000E217F9 /* TSInvalidIdentityKeyErrorMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB74255A581000E217F9 /* TSInvalidIdentityKeyErrorMessage.m */; }; + C33FDD2F255A582000E217F9 /* AppReadiness.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB75255A581000E217F9 /* AppReadiness.m */; }; + C33FDD30255A582000E217F9 /* ClosedGroupUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB76255A581000E217F9 /* ClosedGroupUtilities.swift */; }; + C33FDD31255A582000E217F9 /* NSUserDefaults+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */; }; + C33FDD32255A582000E217F9 /* OWSOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB78255A581000E217F9 /* OWSOperation.m */; }; + C33FDD33255A582000E217F9 /* PhoneNumberUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB79255A581000E217F9 /* PhoneNumberUtil.m */; }; + C33FDD34255A582000E217F9 /* NotificationsProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB7A255A581000E217F9 /* NotificationsProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD36255A582000E217F9 /* OWSVerificationStateChangeMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB7C255A581000E217F9 /* OWSVerificationStateChangeMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD37255A582000E217F9 /* OWSMessageManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB7D255A581100E217F9 /* OWSMessageManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD38255A582000E217F9 /* TSPreKeyManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB7E255A581100E217F9 /* TSPreKeyManager.m */; }; + C33FDD39255A582000E217F9 /* FullTextSearchFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */; }; + C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB80255A581100E217F9 /* Notification+Loki.swift */; }; + C33FDD3B255A582000E217F9 /* UIImage+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB81255A581100E217F9 /* UIImage+OWS.m */; }; + C33FDD3C255A582000E217F9 /* MessageWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB82255A581100E217F9 /* MessageWrapper.swift */; }; + C33FDD3D255A582000E217F9 /* TSQuotedMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB83255A581100E217F9 /* TSQuotedMessage.m */; }; + C33FDD3E255A582000E217F9 /* OWSIncomingSentMessageTranscript.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB84255A581100E217F9 /* OWSIncomingSentMessageTranscript.m */; }; + C33FDD3F255A582000E217F9 /* AppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB85255A581100E217F9 /* AppContext.m */; }; + C33FDD40255A582000E217F9 /* OWSRequestFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB86255A581100E217F9 /* OWSRequestFactory.m */; }; + C33FDD41255A582000E217F9 /* JobQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB87255A581100E217F9 /* JobQueue.swift */; }; + C33FDD42255A582000E217F9 /* TSAccountManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB88255A581200E217F9 /* TSAccountManager.m */; }; + C33FDD43255A582000E217F9 /* FileServerAPI+Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB89255A581200E217F9 /* FileServerAPI+Deprecated.swift */; }; + C33FDD44255A582000E217F9 /* AppContext.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB8A255A581200E217F9 /* AppContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD45255A582000E217F9 /* Storage+SessionManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8B255A581200E217F9 /* Storage+SessionManagement.swift */; }; + C33FDD46255A582000E217F9 /* TSInvalidIdentityKeyErrorMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB8C255A581200E217F9 /* TSInvalidIdentityKeyErrorMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD47255A582000E217F9 /* DeviceLinkingSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8D255A581200E217F9 /* DeviceLinkingSessionDelegate.swift */; }; + C33FDD48255A582000E217F9 /* OWSContact+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB8E255A581200E217F9 /* OWSContact+Private.h */; }; + C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8F255A581200E217F9 /* ParamParser.swift */; }; + C33FDD4A255A582000E217F9 /* OWSMessageDecrypter.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB90255A581200E217F9 /* OWSMessageDecrypter.m */; }; + C33FDD4B255A582000E217F9 /* ProtoUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB91255A581200E217F9 /* ProtoUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD4C255A582000E217F9 /* OWSDeviceProvisioningService.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB92255A581200E217F9 /* OWSDeviceProvisioningService.m */; }; + C33FDD4D255A582000E217F9 /* PreKeyBundle+jsonDict.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB93255A581200E217F9 /* PreKeyBundle+jsonDict.m */; }; + C33FDD4E255A582000E217F9 /* TSAccountManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB94255A581300E217F9 /* TSAccountManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD4F255A582000E217F9 /* Storage+Collections.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB95255A581300E217F9 /* Storage+Collections.swift */; }; + C33FDD50255A582000E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB96255A581300E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.m */; }; + C33FDD51255A582000E217F9 /* OWSDisappearingMessagesConfigurationMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB97255A581300E217F9 /* OWSDisappearingMessagesConfigurationMessage.m */; }; + C33FDD52255A582000E217F9 /* DeviceNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB98255A581300E217F9 /* DeviceNames.swift */; }; + C33FDD53255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */; }; + C33FDD54255A582000E217F9 /* OWS2FAManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB9A255A581300E217F9 /* OWS2FAManager.m */; }; + C33FDD55255A582000E217F9 /* MessageSenderJobQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB9B255A581300E217F9 /* MessageSenderJobQueue.swift */; }; + C33FDD56255A582000E217F9 /* TSIncomingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB9C255A581300E217F9 /* TSIncomingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD57255A582000E217F9 /* OWSCallMessageHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB9D255A581300E217F9 /* OWSCallMessageHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD58255A582000E217F9 /* TSAttachmentPointer.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB9E255A581400E217F9 /* TSAttachmentPointer.m */; }; + C33FDD59255A582000E217F9 /* TTLUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB9F255A581400E217F9 /* TTLUtilities.swift */; }; + C33FDD5A255A582000E217F9 /* TSStorageHeaders.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA0255A581400E217F9 /* TSStorageHeaders.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA1255A581400E217F9 /* OWSOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD5C255A582000E217F9 /* PhoneNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA2255A581400E217F9 /* PhoneNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD5D255A582000E217F9 /* SessionManagementProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA3255A581400E217F9 /* SessionManagementProtocol.swift */; }; + C33FDD5E255A582000E217F9 /* OWSDisappearingMessagesConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA4255A581400E217F9 /* OWSDisappearingMessagesConfiguration.m */; }; + C33FDD5F255A582000E217F9 /* SignalServiceProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA5255A581400E217F9 /* SignalServiceProfile.swift */; }; + C33FDD60255A582000E217F9 /* TSInvalidIdentityKeySendingErrorMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA6255A581400E217F9 /* TSInvalidIdentityKeySendingErrorMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD62255A582000E217F9 /* OWSLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */; }; + C33FDD63255A582000E217F9 /* OWSIdentityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA9255A581500E217F9 /* OWSIdentityManager.m */; }; + C33FDD65255A582000E217F9 /* OWSFileSystem.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBAB255A581500E217F9 /* OWSFileSystem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD66255A582000E217F9 /* Data+SecureRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBAC255A581500E217F9 /* Data+SecureRandom.swift */; }; + C33FDD67255A582000E217F9 /* OWSRecordTranscriptJob.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBAD255A581500E217F9 /* OWSRecordTranscriptJob.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBAE255A581500E217F9 /* SignalAccount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD69255A582000E217F9 /* OWSAnalyticsEvents.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBAF255A581500E217F9 /* OWSAnalyticsEvents.m */; }; + C33FDD6A255A582000E217F9 /* TSErrorMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBB0255A581500E217F9 /* TSErrorMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD6B255A582000E217F9 /* TSSocketManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBB1255A581500E217F9 /* TSSocketManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD6D255A582000E217F9 /* OWSOutgoingNullMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBB3255A581500E217F9 /* OWSOutgoingNullMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */; }; + C33FDD6F255A582000E217F9 /* OWSSyncGroupsMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBB5255A581600E217F9 /* OWSSyncGroupsMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD70255A582000E217F9 /* DataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB6255A581600E217F9 /* DataSource.m */; }; + C33FDD71255A582000E217F9 /* SignalRecipient.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB7255A581600E217F9 /* SignalRecipient.m */; }; + C33FDD72255A582000E217F9 /* TSThread.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB8255A581600E217F9 /* TSThread.m */; }; + C33FDD73255A582000E217F9 /* ProfileManagerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD74255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD75255A582000E217F9 /* OWSPrimaryStorage+Loki.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBBB255A581600E217F9 /* OWSPrimaryStorage+Loki.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD76255A582000E217F9 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; }; + C33FDD77255A582000E217F9 /* OWSAddToProfileWhitelistOfferMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBBD255A581600E217F9 /* OWSAddToProfileWhitelistOfferMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD79255A582000E217F9 /* OWSHTTPSecurityPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBF255A581700E217F9 /* OWSHTTPSecurityPolicy.m */; }; + C33FDD7A255A582000E217F9 /* OWSRequestFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBC0255A581700E217F9 /* OWSRequestFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD7B255A582000E217F9 /* GeneralUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBC1255A581700E217F9 /* GeneralUtilities.swift */; }; + C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBC2255A581700E217F9 /* SSKAsserts.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD7D255A582000E217F9 /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBC3255A581700E217F9 /* AnyPromise+Conversion.swift */; }; + C33FDD7E255A582000E217F9 /* TypingIndicatorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBC4255A581700E217F9 /* TypingIndicatorMessage.swift */; }; + C33FDD80255A582000E217F9 /* OWSMessageReceiver.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBC6255A581700E217F9 /* OWSMessageReceiver.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD82255A582000E217F9 /* OWSFingerprint.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBC8255A581700E217F9 /* OWSFingerprint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD83255A582000E217F9 /* CreatePreKeysOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBC9255A581700E217F9 /* CreatePreKeysOperation.swift */; }; + C33FDD84255A582000E217F9 /* LKGroupUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBCA255A581700E217F9 /* LKGroupUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD85255A582000E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBCB255A581800E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.m */; }; + C33FDD86255A582000E217F9 /* MultiDeviceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBCC255A581800E217F9 /* MultiDeviceProtocol.swift */; }; + C33FDD88255A582000E217F9 /* OWSMessageServiceParams.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBCE255A581800E217F9 /* OWSMessageServiceParams.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD89255A582000E217F9 /* OWSFingerprintBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBCF255A581800E217F9 /* OWSFingerprintBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD8A255A582000E217F9 /* OnionRequestAPI+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD0255A581800E217F9 /* OnionRequestAPI+Encryption.swift */; }; + C33FDD8B255A582000E217F9 /* DeviceLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD1255A581800E217F9 /* DeviceLink.swift */; }; + C33FDD8C255A582000E217F9 /* OWSUnknownContactBlockOfferMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBD2255A581800E217F9 /* OWSUnknownContactBlockOfferMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; }; + C33FDD8E255A582000E217F9 /* OWSBatchMessageProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBD4255A581900E217F9 /* OWSBatchMessageProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD8F255A582000E217F9 /* OWSOutgoingSentMessageTranscript.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD5255A581900E217F9 /* OWSOutgoingSentMessageTranscript.m */; }; + C33FDD90255A582000E217F9 /* OWSUploadOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBD6255A581900E217F9 /* OWSUploadOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */; }; + C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */; }; + C33FDD93255A582000E217F9 /* OWSVerificationStateSyncMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBD9255A581900E217F9 /* OWSVerificationStateSyncMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD94255A582000E217F9 /* Dictionary+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDA255A581900E217F9 /* Dictionary+Description.swift */; }; + C33FDD95255A582000E217F9 /* OWSDevicesService.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBDB255A581900E217F9 /* OWSDevicesService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD96255A582000E217F9 /* ContactDiscoveryService.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDC255A581900E217F9 /* ContactDiscoveryService.m */; }; + C33FDD97255A582000E217F9 /* OWSDisappearingMessagesJob.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDD255A581900E217F9 /* OWSDisappearingMessagesJob.m */; }; + C33FDD98255A582000E217F9 /* LokiPushNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* LokiPushNotificationManager.swift */; }; + C33FDD99255A582000E217F9 /* LKSyncOpenGroupsMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDF255A581A00E217F9 /* LKSyncOpenGroupsMessage.m */; }; + C33FDD9A255A582000E217F9 /* OWSBlockedPhoneNumbersMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBE0255A581A00E217F9 /* OWSBlockedPhoneNumbersMessage.m */; }; + C33FDD9B255A582000E217F9 /* LKGroupUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBE1255A581A00E217F9 /* LKGroupUtilities.m */; }; + C33FDD9C255A582000E217F9 /* OWSUnknownContactBlockOfferMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBE2255A581A00E217F9 /* OWSUnknownContactBlockOfferMessage.m */; }; + C33FDD9D255A582000E217F9 /* CDSSigningCertificate.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBE3255A581A00E217F9 /* CDSSigningCertificate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD9E255A582000E217F9 /* OWSOutgoingSyncMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBE4255A581A00E217F9 /* OWSOutgoingSyncMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDD9F255A582000E217F9 /* OWSDevicesService.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBE5255A581A00E217F9 /* OWSDevicesService.m */; }; + C33FDDA0255A582000E217F9 /* OWSOutgoingNullMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBE6255A581A00E217F9 /* OWSOutgoingNullMessage.m */; }; + C33FDDA1255A582000E217F9 /* NSTimer+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBE7255A581A00E217F9 /* NSTimer+OWS.h */; }; + C33FDDA2255A582000E217F9 /* Storage+OnionRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBE8255A581A00E217F9 /* Storage+OnionRequests.swift */; }; + C33FDDA3255A582000E217F9 /* TSInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBE9255A581A00E217F9 /* TSInteraction.m */; }; + C33FDDA4255A582000E217F9 /* SessionRequestMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBEA255A581A00E217F9 /* SessionRequestMessage.swift */; }; + C33FDDA5255A582000E217F9 /* OWSBlockingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBEB255A581B00E217F9 /* OWSBlockingManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDA6255A582000E217F9 /* OWSRecipientIdentity.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBEC255A581B00E217F9 /* OWSRecipientIdentity.m */; }; + C33FDDA7255A582000E217F9 /* ClosedGroupParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBED255A581B00E217F9 /* ClosedGroupParser.swift */; }; + C33FDDA8255A582000E217F9 /* ClosedGroupUpdateMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBEE255A581B00E217F9 /* ClosedGroupUpdateMessage.swift */; }; + C33FDDA9255A582000E217F9 /* TSStorageKeys.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBEF255A581B00E217F9 /* TSStorageKeys.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDAA255A582000E217F9 /* LokiDatabaseUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBF0255A581B00E217F9 /* LokiDatabaseUtilities.swift */; }; + C33FDDAB255A582000E217F9 /* OWSIdentityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF1255A581B00E217F9 /* OWSIdentityManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDAC255A582000E217F9 /* MessageSender+Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBF2255A581B00E217F9 /* MessageSender+Promise.swift */; }; + C33FDDAD255A582000E217F9 /* OWSBlockedPhoneNumbersMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF3255A581B00E217F9 /* OWSBlockedPhoneNumbersMessage.h */; }; + C33FDDAE255A582000E217F9 /* DisplayNameUtilities2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBF4255A581B00E217F9 /* DisplayNameUtilities2.swift */; }; + C33FDDAF255A582000E217F9 /* OWSDeviceProvisioningService.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF5255A581B00E217F9 /* OWSDeviceProvisioningService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDB1255A582000E217F9 /* OWSIncompleteCallsJob.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBF7255A581C00E217F9 /* OWSIncompleteCallsJob.m */; }; + C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDB3255A582000E217F9 /* OWSError.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF9255A581C00E217F9 /* OWSError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDB4255A582000E217F9 /* PhoneNumberUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBFA255A581C00E217F9 /* PhoneNumberUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDB5255A582000E217F9 /* Storage+PublicChats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBFB255A581C00E217F9 /* Storage+PublicChats.swift */; }; + C33FDDB6255A582000E217F9 /* OWSMessageHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBFC255A581C00E217F9 /* OWSMessageHandler.m */; }; + C33FDDB7255A582000E217F9 /* OWSPrimaryStorage+Calling.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBFD255A581C00E217F9 /* OWSPrimaryStorage+Calling.m */; }; + C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBFE255A581C00E217F9 /* NSSet+Functional.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDB9255A582000E217F9 /* OWSOutgoingSyncMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBFF255A581C00E217F9 /* OWSOutgoingSyncMessage.m */; }; + C33FDDBA255A582000E217F9 /* OWSSyncGroupsMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC00255A581C00E217F9 /* OWSSyncGroupsMessage.m */; }; + C33FDDBB255A582000E217F9 /* TSGroupThread.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC01255A581C00E217F9 /* TSGroupThread.m */; }; + C33FDDBC255A582000E217F9 /* OWSPrimaryStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC02255A581D00E217F9 /* OWSPrimaryStorage.m */; }; + C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC03255A581D00E217F9 /* ByteParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDBE255A582000E217F9 /* DecryptionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC04255A581D00E217F9 /* DecryptionUtilities.swift */; }; + C33FDDBF255A582000E217F9 /* OWSDisappearingMessagesFinder.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC05255A581D00E217F9 /* OWSDisappearingMessagesFinder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDC0255A582000E217F9 /* SignalAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC06255A581D00E217F9 /* SignalAccount.m */; }; + C33FDDC1255A582000E217F9 /* Storage+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC07255A581D00E217F9 /* Storage+ClosedGroups.swift */; }; + C33FDDC2255A582000E217F9 /* SignalService.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC08255A581D00E217F9 /* SignalService.pb.swift */; }; + C33FDDC3255A582000E217F9 /* AccountServiceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC09255A581D00E217F9 /* AccountServiceClient.swift */; }; + C33FDDC4255A582000E217F9 /* OWSContact.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC0A255A581D00E217F9 /* OWSContact.m */; }; + C33FDDC5255A582000E217F9 /* OWSError.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC0B255A581D00E217F9 /* OWSError.m */; }; + C33FDDC6255A582000E217F9 /* TSInfoMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC0C255A581E00E217F9 /* TSInfoMessage.m */; }; + C33FDDC7255A582000E217F9 /* OWSProvisioningMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC0D255A581E00E217F9 /* OWSProvisioningMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDC8255A582000E217F9 /* OWSMessageDecrypter.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC0E255A581E00E217F9 /* OWSMessageDecrypter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDC9255A582000E217F9 /* LKDeviceLinkMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC0F255A581E00E217F9 /* LKDeviceLinkMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDCA255A582000E217F9 /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC10255A581E00E217F9 /* ProofOfWork.swift */; }; + C33FDDCB255A582000E217F9 /* TSSocketManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC11255A581E00E217F9 /* TSSocketManager.m */; }; + C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC12255A581E00E217F9 /* TSConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDCD255A582000E217F9 /* OWSAttachmentDownloads.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC13255A581E00E217F9 /* OWSAttachmentDownloads.m */; }; + C33FDDCF255A582000E217F9 /* TSAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC15255A581E00E217F9 /* TSAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC16255A581E00E217F9 /* FunctionalUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDD1255A582000E217F9 /* SharedSenderKeysImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC17255A581F00E217F9 /* SharedSenderKeysImplementation.swift */; }; + C33FDDD2255A582000E217F9 /* TSAttachmentPointer.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC18255A581F00E217F9 /* TSAttachmentPointer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDD3255A582000E217F9 /* OWSQueues.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC19255A581F00E217F9 /* OWSQueues.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C33FDDD5255A582000E217F9 /* OWSBackgroundTask.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */; }; + C33FDDD6255A582000E217F9 /* TSCall.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1C255A581F00E217F9 /* TSCall.m */; }; + C33FDDD7255A582000E217F9 /* OWSSyncConfigurationMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1D255A581F00E217F9 /* OWSSyncConfigurationMessage.m */; }; + C33FDDD8255A582000E217F9 /* OWSUploadOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1E255A581F00E217F9 /* OWSUploadOperation.m */; }; + C33FDDD9255A582000E217F9 /* LokiSessionResetImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC1F255A581F00E217F9 /* LokiSessionResetImplementation.swift */; }; + C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */; }; C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */; }; C3471ED42555386B00297E91 /* AESGCM.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D72553860B00C340D1 /* AESGCM.swift */; }; @@ -632,6 +1062,8 @@ C364535C252467900045C478 /* AudioUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C364535B252467900045C478 /* AudioUtilities.swift */; }; C369549D24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C369549C24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift */; }; C36B8707243C50C60049991D /* SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 453518921FC63DBF00210559 /* SignalMessaging.framework */; }; + C38EEF0A255B49A8007E1867 /* SSKProtoEnvelope+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EEF09255B49A8007E1867 /* SSKProtoEnvelope+Conversion.swift */; }; + C38EEFD6255B5BA2007E1867 /* OldSnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EEFD5255B5BA2007E1867 /* OldSnodeAPI.swift */; }; C396DAEF2518408B00FF6DC5 /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAE82518408900FF6DC5 /* ParsingState.swift */; }; C396DAF02518408B00FF6DC5 /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAE92518408A00FF6DC5 /* String+Lines.swift */; }; C396DAF12518408B00FF6DC5 /* EnumeratedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAEA2518408A00FF6DC5 /* EnumeratedView.swift */; }; @@ -695,7 +1127,6 @@ C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */; }; C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D02553860800C340D1 /* Promise+Threading.swift */; }; C3C2A5DE2553860B00C340D1 /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Utilities.swift */; }; - C3C2A5DF2553860B00C340D1 /* Promise+Delaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */; }; C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -795,10 +1226,8 @@ C3C2AA002553B9C400C340D1 /* NSDate+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A9E52553B9C300C340D1 /* NSDate+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2AA012553B9C400C340D1 /* Threading.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A9E62553B9C300C340D1 /* Threading.m */; }; C3C2AA022553B9C400C340D1 /* Cryptography.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A9E72553B9C300C340D1 /* Cryptography.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C3C2AA032553B9C400C340D1 /* Randomness.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A9E82553B9C300C340D1 /* Randomness.m */; }; C3C2AA042553B9C400C340D1 /* OWSAsserts.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A9E92553B9C300C340D1 /* OWSAsserts.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2AA052553B9C400C340D1 /* SCKExceptionWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A9EA2553B9C300C340D1 /* SCKExceptionWrapper.m */; }; - C3C2AA062553B9C400C340D1 /* Randomness.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A9EB2553B9C400C340D1 /* Randomness.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2AA072553B9C400C340D1 /* Cryptography.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A9EC2553B9C400C340D1 /* Cryptography.m */; }; C3C2AA082553B9C400C340D1 /* NSString+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A9ED2553B9C400C340D1 /* NSString+OWS.m */; }; C3C2AA092553B9C400C340D1 /* NSObject+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A9EE2553B9C400C340D1 /* NSObject+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -891,6 +1320,13 @@ remoteGlobalIDString = C331FF1A2558F9D300070591; remoteInfo = SessionUIKit; }; + C33FD9B0255A548A00E217F9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = C33FD9AA255A548A00E217F9; + remoteInfo = SignalUtilitiesKit; + }; C36B8705243C50B00049991D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D221A080169C9E5E00537ABF /* Project object */; @@ -949,6 +1385,7 @@ files = ( C3C2A86A2553B41A00C340D1 /* SessionProtocolKit.framework in Embed Frameworks */, C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */, + C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */, C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */, 4535189A1FC63DBF00210559 /* SignalMessaging.framework in Embed Frameworks */, C331FF232558F9D300070591 /* SessionUIKit.framework in Embed Frameworks */, @@ -1500,9 +1937,12 @@ 4CFD151C22415AA400F2450F /* CallVideoHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoHintView.swift; sourceTree = ""; }; 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LegacyNotificationsAdaptee.swift; path = UserInterface/Notifications/LegacyNotificationsAdaptee.swift; sourceTree = ""; }; 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = ""; }; + 53D547348A367C8A14D37FC0 /* Pods_SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5F3070F3395081DD0EB4F933 /* Pods-SignalUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUtilitiesKit/Pods-SignalUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; 69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; 6A26D6558DE69AF455E571C1 /* Pods-SessionMessagingKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.debug.xcconfig"; sourceTree = ""; }; 70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + 71CFEDD2D3C54277731012DF /* Pods_SessionUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 768A1A2A17FC9CD300E00ED8 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; @@ -1521,6 +1961,7 @@ 948239851C08032C842937CC /* Pods-SignalMessaging.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.test.xcconfig"; sourceTree = ""; }; 9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9B533A9FA46206D3D99C9ADA /* Pods-SignalMessaging.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.debug.xcconfig"; sourceTree = ""; }; + 9C0469AC557930C01552CC83 /* Pods-SignalUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUtilitiesKit/Pods-SignalUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; A1C32D4D17A0652C000A904E /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = System/Library/Frameworks/AddressBook.framework; sourceTree = SDKROOT; }; @@ -1628,6 +2069,7 @@ B97940261832BD2400BD66CB /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIUtil.m; sourceTree = ""; }; B9EB5ABC1884C002007CBB57 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; C022DD8E076866C6241610BF /* Pods-SessionSnodeKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionSnodeKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionSnodeKit/Pods-SessionSnodeKit.app store release.xcconfig"; sourceTree = ""; }; + C1A746BC424B531D8ED478F6 /* Pods-SessionUIKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.app store release.xcconfig"; sourceTree = ""; }; C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Profile.swift"; sourceTree = ""; }; C300A5BC2554B00D00555489 /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; C300A5C82554B04E00555489 /* SessionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRequest.swift; sourceTree = ""; }; @@ -1654,6 +2096,429 @@ C331FF1B2558F9D300070591 /* SessionUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C331FF1D2558F9D300070591 /* SessionUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUIKit.h; sourceTree = ""; }; C331FF1E2558F9D300070591 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SignalUtilitiesKit.h; sourceTree = ""; }; + C33FD9AE255A548A00E217F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C33FDA66255A57F900E217F9 /* OWSOutgoingCallMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingCallMessage.h; sourceTree = ""; }; + C33FDA67255A57F900E217F9 /* OWSPrimaryStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSPrimaryStorage.h; sourceTree = ""; }; + C33FDA68255A57F900E217F9 /* OWSBlockingManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBlockingManager.m; sourceTree = ""; }; + C33FDA69255A57F900E217F9 /* SSKPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKPreferences.swift; sourceTree = ""; }; + C33FDA6A255A57F900E217F9 /* OWSMessageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageManager.m; sourceTree = ""; }; + C33FDA6B255A57FA00E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingConfigurationUpdateInfoMessage.m; sourceTree = ""; }; + C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProtoUtils.m; sourceTree = ""; }; + C33FDA6D255A57FA00E217F9 /* YapDatabase+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "YapDatabase+Promise.swift"; sourceTree = ""; }; + C33FDA6E255A57FA00E217F9 /* Array+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Description.swift"; sourceTree = ""; }; + C33FDA6F255A57FA00E217F9 /* ReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = ""; }; + C33FDA70255A57FA00E217F9 /* TSMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSMessage.h; sourceTree = ""; }; + C33FDA71255A57FA00E217F9 /* OWSReadReceiptManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSReadReceiptManager.m; sourceTree = ""; }; + C33FDA72255A57FA00E217F9 /* OWSFailedAttachmentDownloadsJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFailedAttachmentDownloadsJob.h; sourceTree = ""; }; + C33FDA73255A57FA00E217F9 /* ECKeyPair+Hexadecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ECKeyPair+Hexadecimal.swift"; sourceTree = ""; }; + C33FDA74255A57FB00E217F9 /* ClosedGroupsProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosedGroupsProtocol.swift; sourceTree = ""; }; + C33FDA75255A57FB00E217F9 /* OWSSyncManagerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSyncManagerProtocol.h; sourceTree = ""; }; + C33FDA76255A57FB00E217F9 /* OWSSyncGroupsRequestMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSyncGroupsRequestMessage.m; sourceTree = ""; }; + C33FDA77255A57FB00E217F9 /* Contact.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Contact.m; sourceTree = ""; }; + C33FDA78255A57FB00E217F9 /* SSKWebSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKWebSocket.swift; sourceTree = ""; }; + C33FDA79255A57FB00E217F9 /* TSGroupThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSGroupThread.h; sourceTree = ""; }; + C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+SSK.swift"; sourceTree = ""; }; + C33FDA7B255A57FB00E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSInvalidIdentityKeyReceivingErrorMessage.h; sourceTree = ""; }; + C33FDA7C255A57FB00E217F9 /* Debugging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Debugging.swift; sourceTree = ""; }; + C33FDA7D255A57FB00E217F9 /* OWSCensorshipConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSCensorshipConfiguration.m; sourceTree = ""; }; + C33FDA7E255A57FB00E217F9 /* Mention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; + C33FDA7F255A57FC00E217F9 /* OWSRecordTranscriptJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSRecordTranscriptJob.m; sourceTree = ""; }; + C33FDA80255A57FC00E217F9 /* OWSDisappearingMessagesJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDisappearingMessagesJob.h; sourceTree = ""; }; + C33FDA81255A57FC00E217F9 /* MentionsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionsManager.swift; sourceTree = ""; }; + C33FDA83255A57FC00E217F9 /* Promise+retainUntilComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+retainUntilComplete.swift"; sourceTree = ""; }; + C33FDA84255A57FC00E217F9 /* ContactsUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactsUpdater.m; sourceTree = ""; }; + C33FDA85255A57FC00E217F9 /* OWSPrimaryStorage+Loki.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OWSPrimaryStorage+Loki.swift"; sourceTree = ""; }; + C33FDA86255A57FC00E217F9 /* OWSDisappearingMessagesFinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingMessagesFinder.m; sourceTree = ""; }; + C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicators.swift; sourceTree = ""; }; + C33FDA88255A57FD00E217F9 /* YapDatabaseTransaction+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YapDatabaseTransaction+OWS.h"; sourceTree = ""; }; + C33FDA89255A57FD00E217F9 /* OWSAnalytics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAnalytics.m; sourceTree = ""; }; + C33FDA8A255A57FD00E217F9 /* PhoneNumber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhoneNumber.m; sourceTree = ""; }; + C33FDA8B255A57FD00E217F9 /* AppVersion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppVersion.m; sourceTree = ""; }; + C33FDA8C255A57FD00E217F9 /* PublicChatPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicChatPoller.swift; sourceTree = ""; }; + C33FDA8D255A57FD00E217F9 /* OWSReceiptsForSenderMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSReceiptsForSenderMessage.h; sourceTree = ""; }; + C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFileSystem.m; sourceTree = ""; }; + C33FDA8F255A57FD00E217F9 /* NSTimer+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSTimer+OWS.m"; sourceTree = ""; }; + C33FDA90255A57FD00E217F9 /* TSYapDatabaseObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSYapDatabaseObject.m; sourceTree = ""; }; + C33FDA91255A57FD00E217F9 /* LKSyncOpenGroupsMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LKSyncOpenGroupsMessage.h; sourceTree = ""; }; + C33FDA92255A57FE00E217F9 /* OWSMessageSender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageSender.h; sourceTree = ""; }; + C33FDA94255A57FE00E217F9 /* Data+Streaming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Streaming.swift"; sourceTree = ""; }; + C33FDA95255A57FE00E217F9 /* OWSChunkedOutputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSChunkedOutputStream.h; sourceTree = ""; }; + C33FDA96255A57FE00E217F9 /* OWSDispatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDispatch.h; sourceTree = ""; }; + C33FDA97255A57FE00E217F9 /* TSIncomingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSIncomingMessage.m; sourceTree = ""; }; + C33FDA98255A57FE00E217F9 /* RotateSignedKeyOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotateSignedKeyOperation.swift; sourceTree = ""; }; + C33FDA99255A57FE00E217F9 /* OutageDetection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutageDetection.swift; sourceTree = ""; }; + C33FDA9A255A57FE00E217F9 /* OWSLinkedDeviceReadReceipt.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSLinkedDeviceReadReceipt.m; sourceTree = ""; }; + C33FDA9B255A57FE00E217F9 /* OWSProvisioningMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProvisioningMessage.m; sourceTree = ""; }; + C33FDA9C255A57FE00E217F9 /* OWSSyncContactsMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSyncContactsMessage.m; sourceTree = ""; }; + C33FDA9D255A57FF00E217F9 /* OWSContactsOutputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsOutputStream.h; sourceTree = ""; }; + C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseDispatchQueue.swift; sourceTree = ""; }; + C33FDA9F255A57FF00E217F9 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + C33FDAA0255A57FF00E217F9 /* OWSRecipientIdentity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSRecipientIdentity.h; sourceTree = ""; }; + C33FDAA1255A57FF00E217F9 /* TSYapDatabaseObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSYapDatabaseObject.h; sourceTree = ""; }; + C33FDAA2255A57FF00E217F9 /* OWSAddToContactsOfferMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAddToContactsOfferMessage.m; sourceTree = ""; }; + C33FDAA3255A57FF00E217F9 /* OWSAddToContactsOfferMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToContactsOfferMessage.h; sourceTree = ""; }; + C33FDAA4255A57FF00E217F9 /* SSKProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKProto.swift; sourceTree = ""; }; + C33FDAA5255A57FF00E217F9 /* OWSRequestMaker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSRequestMaker.swift; sourceTree = ""; }; + C33FDAA6255A57FF00E217F9 /* OWSLinkedDeviceReadReceipt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSLinkedDeviceReadReceipt.h; sourceTree = ""; }; + C33FDAA7255A57FF00E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+SignedPreKeyStore.h"; sourceTree = ""; }; + C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; + C33FDAA9255A580000E217F9 /* Mnemonic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; + C33FDAAA255A580000E217F9 /* NSObject+Casting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+Casting.m"; sourceTree = ""; }; + C33FDAAB255A580000E217F9 /* OWSWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSWebSocket.m; sourceTree = ""; }; + C33FDAAC255A580000E217F9 /* OWSMessageHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageHandler.h; sourceTree = ""; }; + C33FDAAD255A580000E217F9 /* OWSDeviceProvisioner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDeviceProvisioner.h; sourceTree = ""; }; + C33FDAAE255A580000E217F9 /* OWSReceiptsForSenderMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSReceiptsForSenderMessage.m; sourceTree = ""; }; + C33FDAAF255A580000E217F9 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; + C33FDAB0255A580000E217F9 /* ProvisioningProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProvisioningProto.swift; sourceTree = ""; }; + C33FDAB1255A580000E217F9 /* OWSStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSStorage.m; sourceTree = ""; }; + C33FDAB2255A580000E217F9 /* TSNetworkManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSNetworkManager.m; sourceTree = ""; }; + C33FDAB3255A580000E217F9 /* TSContactThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSContactThread.h; sourceTree = ""; }; + C33FDAB4255A580000E217F9 /* OWSMessageReceiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageReceiver.m; sourceTree = ""; }; + C33FDAB5255A580000E217F9 /* TSNetworkManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSNetworkManager.h; sourceTree = ""; }; + C33FDAB6255A580100E217F9 /* SyncMessagesProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncMessagesProtocol.swift; sourceTree = ""; }; + C33FDAB7255A580100E217F9 /* OWSFailedMessagesJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFailedMessagesJob.m; sourceTree = ""; }; + C33FDAB8255A580100E217F9 /* NSArray+Functional.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Functional.m"; sourceTree = ""; }; + C33FDAB9255A580100E217F9 /* OWSStorage+Subclass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSStorage+Subclass.h"; sourceTree = ""; }; + C33FDABA255A580100E217F9 /* OWSWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSWebSocket.h; sourceTree = ""; }; + C33FDABB255A580100E217F9 /* OWSGroupsOutputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGroupsOutputStream.h; sourceTree = ""; }; + C33FDABD255A580100E217F9 /* OWSOutgoingReceiptManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingReceiptManager.h; sourceTree = ""; }; + C33FDABE255A580100E217F9 /* TSConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSConstants.m; sourceTree = ""; }; + C33FDAC0255A580100E217F9 /* OWSIncomingMessageFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIncomingMessageFinder.h; sourceTree = ""; }; + C33FDAC1255A580100E217F9 /* NSSet+Functional.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSSet+Functional.m"; sourceTree = ""; }; + C33FDAC2255A580200E217F9 /* TSAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttachment.m; sourceTree = ""; }; + C33FDAC3255A580200E217F9 /* OWSDispatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDispatch.m; sourceTree = ""; }; + C33FDAC4255A580200E217F9 /* TSAttachmentStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttachmentStream.m; sourceTree = ""; }; + C33FDAC5255A580200E217F9 /* OWSCountryMetadata.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSCountryMetadata.m; sourceTree = ""; }; + C33FDAC6255A580200E217F9 /* Fingerprint.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fingerprint.pb.swift; sourceTree = ""; }; + C33FDAC7255A580200E217F9 /* OWSSignalService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSignalService.h; sourceTree = ""; }; + C33FDAC8255A580200E217F9 /* OWSCensorshipConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCensorshipConfiguration.h; sourceTree = ""; }; + C33FDAC9255A580200E217F9 /* OWSContact.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContact.h; sourceTree = ""; }; + C33FDACA255A580200E217F9 /* LokiMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LokiMessage.swift; sourceTree = ""; }; + C33FDACB255A580200E217F9 /* CDSSigningCertificate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDSSigningCertificate.m; sourceTree = ""; }; + C33FDACC255A580200E217F9 /* OWSMessageSend.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSMessageSend.swift; sourceTree = ""; }; + C33FDACD255A580200E217F9 /* SSKJobRecord.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SSKJobRecord.m; sourceTree = ""; }; + C33FDACE255A580300E217F9 /* OWSVerificationStateSyncMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSVerificationStateSyncMessage.m; sourceTree = ""; }; + C33FDACF255A580300E217F9 /* OWSAttachmentDownloads.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAttachmentDownloads.h; sourceTree = ""; }; + C33FDAD0255A580300E217F9 /* CDSQuote.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CDSQuote.m; sourceTree = ""; }; + C33FDAD1255A580300E217F9 /* OWSDynamicOutgoingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDynamicOutgoingMessage.m; sourceTree = ""; }; + C33FDAD2255A580300E217F9 /* Contact.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Contact.h; sourceTree = ""; }; + C33FDAD3255A580300E217F9 /* TSThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSThread.h; sourceTree = ""; }; + C33FDAD4255A580300E217F9 /* EncryptionUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionUtilities.swift; sourceTree = ""; }; + C33FDAD5255A580300E217F9 /* TSQuotedMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSQuotedMessage.h; sourceTree = ""; }; + C33FDAD6255A580300E217F9 /* OWSAnalytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAnalytics.h; sourceTree = ""; }; + C33FDAD7255A580300E217F9 /* OWSDeviceProvisioner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDeviceProvisioner.m; sourceTree = ""; }; + C33FDAD8255A580300E217F9 /* OWSGroupsOutputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSGroupsOutputStream.m; sourceTree = ""; }; + C33FDAD9255A580300E217F9 /* OWSDisappearingMessagesConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDisappearingMessagesConfiguration.h; sourceTree = ""; }; + C33FDADA255A580400E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDisappearingConfigurationUpdateInfoMessage.h; sourceTree = ""; }; + C33FDADB255A580400E217F9 /* OWSFailedMessagesJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFailedMessagesJob.h; sourceTree = ""; }; + C33FDADC255A580400E217F9 /* NSObject+Casting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+Casting.h"; sourceTree = ""; }; + C33FDADD255A580400E217F9 /* TSInfoMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSInfoMessage.h; sourceTree = ""; }; + C33FDADE255A580400E217F9 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = ""; }; + C33FDADF255A580400E217F9 /* PublicChatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicChatManager.swift; sourceTree = ""; }; + C33FDAE0255A580400E217F9 /* ByteParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ByteParser.m; sourceTree = ""; }; + C33FDAE1255A580400E217F9 /* OWSReadTracking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSReadTracking.h; sourceTree = ""; }; + C33FDAE2255A580400E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSReadReceiptsForLinkedDevicesMessage.h; sourceTree = ""; }; + C33FDAE3255A580400E217F9 /* TSPrefix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSPrefix.h; sourceTree = ""; }; + C33FDAE4255A580400E217F9 /* TSAttachmentStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAttachmentStream.h; sourceTree = ""; }; + C33FDAE5255A580400E217F9 /* OWSAddToProfileWhitelistOfferMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAddToProfileWhitelistOfferMessage.m; sourceTree = ""; }; + C33FDAE6255A580400E217F9 /* TSInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSInteraction.h; sourceTree = ""; }; + C33FDAE7255A580500E217F9 /* TSErrorMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSErrorMessage.m; sourceTree = ""; }; + C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageUtils.h; sourceTree = ""; }; + C33FDAE9255A580500E217F9 /* SSKMessageSenderJobRecord.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SSKMessageSenderJobRecord.m; sourceTree = ""; }; + C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupFragment.h; sourceTree = ""; }; + C33FDAEB255A580500E217F9 /* OWSDeviceProvisioningCodeService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDeviceProvisioningCodeService.m; sourceTree = ""; }; + C33FDAEC255A580500E217F9 /* SignalRecipient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalRecipient.h; sourceTree = ""; }; + C33FDAED255A580500E217F9 /* SSKMessageSenderJobRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKMessageSenderJobRecord.h; sourceTree = ""; }; + C33FDAEE255A580500E217F9 /* OWSFingerprintBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFingerprintBuilder.m; sourceTree = ""; }; + C33FDAEF255A580500E217F9 /* NSData+Image.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Image.m"; sourceTree = ""; }; + C33FDAF0255A580500E217F9 /* ContactsUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsUpdater.h; sourceTree = ""; }; + C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSThumbnailService.swift; sourceTree = ""; }; + C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxiedContentDownloader.swift; sourceTree = ""; }; + C33FDAF3255A580500E217F9 /* OWSPrimaryStorage+SessionStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+SessionStore.m"; sourceTree = ""; }; + C33FDAF4255A580600E217F9 /* SSKEnvironment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SSKEnvironment.m; sourceTree = ""; }; + C33FDAF5255A580600E217F9 /* OWSAnalyticsEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAnalyticsEvents.h; sourceTree = ""; }; + C33FDAF6255A580600E217F9 /* OWSIncomingSentMessageTranscript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIncomingSentMessageTranscript.h; sourceTree = ""; }; + C33FDAF7255A580600E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+SignedPreKeyStore.m"; sourceTree = ""; }; + C33FDAF8255A580600E217F9 /* OWSSyncGroupsRequestMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSyncGroupsRequestMessage.h; sourceTree = ""; }; + C33FDAF9255A580600E217F9 /* TSContactThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSContactThread.m; sourceTree = ""; }; + C33FDAFA255A580600E217F9 /* LKDeviceLinkMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKDeviceLinkMessage.m; sourceTree = ""; }; + C33FDAFB255A580600E217F9 /* SessionMetaProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionMetaProtocol.swift; sourceTree = ""; }; + C33FDAFC255A580600E217F9 /* MIMETypeUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIMETypeUtil.h; sourceTree = ""; }; + C33FDAFD255A580600E217F9 /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = ""; }; + C33FDAFE255A580600E217F9 /* OWSStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSStorage.h; sourceTree = ""; }; + C33FDAFF255A580600E217F9 /* DisplayNameUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayNameUtilities.swift; sourceTree = ""; }; + C33FDB00255A580600E217F9 /* OWSRequestBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSRequestBuilder.m; sourceTree = ""; }; + C33FDB01255A580700E217F9 /* AppReadiness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppReadiness.h; sourceTree = ""; }; + C33FDB02255A580700E217F9 /* TSCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSCall.h; sourceTree = ""; }; + C33FDB03255A580700E217F9 /* OWSPrimaryStorage+SessionStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+SessionStore.h"; sourceTree = ""; }; + C33FDB05255A580700E217F9 /* OWSFingerprint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFingerprint.m; sourceTree = ""; }; + C33FDB06255A580700E217F9 /* OWSRequestBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSRequestBuilder.h; sourceTree = ""; }; + C33FDB07255A580700E217F9 /* OWSBackupFragment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupFragment.m; sourceTree = ""; }; + C33FDB08255A580700E217F9 /* OWSProfileKeyMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProfileKeyMessage.h; sourceTree = ""; }; + C33FDB09255A580700E217F9 /* NSError+MessageSending.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+MessageSending.m"; sourceTree = ""; }; + C33FDB0A255A580700E217F9 /* TSGroupModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSGroupModel.h; sourceTree = ""; }; + C33FDB0B255A580700E217F9 /* OWSVerificationStateChangeMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSVerificationStateChangeMessage.m; sourceTree = ""; }; + C33FDB0C255A580700E217F9 /* CDSQuote.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSQuote.h; sourceTree = ""; }; + C33FDB0D255A580800E217F9 /* NSArray+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+OWS.m"; sourceTree = ""; }; + C33FDB0E255A580800E217F9 /* NSError+MessageSending.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+MessageSending.h"; sourceTree = ""; }; + C33FDB0F255A580800E217F9 /* DeviceLinkingSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLinkingSession.swift; sourceTree = ""; }; + C33FDB10255A580800E217F9 /* OWSBatchMessageProcessor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBatchMessageProcessor.m; sourceTree = ""; }; + C33FDB12255A580800E217F9 /* NSString+SSK.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+SSK.h"; sourceTree = ""; }; + C33FDB13255A580800E217F9 /* LKUnlinkDeviceMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKUnlinkDeviceMessage.m; sourceTree = ""; }; + C33FDB14255A580800E217F9 /* OWSMath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMath.h; sourceTree = ""; }; + C33FDB16255A580800E217F9 /* OWSDisappearingMessagesConfigurationMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDisappearingMessagesConfigurationMessage.h; sourceTree = ""; }; + C33FDB17255A580800E217F9 /* FunctionalUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FunctionalUtil.m; sourceTree = ""; }; + C33FDB18255A580800E217F9 /* OWSSignalService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSignalService.m; sourceTree = ""; }; + C33FDB19255A580900E217F9 /* GroupUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupUtilities.swift; sourceTree = ""; }; + C33FDB1A255A580900E217F9 /* OWSPrimaryStorage+Calling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+Calling.h"; sourceTree = ""; }; + C33FDB1B255A580900E217F9 /* OWSDeviceProvisioningCodeService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDeviceProvisioningCodeService.h; sourceTree = ""; }; + C33FDB1C255A580900E217F9 /* UIImage+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+OWS.h"; sourceTree = ""; }; + C33FDB1D255A580900E217F9 /* OWSReadReceiptManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSReadReceiptManager.h; sourceTree = ""; }; + C33FDB1E255A580900E217F9 /* OWSIncomingMessageFinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIncomingMessageFinder.m; sourceTree = ""; }; + C33FDB1F255A580900E217F9 /* DeviceLinkingUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLinkingUtilities.swift; sourceTree = ""; }; + C33FDB20255A580900E217F9 /* TSDatabaseSecondaryIndexes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSDatabaseSecondaryIndexes.m; sourceTree = ""; }; + C33FDB21255A580900E217F9 /* OWS2FAManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FAManager.h; sourceTree = ""; }; + C33FDB22255A580900E217F9 /* OWSMediaUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSMediaUtils.swift; sourceTree = ""; }; + C33FDB24255A580900E217F9 /* OWSOutgoingSentMessageTranscript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingSentMessageTranscript.h; sourceTree = ""; }; + C33FDB25255A580900E217F9 /* TSDatabaseSecondaryIndexes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSDatabaseSecondaryIndexes.h; sourceTree = ""; }; + C33FDB26255A580A00E217F9 /* FingerprintProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FingerprintProto.swift; sourceTree = ""; }; + C33FDB27255A580A00E217F9 /* OWSEndSessionMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSEndSessionMessage.m; sourceTree = ""; }; + C33FDB28255A580A00E217F9 /* OWSOutgoingCallMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingCallMessage.m; sourceTree = ""; }; + C33FDB29255A580A00E217F9 /* NSData+Image.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Image.h"; sourceTree = ""; }; + C33FDB2A255A580A00E217F9 /* OWSIncompleteCallsJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIncompleteCallsJob.h; sourceTree = ""; }; + C33FDB2B255A580A00E217F9 /* OWSProvisioningCipher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProvisioningCipher.m; sourceTree = ""; }; + C33FDB2C255A580A00E217F9 /* TSDatabaseView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSDatabaseView.h; sourceTree = ""; }; + C33FDB2D255A580A00E217F9 /* OWSDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDevice.h; sourceTree = ""; }; + C33FDB2E255A580A00E217F9 /* OWSEndSessionMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSEndSessionMessage.h; sourceTree = ""; }; + C33FDB2F255A580A00E217F9 /* ContactsManagerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsManagerProtocol.h; sourceTree = ""; }; + C33FDB30255A580A00E217F9 /* OWSDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDevice.m; sourceTree = ""; }; + C33FDB31255A580A00E217F9 /* SSKEnvironment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKEnvironment.h; sourceTree = ""; }; + C33FDB32255A580A00E217F9 /* SSKIncrementingIdFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKIncrementingIdFinder.swift; sourceTree = ""; }; + C33FDB33255A580B00E217F9 /* Provisioning.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Provisioning.pb.swift; sourceTree = ""; }; + C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosedGroupPoller.swift; sourceTree = ""; }; + C33FDB35255A580B00E217F9 /* OWSContactDiscoveryOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSContactDiscoveryOperation.swift; sourceTree = ""; }; + C33FDB36255A580B00E217F9 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + C33FDB37255A580B00E217F9 /* Storage+SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Storage+SnodeAPI.swift"; sourceTree = ""; }; + C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackgroundTask.h; sourceTree = ""; }; + C33FDB3A255A580B00E217F9 /* Poller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Poller.swift; sourceTree = ""; }; + C33FDB3B255A580B00E217F9 /* NSNotificationCenter+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSNotificationCenter+OWS.h"; sourceTree = ""; }; + C33FDB3D255A580B00E217F9 /* OWSProfileKeyMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProfileKeyMessage.m; sourceTree = ""; }; + C33FDB3E255A580B00E217F9 /* OWSMessageSender.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageSender.m; sourceTree = ""; }; + C33FDB3F255A580C00E217F9 /* String+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SSK.swift"; sourceTree = ""; }; + C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalIOSProto.swift; sourceTree = ""; }; + C33FDB41255A580C00E217F9 /* MIMETypeUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIMETypeUtil.m; sourceTree = ""; }; + C33FDB42255A580C00E217F9 /* OWSCountryMetadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCountryMetadata.h; sourceTree = ""; }; + C33FDB43255A580C00E217F9 /* YapDatabaseConnection+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "YapDatabaseConnection+OWS.m"; sourceTree = ""; }; + C33FDB44255A580C00E217F9 /* OWSContactsOutputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsOutputStream.m; sourceTree = ""; }; + C33FDB45255A580C00E217F9 /* NSString+SSK.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+SSK.m"; sourceTree = ""; }; + C33FDB46255A580C00E217F9 /* TSDatabaseView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSDatabaseView.m; sourceTree = ""; }; + C33FDB47255A580C00E217F9 /* OWSPrimaryStorage+PreKeyStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+PreKeyStore.h"; sourceTree = ""; }; + C33FDB48255A580C00E217F9 /* TSOutgoingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSOutgoingMessage.h; sourceTree = ""; }; + C33FDB49255A580C00E217F9 /* WeakTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakTimer.swift; sourceTree = ""; }; + C33FDB4A255A580C00E217F9 /* SignalServiceClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalServiceClient.swift; sourceTree = ""; }; + C33FDB4B255A580C00E217F9 /* OWSChunkedOutputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSChunkedOutputStream.m; sourceTree = ""; }; + C33FDB4C255A580D00E217F9 /* AppVersion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppVersion.h; sourceTree = ""; }; + C33FDB4F255A580D00E217F9 /* SSKJobRecord.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKJobRecord.h; sourceTree = ""; }; + C33FDB50255A580D00E217F9 /* OWSPrimaryStorage+PreKeyStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+PreKeyStore.m"; sourceTree = ""; }; + C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSUserDefaults+OWS.h"; sourceTree = ""; }; + C33FDB52255A580D00E217F9 /* OWSHTTPSecurityPolicy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSHTTPSecurityPolicy.h; sourceTree = ""; }; + C33FDB53255A580D00E217F9 /* PreKeyBundle+jsonDict.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PreKeyBundle+jsonDict.h"; sourceTree = ""; }; + C33FDB54255A580D00E217F9 /* DataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataSource.h; sourceTree = ""; }; + C33FDB55255A580D00E217F9 /* TSInvalidIdentityKeySendingErrorMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInvalidIdentityKeySendingErrorMessage.m; sourceTree = ""; }; + C33FDB56255A580D00E217F9 /* TSOutgoingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSOutgoingMessage.m; sourceTree = ""; }; + C33FDB57255A580D00E217F9 /* OWSSyncContactsMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSyncContactsMessage.h; sourceTree = ""; }; + C33FDB58255A580E00E217F9 /* OWSPrimaryStorage+Loki.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+Loki.m"; sourceTree = ""; }; + C33FDB59255A580E00E217F9 /* OWSFailedAttachmentDownloadsJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFailedAttachmentDownloadsJob.m; sourceTree = ""; }; + C33FDB5A255A580E00E217F9 /* OWSUDManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSUDManager.swift; sourceTree = ""; }; + C33FDB5B255A580E00E217F9 /* YapDatabaseTransaction+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "YapDatabaseTransaction+OWS.m"; sourceTree = ""; }; + C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Functional.h"; sourceTree = ""; }; + C33FDB5D255A580E00E217F9 /* TSErrorMessage_privateConstructor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSErrorMessage_privateConstructor.h; sourceTree = ""; }; + C33FDB5E255A580E00E217F9 /* ContactParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactParser.swift; sourceTree = ""; }; + C33FDB5F255A580E00E217F9 /* YapDatabaseConnection+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YapDatabaseConnection+OWS.h"; sourceTree = ""; }; + C33FDB60255A580E00E217F9 /* TSMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSMessage.m; sourceTree = ""; }; + C33FDB61255A580E00E217F9 /* LKUnlinkDeviceMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LKUnlinkDeviceMessage.h; sourceTree = ""; }; + C33FDB62255A580E00E217F9 /* OWSProvisioningCipher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProvisioningCipher.h; sourceTree = ""; }; + C33FDB63255A580E00E217F9 /* OWSDynamicOutgoingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDynamicOutgoingMessage.h; sourceTree = ""; }; + C33FDB64255A580E00E217F9 /* PreKeyRefreshOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreKeyRefreshOperation.swift; sourceTree = ""; }; + C33FDB65255A580F00E217F9 /* OWSSyncConfigurationMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSyncConfigurationMessage.h; sourceTree = ""; }; + C33FDB66255A580F00E217F9 /* ContactDiscoveryService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactDiscoveryService.h; sourceTree = ""; }; + C33FDB67255A580F00E217F9 /* OWSMediaGalleryFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMediaGalleryFinder.h; sourceTree = ""; }; + C33FDB68255A580F00E217F9 /* ContentProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentProxy.swift; sourceTree = ""; }; + C33FDB69255A580F00E217F9 /* FeatureFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; + C33FDB6A255A580F00E217F9 /* SignalMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalMessage.swift; sourceTree = ""; }; + C33FDB6B255A580F00E217F9 /* LKUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LKUserDefaults.swift; sourceTree = ""; }; + C33FDB6C255A580F00E217F9 /* NSNotificationCenter+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNotificationCenter+OWS.m"; sourceTree = ""; }; + C33FDB6D255A580F00E217F9 /* TSPreKeyManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSPreKeyManager.h; sourceTree = ""; }; + C33FDB6E255A580F00E217F9 /* SSKProtoPrekeyBundleMessage+Loki.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SSKProtoPrekeyBundleMessage+Loki.swift"; sourceTree = ""; }; + C33FDB6F255A580F00E217F9 /* OWSOutgoingReceiptManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingReceiptManager.m; sourceTree = ""; }; + C33FDB70255A580F00E217F9 /* OWSMessageServiceParams.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageServiceParams.m; sourceTree = ""; }; + C33FDB71255A581000E217F9 /* OWSMediaGalleryFinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMediaGalleryFinder.m; sourceTree = ""; }; + C33FDB72255A581000E217F9 /* DeviceLinkIndex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLinkIndex.swift; sourceTree = ""; }; + C33FDB73255A581000E217F9 /* TSGroupModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSGroupModel.m; sourceTree = ""; }; + C33FDB74255A581000E217F9 /* TSInvalidIdentityKeyErrorMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInvalidIdentityKeyErrorMessage.m; sourceTree = ""; }; + C33FDB75255A581000E217F9 /* AppReadiness.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppReadiness.m; sourceTree = ""; }; + C33FDB76255A581000E217F9 /* ClosedGroupUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosedGroupUtilities.swift; sourceTree = ""; }; + C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSUserDefaults+OWS.m"; sourceTree = ""; }; + C33FDB78255A581000E217F9 /* OWSOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOperation.m; sourceTree = ""; }; + C33FDB79255A581000E217F9 /* PhoneNumberUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhoneNumberUtil.m; sourceTree = ""; }; + C33FDB7A255A581000E217F9 /* NotificationsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationsProtocol.h; sourceTree = ""; }; + C33FDB7C255A581000E217F9 /* OWSVerificationStateChangeMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSVerificationStateChangeMessage.h; sourceTree = ""; }; + C33FDB7D255A581100E217F9 /* OWSMessageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageManager.h; sourceTree = ""; }; + C33FDB7E255A581100E217F9 /* TSPreKeyManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSPreKeyManager.m; sourceTree = ""; }; + C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullTextSearchFinder.swift; sourceTree = ""; }; + C33FDB80255A581100E217F9 /* Notification+Loki.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+Loki.swift"; sourceTree = ""; }; + C33FDB81255A581100E217F9 /* UIImage+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+OWS.m"; sourceTree = ""; }; + C33FDB82255A581100E217F9 /* MessageWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageWrapper.swift; sourceTree = ""; }; + C33FDB83255A581100E217F9 /* TSQuotedMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSQuotedMessage.m; sourceTree = ""; }; + C33FDB84255A581100E217F9 /* OWSIncomingSentMessageTranscript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIncomingSentMessageTranscript.m; sourceTree = ""; }; + C33FDB85255A581100E217F9 /* AppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppContext.m; sourceTree = ""; }; + C33FDB86255A581100E217F9 /* OWSRequestFactory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSRequestFactory.m; sourceTree = ""; }; + C33FDB87255A581100E217F9 /* JobQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JobQueue.swift; sourceTree = ""; }; + C33FDB88255A581200E217F9 /* TSAccountManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAccountManager.m; sourceTree = ""; }; + C33FDB89255A581200E217F9 /* FileServerAPI+Deprecated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FileServerAPI+Deprecated.swift"; sourceTree = ""; }; + C33FDB8A255A581200E217F9 /* AppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppContext.h; sourceTree = ""; }; + C33FDB8B255A581200E217F9 /* Storage+SessionManagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Storage+SessionManagement.swift"; sourceTree = ""; }; + C33FDB8C255A581200E217F9 /* TSInvalidIdentityKeyErrorMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSInvalidIdentityKeyErrorMessage.h; sourceTree = ""; }; + C33FDB8D255A581200E217F9 /* DeviceLinkingSessionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLinkingSessionDelegate.swift; sourceTree = ""; }; + C33FDB8E255A581200E217F9 /* OWSContact+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSContact+Private.h"; sourceTree = ""; }; + C33FDB8F255A581200E217F9 /* ParamParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParamParser.swift; sourceTree = ""; }; + C33FDB90255A581200E217F9 /* OWSMessageDecrypter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageDecrypter.m; sourceTree = ""; }; + C33FDB91255A581200E217F9 /* ProtoUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProtoUtils.h; sourceTree = ""; }; + C33FDB92255A581200E217F9 /* OWSDeviceProvisioningService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDeviceProvisioningService.m; sourceTree = ""; }; + C33FDB93255A581200E217F9 /* PreKeyBundle+jsonDict.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "PreKeyBundle+jsonDict.m"; sourceTree = ""; }; + C33FDB94255A581300E217F9 /* TSAccountManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAccountManager.h; sourceTree = ""; }; + C33FDB95255A581300E217F9 /* Storage+Collections.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Storage+Collections.swift"; sourceTree = ""; }; + C33FDB96255A581300E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSReadReceiptsForLinkedDevicesMessage.m; sourceTree = ""; }; + C33FDB97255A581300E217F9 /* OWSDisappearingMessagesConfigurationMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingMessagesConfigurationMessage.m; sourceTree = ""; }; + C33FDB98255A581300E217F9 /* DeviceNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceNames.swift; sourceTree = ""; }; + C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+keyFromIntLong.m"; sourceTree = ""; }; + C33FDB9A255A581300E217F9 /* OWS2FAManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS2FAManager.m; sourceTree = ""; }; + C33FDB9B255A581300E217F9 /* MessageSenderJobQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSenderJobQueue.swift; sourceTree = ""; }; + C33FDB9C255A581300E217F9 /* TSIncomingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSIncomingMessage.h; sourceTree = ""; }; + C33FDB9D255A581300E217F9 /* OWSCallMessageHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCallMessageHandler.h; sourceTree = ""; }; + C33FDB9E255A581400E217F9 /* TSAttachmentPointer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttachmentPointer.m; sourceTree = ""; }; + C33FDB9F255A581400E217F9 /* TTLUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TTLUtilities.swift; sourceTree = ""; }; + C33FDBA0255A581400E217F9 /* TSStorageHeaders.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSStorageHeaders.h; sourceTree = ""; }; + C33FDBA1255A581400E217F9 /* OWSOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOperation.h; sourceTree = ""; }; + C33FDBA2255A581400E217F9 /* PhoneNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhoneNumber.h; sourceTree = ""; }; + C33FDBA3255A581400E217F9 /* SessionManagementProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionManagementProtocol.swift; sourceTree = ""; }; + C33FDBA4255A581400E217F9 /* OWSDisappearingMessagesConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingMessagesConfiguration.m; sourceTree = ""; }; + C33FDBA5255A581400E217F9 /* SignalServiceProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalServiceProfile.swift; sourceTree = ""; }; + C33FDBA6255A581400E217F9 /* TSInvalidIdentityKeySendingErrorMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSInvalidIdentityKeySendingErrorMessage.h; sourceTree = ""; }; + C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSLinkPreview.swift; sourceTree = ""; }; + C33FDBA9255A581500E217F9 /* OWSIdentityManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIdentityManager.m; sourceTree = ""; }; + C33FDBAB255A581500E217F9 /* OWSFileSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFileSystem.h; sourceTree = ""; }; + C33FDBAC255A581500E217F9 /* Data+SecureRandom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+SecureRandom.swift"; sourceTree = ""; }; + C33FDBAD255A581500E217F9 /* OWSRecordTranscriptJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSRecordTranscriptJob.h; sourceTree = ""; }; + C33FDBAE255A581500E217F9 /* SignalAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalAccount.h; sourceTree = ""; }; + C33FDBAF255A581500E217F9 /* OWSAnalyticsEvents.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAnalyticsEvents.m; sourceTree = ""; }; + C33FDBB0255A581500E217F9 /* TSErrorMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSErrorMessage.h; sourceTree = ""; }; + C33FDBB1255A581500E217F9 /* TSSocketManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSSocketManager.h; sourceTree = ""; }; + C33FDBB3255A581500E217F9 /* OWSOutgoingNullMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingNullMessage.h; sourceTree = ""; }; + C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLSessionDataTask+StatusCode.m"; sourceTree = ""; }; + C33FDBB5255A581600E217F9 /* OWSSyncGroupsMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSyncGroupsMessage.h; sourceTree = ""; }; + C33FDBB6255A581600E217F9 /* DataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataSource.m; sourceTree = ""; }; + C33FDBB7255A581600E217F9 /* SignalRecipient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalRecipient.m; sourceTree = ""; }; + C33FDBB8255A581600E217F9 /* TSThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSThread.m; sourceTree = ""; }; + C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileManagerProtocol.h; sourceTree = ""; }; + C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+keyFromIntLong.h"; sourceTree = ""; }; + C33FDBBB255A581600E217F9 /* OWSPrimaryStorage+Loki.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+Loki.h"; sourceTree = ""; }; + C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKKeychainStorage.swift; sourceTree = ""; }; + C33FDBBD255A581600E217F9 /* OWSAddToProfileWhitelistOfferMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToProfileWhitelistOfferMessage.h; sourceTree = ""; }; + C33FDBBF255A581700E217F9 /* OWSHTTPSecurityPolicy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSHTTPSecurityPolicy.m; sourceTree = ""; }; + C33FDBC0255A581700E217F9 /* OWSRequestFactory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSRequestFactory.h; sourceTree = ""; }; + C33FDBC1255A581700E217F9 /* GeneralUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralUtilities.swift; sourceTree = ""; }; + C33FDBC2255A581700E217F9 /* SSKAsserts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKAsserts.h; sourceTree = ""; }; + C33FDBC3255A581700E217F9 /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = ""; }; + C33FDBC4255A581700E217F9 /* TypingIndicatorMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorMessage.swift; sourceTree = ""; }; + C33FDBC6255A581700E217F9 /* OWSMessageReceiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageReceiver.h; sourceTree = ""; }; + C33FDBC8255A581700E217F9 /* OWSFingerprint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFingerprint.h; sourceTree = ""; }; + C33FDBC9255A581700E217F9 /* CreatePreKeysOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePreKeysOperation.swift; sourceTree = ""; }; + C33FDBCA255A581700E217F9 /* LKGroupUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LKGroupUtilities.h; sourceTree = ""; }; + C33FDBCB255A581800E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInvalidIdentityKeyReceivingErrorMessage.m; sourceTree = ""; }; + C33FDBCC255A581800E217F9 /* MultiDeviceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiDeviceProtocol.swift; sourceTree = ""; }; + C33FDBCE255A581800E217F9 /* OWSMessageServiceParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageServiceParams.h; sourceTree = ""; }; + C33FDBCF255A581800E217F9 /* OWSFingerprintBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFingerprintBuilder.h; sourceTree = ""; }; + C33FDBD0255A581800E217F9 /* OnionRequestAPI+Encryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OnionRequestAPI+Encryption.swift"; sourceTree = ""; }; + C33FDBD1255A581800E217F9 /* DeviceLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceLink.swift; sourceTree = ""; }; + C33FDBD2255A581800E217F9 /* OWSUnknownContactBlockOfferMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnknownContactBlockOfferMessage.h; sourceTree = ""; }; + C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSignalAddress.swift; sourceTree = ""; }; + C33FDBD4255A581900E217F9 /* OWSBatchMessageProcessor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBatchMessageProcessor.h; sourceTree = ""; }; + C33FDBD5255A581900E217F9 /* OWSOutgoingSentMessageTranscript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingSentMessageTranscript.m; sourceTree = ""; }; + C33FDBD6255A581900E217F9 /* OWSUploadOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUploadOperation.h; sourceTree = ""; }; + C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageUtils.m; sourceTree = ""; }; + C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalIOS.pb.swift; sourceTree = ""; }; + C33FDBD9255A581900E217F9 /* OWSVerificationStateSyncMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSVerificationStateSyncMessage.h; sourceTree = ""; }; + C33FDBDA255A581900E217F9 /* Dictionary+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Description.swift"; sourceTree = ""; }; + C33FDBDB255A581900E217F9 /* OWSDevicesService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDevicesService.h; sourceTree = ""; }; + C33FDBDC255A581900E217F9 /* ContactDiscoveryService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactDiscoveryService.m; sourceTree = ""; }; + C33FDBDD255A581900E217F9 /* OWSDisappearingMessagesJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingMessagesJob.m; sourceTree = ""; }; + C33FDBDE255A581900E217F9 /* LokiPushNotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LokiPushNotificationManager.swift; sourceTree = ""; }; + C33FDBDF255A581A00E217F9 /* LKSyncOpenGroupsMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKSyncOpenGroupsMessage.m; sourceTree = ""; }; + C33FDBE0255A581A00E217F9 /* OWSBlockedPhoneNumbersMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBlockedPhoneNumbersMessage.m; sourceTree = ""; }; + C33FDBE1255A581A00E217F9 /* LKGroupUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKGroupUtilities.m; sourceTree = ""; }; + C33FDBE2255A581A00E217F9 /* OWSUnknownContactBlockOfferMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnknownContactBlockOfferMessage.m; sourceTree = ""; }; + C33FDBE3255A581A00E217F9 /* CDSSigningCertificate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CDSSigningCertificate.h; sourceTree = ""; }; + C33FDBE4255A581A00E217F9 /* OWSOutgoingSyncMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingSyncMessage.h; sourceTree = ""; }; + C33FDBE5255A581A00E217F9 /* OWSDevicesService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDevicesService.m; sourceTree = ""; }; + C33FDBE6255A581A00E217F9 /* OWSOutgoingNullMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingNullMessage.m; sourceTree = ""; }; + C33FDBE7255A581A00E217F9 /* NSTimer+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSTimer+OWS.h"; sourceTree = ""; }; + C33FDBE8255A581A00E217F9 /* Storage+OnionRequests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Storage+OnionRequests.swift"; sourceTree = ""; }; + C33FDBE9255A581A00E217F9 /* TSInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInteraction.m; sourceTree = ""; }; + C33FDBEA255A581A00E217F9 /* SessionRequestMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionRequestMessage.swift; sourceTree = ""; }; + C33FDBEB255A581B00E217F9 /* OWSBlockingManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBlockingManager.h; sourceTree = ""; }; + C33FDBEC255A581B00E217F9 /* OWSRecipientIdentity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSRecipientIdentity.m; sourceTree = ""; }; + C33FDBED255A581B00E217F9 /* ClosedGroupParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosedGroupParser.swift; sourceTree = ""; }; + C33FDBEE255A581B00E217F9 /* ClosedGroupUpdateMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosedGroupUpdateMessage.swift; sourceTree = ""; }; + C33FDBEF255A581B00E217F9 /* TSStorageKeys.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSStorageKeys.h; sourceTree = ""; }; + C33FDBF0255A581B00E217F9 /* LokiDatabaseUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LokiDatabaseUtilities.swift; sourceTree = ""; }; + C33FDBF1255A581B00E217F9 /* OWSIdentityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIdentityManager.h; sourceTree = ""; }; + C33FDBF2255A581B00E217F9 /* MessageSender+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Promise.swift"; sourceTree = ""; }; + C33FDBF3255A581B00E217F9 /* OWSBlockedPhoneNumbersMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBlockedPhoneNumbersMessage.h; sourceTree = ""; }; + C33FDBF4255A581B00E217F9 /* DisplayNameUtilities2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayNameUtilities2.swift; sourceTree = ""; }; + C33FDBF5255A581B00E217F9 /* OWSDeviceProvisioningService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDeviceProvisioningService.h; sourceTree = ""; }; + C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLSessionDataTask+StatusCode.h"; sourceTree = ""; }; + C33FDBF7255A581C00E217F9 /* OWSIncompleteCallsJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIncompleteCallsJob.m; sourceTree = ""; }; + C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+OWS.h"; sourceTree = ""; }; + C33FDBF9255A581C00E217F9 /* OWSError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSError.h; sourceTree = ""; }; + C33FDBFA255A581C00E217F9 /* PhoneNumberUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhoneNumberUtil.h; sourceTree = ""; }; + C33FDBFB255A581C00E217F9 /* Storage+PublicChats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Storage+PublicChats.swift"; sourceTree = ""; }; + C33FDBFC255A581C00E217F9 /* OWSMessageHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageHandler.m; sourceTree = ""; }; + C33FDBFD255A581C00E217F9 /* OWSPrimaryStorage+Calling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+Calling.m"; sourceTree = ""; }; + C33FDBFE255A581C00E217F9 /* NSSet+Functional.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSSet+Functional.h"; sourceTree = ""; }; + C33FDBFF255A581C00E217F9 /* OWSOutgoingSyncMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingSyncMessage.m; sourceTree = ""; }; + C33FDC00255A581C00E217F9 /* OWSSyncGroupsMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSyncGroupsMessage.m; sourceTree = ""; }; + C33FDC01255A581C00E217F9 /* TSGroupThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSGroupThread.m; sourceTree = ""; }; + C33FDC02255A581D00E217F9 /* OWSPrimaryStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSPrimaryStorage.m; sourceTree = ""; }; + C33FDC03255A581D00E217F9 /* ByteParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ByteParser.h; sourceTree = ""; }; + C33FDC04255A581D00E217F9 /* DecryptionUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecryptionUtilities.swift; sourceTree = ""; }; + C33FDC05255A581D00E217F9 /* OWSDisappearingMessagesFinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDisappearingMessagesFinder.h; sourceTree = ""; }; + C33FDC06255A581D00E217F9 /* SignalAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalAccount.m; sourceTree = ""; }; + C33FDC07255A581D00E217F9 /* Storage+ClosedGroups.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Storage+ClosedGroups.swift"; sourceTree = ""; }; + C33FDC08255A581D00E217F9 /* SignalService.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalService.pb.swift; sourceTree = ""; }; + C33FDC09255A581D00E217F9 /* AccountServiceClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountServiceClient.swift; sourceTree = ""; }; + C33FDC0A255A581D00E217F9 /* OWSContact.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContact.m; sourceTree = ""; }; + C33FDC0B255A581D00E217F9 /* OWSError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSError.m; sourceTree = ""; }; + C33FDC0C255A581E00E217F9 /* TSInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInfoMessage.m; sourceTree = ""; }; + C33FDC0D255A581E00E217F9 /* OWSProvisioningMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProvisioningMessage.h; sourceTree = ""; }; + C33FDC0E255A581E00E217F9 /* OWSMessageDecrypter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageDecrypter.h; sourceTree = ""; }; + C33FDC0F255A581E00E217F9 /* LKDeviceLinkMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LKDeviceLinkMessage.h; sourceTree = ""; }; + C33FDC10255A581E00E217F9 /* ProofOfWork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = ""; }; + C33FDC11255A581E00E217F9 /* TSSocketManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSSocketManager.m; sourceTree = ""; }; + C33FDC12255A581E00E217F9 /* TSConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSConstants.h; sourceTree = ""; }; + C33FDC13255A581E00E217F9 /* OWSAttachmentDownloads.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAttachmentDownloads.m; sourceTree = ""; }; + C33FDC15255A581E00E217F9 /* TSAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAttachment.h; sourceTree = ""; }; + C33FDC16255A581E00E217F9 /* FunctionalUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FunctionalUtil.h; sourceTree = ""; }; + C33FDC17255A581F00E217F9 /* SharedSenderKeysImplementation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedSenderKeysImplementation.swift; sourceTree = ""; }; + C33FDC18255A581F00E217F9 /* TSAttachmentPointer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAttachmentPointer.h; sourceTree = ""; }; + C33FDC19255A581F00E217F9 /* OWSQueues.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQueues.h; sourceTree = ""; }; + C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackgroundTask.m; sourceTree = ""; }; + C33FDC1C255A581F00E217F9 /* TSCall.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSCall.m; sourceTree = ""; }; + C33FDC1D255A581F00E217F9 /* OWSSyncConfigurationMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSyncConfigurationMessage.m; sourceTree = ""; }; + C33FDC1E255A581F00E217F9 /* OWSUploadOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUploadOperation.m; sourceTree = ""; }; + C33FDC1F255A581F00E217F9 /* LokiSessionResetImplementation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LokiSessionResetImplementation.swift; sourceTree = ""; }; C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+Encryption.swift"; sourceTree = ""; }; C3471F4125553A4D00297E91 /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; C3471F4B25553AB000297E91 /* MessageReceiver+Decryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Decryption.swift"; sourceTree = ""; }; @@ -1686,6 +2551,8 @@ C364534F252449260045C478 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = ""; }; C364535B252467900045C478 /* AudioUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioUtilities.swift; sourceTree = ""; }; C369549C24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiDeviceRemovalSheet.swift; sourceTree = ""; }; + C38EEF09255B49A8007E1867 /* SSKProtoEnvelope+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SSKProtoEnvelope+Conversion.swift"; sourceTree = ""; }; + C38EEFD5255B5BA2007E1867 /* OldSnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OldSnodeAPI.swift; sourceTree = ""; }; C396469C2509D3ED00B0B9F5 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = translations/pl.lproj/Localizable.strings; sourceTree = ""; }; C396469D2509D3F400B0B9F5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = translations/ja.lproj/Localizable.strings; sourceTree = ""; }; C396469E2509D40400B0B9F5 /* vi-VN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "vi-VN"; path = "translations/vi-VN.lproj/Localizable.strings"; sourceTree = ""; }; @@ -1860,10 +2727,8 @@ C3C2A9E52553B9C300C340D1 /* NSDate+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDate+OWS.h"; path = "Utility/NSDate+OWS.h"; sourceTree = ""; }; C3C2A9E62553B9C300C340D1 /* Threading.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Threading.m; path = Utility/Threading.m; sourceTree = ""; }; C3C2A9E72553B9C300C340D1 /* Cryptography.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Cryptography.h; path = Utility/Cryptography.h; sourceTree = ""; }; - C3C2A9E82553B9C300C340D1 /* Randomness.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Randomness.m; path = Utility/Randomness.m; sourceTree = ""; }; C3C2A9E92553B9C300C340D1 /* OWSAsserts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAsserts.h; path = Utility/OWSAsserts.h; sourceTree = ""; }; C3C2A9EA2553B9C300C340D1 /* SCKExceptionWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SCKExceptionWrapper.m; path = Utility/SCKExceptionWrapper.m; sourceTree = ""; }; - C3C2A9EB2553B9C400C340D1 /* Randomness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Randomness.h; path = Utility/Randomness.h; sourceTree = ""; }; C3C2A9EC2553B9C400C340D1 /* Cryptography.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Cryptography.m; path = Utility/Cryptography.m; sourceTree = ""; }; C3C2A9ED2553B9C400C340D1 /* NSString+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSString+OWS.m"; path = "Utility/NSString+OWS.m"; sourceTree = ""; }; C3C2A9EE2553B9C400C340D1 /* NSObject+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSObject+OWS.h"; path = "Utility/NSObject+OWS.h"; sourceTree = ""; }; @@ -1881,6 +2746,7 @@ C3DFFAC723E970080058DAF8 /* OpenGroupSuggestionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSuggestionSheet.swift; sourceTree = ""; }; C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditClosedGroupVC.swift; sourceTree = ""; }; C3E7134E251C867C009649BB /* Sodium+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Conversion.swift"; sourceTree = ""; }; + C88965DE4F4EC4FC919BEC4E /* Pods-SessionUIKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.debug.xcconfig"; sourceTree = ""; }; D17BB5C25D615AB49813100C /* Pods_Signal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Signal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; @@ -1945,6 +2811,19 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 10AC6C7D50A0C865C5E4779B /* Pods_SessionUIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C33FD9A8255A548A00E217F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */, + C33FD9C3255A54EF00E217F9 /* SessionProtocolKit.framework in Frameworks */, + C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */, + C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */, + B3E0C9C6F1633B1ABCE5AD0B /* Pods_SignalUtilitiesKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2014,6 +2893,7 @@ C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */, D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */, D2179CFC16BB0B3A0006F3AB /* CoreTelephony.framework in Frameworks */, + C33FD9B2255A548A00E217F9 /* SignalUtilitiesKit.framework in Frameworks */, D221A08E169C9E5E00537ABF /* UIKit.framework in Frameworks */, D221A090169C9E5E00537ABF /* Foundation.framework in Frameworks */, D221A0E8169DFFC500537ABF /* AVFoundation.framework in Frameworks */, @@ -3030,6 +3910,10 @@ 174BD0AE74771D02DAC2B7A9 /* Pods-SessionProtocolKit.app store release.xcconfig */, 264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */, 7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */, + C88965DE4F4EC4FC919BEC4E /* Pods-SessionUIKit.debug.xcconfig */, + C1A746BC424B531D8ED478F6 /* Pods-SessionUIKit.app store release.xcconfig */, + 5F3070F3395081DD0EB4F933 /* Pods-SignalUtilitiesKit.debug.xcconfig */, + 9C0469AC557930C01552CC83 /* Pods-SignalUtilitiesKit.app store release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -3397,6 +4281,445 @@ path = Components; sourceTree = ""; }; + C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */ = { + isa = PBXGroup; + children = ( + C33FDC09255A581D00E217F9 /* AccountServiceClient.swift */, + C33FDBC3255A581700E217F9 /* AnyPromise+Conversion.swift */, + C33FDB8A255A581200E217F9 /* AppContext.h */, + C33FDB85255A581100E217F9 /* AppContext.m */, + C33FDB01255A580700E217F9 /* AppReadiness.h */, + C33FDB75255A581000E217F9 /* AppReadiness.m */, + C33FDB4C255A580D00E217F9 /* AppVersion.h */, + C33FDA8B255A57FD00E217F9 /* AppVersion.m */, + C33FDA6E255A57FA00E217F9 /* Array+Description.swift */, + C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, + C33FDC03255A581D00E217F9 /* ByteParser.h */, + C33FDAE0255A580400E217F9 /* ByteParser.m */, + C33FDB0C255A580700E217F9 /* CDSQuote.h */, + C33FDAD0255A580300E217F9 /* CDSQuote.m */, + C33FDBE3255A581A00E217F9 /* CDSSigningCertificate.h */, + C33FDACB255A580200E217F9 /* CDSSigningCertificate.m */, + C33FDBED255A581B00E217F9 /* ClosedGroupParser.swift */, + C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */, + C33FDA74255A57FB00E217F9 /* ClosedGroupsProtocol.swift */, + C33FDBEE255A581B00E217F9 /* ClosedGroupUpdateMessage.swift */, + C33FDB76255A581000E217F9 /* ClosedGroupUtilities.swift */, + C33FDAD2255A580300E217F9 /* Contact.h */, + C33FDA77255A57FB00E217F9 /* Contact.m */, + C33FDB66255A580F00E217F9 /* ContactDiscoveryService.h */, + C33FDBDC255A581900E217F9 /* ContactDiscoveryService.m */, + C33FDB5E255A580E00E217F9 /* ContactParser.swift */, + C33FDB2F255A580A00E217F9 /* ContactsManagerProtocol.h */, + C33FDAF0255A580500E217F9 /* ContactsUpdater.h */, + C33FDA84255A57FC00E217F9 /* ContactsUpdater.m */, + C33FDB68255A580F00E217F9 /* ContentProxy.swift */, + C33FDBC9255A581700E217F9 /* CreatePreKeysOperation.swift */, + C33FDBAC255A581500E217F9 /* Data+SecureRandom.swift */, + C33FDA94255A57FE00E217F9 /* Data+Streaming.swift */, + C33FDB54255A580D00E217F9 /* DataSource.h */, + C33FDBB6255A581600E217F9 /* DataSource.m */, + C33FDA7C255A57FB00E217F9 /* Debugging.swift */, + C33FDC04255A581D00E217F9 /* DecryptionUtilities.swift */, + C33FDBD1255A581800E217F9 /* DeviceLink.swift */, + C33FDB72255A581000E217F9 /* DeviceLinkIndex.swift */, + C33FDB0F255A580800E217F9 /* DeviceLinkingSession.swift */, + C33FDB8D255A581200E217F9 /* DeviceLinkingSessionDelegate.swift */, + C33FDB1F255A580900E217F9 /* DeviceLinkingUtilities.swift */, + C33FDB98255A581300E217F9 /* DeviceNames.swift */, + C33FDBDA255A581900E217F9 /* Dictionary+Description.swift */, + C33FDAFF255A580600E217F9 /* DisplayNameUtilities.swift */, + C33FDBF4255A581B00E217F9 /* DisplayNameUtilities2.swift */, + C33FDA73255A57FA00E217F9 /* ECKeyPair+Hexadecimal.swift */, + C33FDAD4255A580300E217F9 /* EncryptionUtilities.swift */, + C33FDB69255A580F00E217F9 /* FeatureFlags.swift */, + C33FDB89255A581200E217F9 /* FileServerAPI+Deprecated.swift */, + C33FDAC6255A580200E217F9 /* Fingerprint.pb.swift */, + C33FDB26255A580A00E217F9 /* FingerprintProto.swift */, + C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */, + C33FDC16255A581E00E217F9 /* FunctionalUtil.h */, + C33FDB17255A580800E217F9 /* FunctionalUtil.m */, + C33FDBC1255A581700E217F9 /* GeneralUtilities.swift */, + C33FDB19255A580900E217F9 /* GroupUtilities.swift */, + C33FDB87255A581100E217F9 /* JobQueue.swift */, + C33FDC0F255A581E00E217F9 /* LKDeviceLinkMessage.h */, + C33FDAFA255A580600E217F9 /* LKDeviceLinkMessage.m */, + C33FDBCA255A581700E217F9 /* LKGroupUtilities.h */, + C33FDBE1255A581A00E217F9 /* LKGroupUtilities.m */, + C33FDA91255A57FD00E217F9 /* LKSyncOpenGroupsMessage.h */, + C33FDBDF255A581A00E217F9 /* LKSyncOpenGroupsMessage.m */, + C33FDB61255A580E00E217F9 /* LKUnlinkDeviceMessage.h */, + C33FDB13255A580800E217F9 /* LKUnlinkDeviceMessage.m */, + C33FDB6B255A580F00E217F9 /* LKUserDefaults.swift */, + C33FDBF0255A581B00E217F9 /* LokiDatabaseUtilities.swift */, + C33FDACA255A580200E217F9 /* LokiMessage.swift */, + C33FDBDE255A581900E217F9 /* LokiPushNotificationManager.swift */, + C33FDC1F255A581F00E217F9 /* LokiSessionResetImplementation.swift */, + C33FDAFD255A580600E217F9 /* LRUCache.swift */, + C33FDA7E255A57FB00E217F9 /* Mention.swift */, + C33FDA81255A57FC00E217F9 /* MentionsManager.swift */, + C33FDBF2255A581B00E217F9 /* MessageSender+Promise.swift */, + C33FDB9B255A581300E217F9 /* MessageSenderJobQueue.swift */, + C33FDB82255A581100E217F9 /* MessageWrapper.swift */, + C33FDAFC255A580600E217F9 /* MIMETypeUtil.h */, + C33FDB41255A580C00E217F9 /* MIMETypeUtil.m */, + C33FDAA9255A580000E217F9 /* Mnemonic.swift */, + C33FDBCC255A581800E217F9 /* MultiDeviceProtocol.swift */, + C33FDA9F255A57FF00E217F9 /* NetworkManager.swift */, + C33FDB80255A581100E217F9 /* Notification+Loki.swift */, + C33FDB7A255A581000E217F9 /* NotificationsProtocol.h */, + C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */, + C33FDAB8255A580100E217F9 /* NSArray+Functional.m */, + C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */, + C33FDB0D255A580800E217F9 /* NSArray+OWS.m */, + C33FDB29255A580A00E217F9 /* NSData+Image.h */, + C33FDAEF255A580500E217F9 /* NSData+Image.m */, + C33FDB0E255A580800E217F9 /* NSError+MessageSending.h */, + C33FDB09255A580700E217F9 /* NSError+MessageSending.m */, + C33FDB3B255A580B00E217F9 /* NSNotificationCenter+OWS.h */, + C33FDB6C255A580F00E217F9 /* NSNotificationCenter+OWS.m */, + C33FDADC255A580400E217F9 /* NSObject+Casting.h */, + C33FDAAA255A580000E217F9 /* NSObject+Casting.m */, + C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */, + C33FDBFE255A581C00E217F9 /* NSSet+Functional.h */, + C33FDAC1255A580100E217F9 /* NSSet+Functional.m */, + C33FDB12255A580800E217F9 /* NSString+SSK.h */, + C33FDB45255A580C00E217F9 /* NSString+SSK.m */, + C33FDBE7255A581A00E217F9 /* NSTimer+OWS.h */, + C33FDA8F255A57FD00E217F9 /* NSTimer+OWS.m */, + C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */, + C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */, + C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */, + C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */, + C33FDBD0255A581800E217F9 /* OnionRequestAPI+Encryption.swift */, + C33FDA99255A57FE00E217F9 /* OutageDetection.swift */, + C33FDB21255A580900E217F9 /* OWS2FAManager.h */, + C33FDB9A255A581300E217F9 /* OWS2FAManager.m */, + C33FDAA3255A57FF00E217F9 /* OWSAddToContactsOfferMessage.h */, + C33FDAA2255A57FF00E217F9 /* OWSAddToContactsOfferMessage.m */, + C33FDBBD255A581600E217F9 /* OWSAddToProfileWhitelistOfferMessage.h */, + C33FDAE5255A580400E217F9 /* OWSAddToProfileWhitelistOfferMessage.m */, + C33FDAD6255A580300E217F9 /* OWSAnalytics.h */, + C33FDA89255A57FD00E217F9 /* OWSAnalytics.m */, + C33FDAF5255A580600E217F9 /* OWSAnalyticsEvents.h */, + C33FDBAF255A581500E217F9 /* OWSAnalyticsEvents.m */, + C33FDACF255A580300E217F9 /* OWSAttachmentDownloads.h */, + C33FDC13255A581E00E217F9 /* OWSAttachmentDownloads.m */, + C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */, + C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */, + C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */, + C33FDB07255A580700E217F9 /* OWSBackupFragment.m */, + C33FDBD4255A581900E217F9 /* OWSBatchMessageProcessor.h */, + C33FDB10255A580800E217F9 /* OWSBatchMessageProcessor.m */, + C33FDBF3255A581B00E217F9 /* OWSBlockedPhoneNumbersMessage.h */, + C33FDBE0255A581A00E217F9 /* OWSBlockedPhoneNumbersMessage.m */, + C33FDBEB255A581B00E217F9 /* OWSBlockingManager.h */, + C33FDA68255A57F900E217F9 /* OWSBlockingManager.m */, + C33FDB9D255A581300E217F9 /* OWSCallMessageHandler.h */, + C33FDAC8255A580200E217F9 /* OWSCensorshipConfiguration.h */, + C33FDA7D255A57FB00E217F9 /* OWSCensorshipConfiguration.m */, + C33FDA95255A57FE00E217F9 /* OWSChunkedOutputStream.h */, + C33FDB4B255A580C00E217F9 /* OWSChunkedOutputStream.m */, + C33FDAC9255A580200E217F9 /* OWSContact.h */, + C33FDC0A255A581D00E217F9 /* OWSContact.m */, + C33FDB8E255A581200E217F9 /* OWSContact+Private.h */, + C33FDB35255A580B00E217F9 /* OWSContactDiscoveryOperation.swift */, + C33FDA9D255A57FF00E217F9 /* OWSContactsOutputStream.h */, + C33FDB44255A580C00E217F9 /* OWSContactsOutputStream.m */, + C33FDB42255A580C00E217F9 /* OWSCountryMetadata.h */, + C33FDAC5255A580200E217F9 /* OWSCountryMetadata.m */, + C33FDB2D255A580A00E217F9 /* OWSDevice.h */, + C33FDB30255A580A00E217F9 /* OWSDevice.m */, + C33FDAAD255A580000E217F9 /* OWSDeviceProvisioner.h */, + C33FDAD7255A580300E217F9 /* OWSDeviceProvisioner.m */, + C33FDB1B255A580900E217F9 /* OWSDeviceProvisioningCodeService.h */, + C33FDAEB255A580500E217F9 /* OWSDeviceProvisioningCodeService.m */, + C33FDBF5255A581B00E217F9 /* OWSDeviceProvisioningService.h */, + C33FDB92255A581200E217F9 /* OWSDeviceProvisioningService.m */, + C33FDBDB255A581900E217F9 /* OWSDevicesService.h */, + C33FDBE5255A581A00E217F9 /* OWSDevicesService.m */, + C33FDADA255A580400E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.h */, + C33FDA6B255A57FA00E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.m */, + C33FDAD9255A580300E217F9 /* OWSDisappearingMessagesConfiguration.h */, + C33FDBA4255A581400E217F9 /* OWSDisappearingMessagesConfiguration.m */, + C33FDB16255A580800E217F9 /* OWSDisappearingMessagesConfigurationMessage.h */, + C33FDB97255A581300E217F9 /* OWSDisappearingMessagesConfigurationMessage.m */, + C33FDC05255A581D00E217F9 /* OWSDisappearingMessagesFinder.h */, + C33FDA86255A57FC00E217F9 /* OWSDisappearingMessagesFinder.m */, + C33FDA80255A57FC00E217F9 /* OWSDisappearingMessagesJob.h */, + C33FDBDD255A581900E217F9 /* OWSDisappearingMessagesJob.m */, + C33FDA96255A57FE00E217F9 /* OWSDispatch.h */, + C33FDAC3255A580200E217F9 /* OWSDispatch.m */, + C33FDB63255A580E00E217F9 /* OWSDynamicOutgoingMessage.h */, + C33FDAD1255A580300E217F9 /* OWSDynamicOutgoingMessage.m */, + C33FDB2E255A580A00E217F9 /* OWSEndSessionMessage.h */, + C33FDB27255A580A00E217F9 /* OWSEndSessionMessage.m */, + C33FDBF9255A581C00E217F9 /* OWSError.h */, + C33FDC0B255A581D00E217F9 /* OWSError.m */, + C33FDA72255A57FA00E217F9 /* OWSFailedAttachmentDownloadsJob.h */, + C33FDB59255A580E00E217F9 /* OWSFailedAttachmentDownloadsJob.m */, + C33FDADB255A580400E217F9 /* OWSFailedMessagesJob.h */, + C33FDAB7255A580100E217F9 /* OWSFailedMessagesJob.m */, + C33FDBAB255A581500E217F9 /* OWSFileSystem.h */, + C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */, + C33FDBC8255A581700E217F9 /* OWSFingerprint.h */, + C33FDB05255A580700E217F9 /* OWSFingerprint.m */, + C33FDBCF255A581800E217F9 /* OWSFingerprintBuilder.h */, + C33FDAEE255A580500E217F9 /* OWSFingerprintBuilder.m */, + C33FDABB255A580100E217F9 /* OWSGroupsOutputStream.h */, + C33FDAD8255A580300E217F9 /* OWSGroupsOutputStream.m */, + C33FDB52255A580D00E217F9 /* OWSHTTPSecurityPolicy.h */, + C33FDBBF255A581700E217F9 /* OWSHTTPSecurityPolicy.m */, + C33FDBF1255A581B00E217F9 /* OWSIdentityManager.h */, + C33FDBA9255A581500E217F9 /* OWSIdentityManager.m */, + C33FDAC0255A580100E217F9 /* OWSIncomingMessageFinder.h */, + C33FDB1E255A580900E217F9 /* OWSIncomingMessageFinder.m */, + C33FDAF6255A580600E217F9 /* OWSIncomingSentMessageTranscript.h */, + C33FDB84255A581100E217F9 /* OWSIncomingSentMessageTranscript.m */, + C33FDB2A255A580A00E217F9 /* OWSIncompleteCallsJob.h */, + C33FDBF7255A581C00E217F9 /* OWSIncompleteCallsJob.m */, + C33FDAA6255A57FF00E217F9 /* OWSLinkedDeviceReadReceipt.h */, + C33FDA9A255A57FE00E217F9 /* OWSLinkedDeviceReadReceipt.m */, + C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */, + C33FDB14255A580800E217F9 /* OWSMath.h */, + C33FDB67255A580F00E217F9 /* OWSMediaGalleryFinder.h */, + C33FDB71255A581000E217F9 /* OWSMediaGalleryFinder.m */, + C33FDB22255A580900E217F9 /* OWSMediaUtils.swift */, + C33FDC0E255A581E00E217F9 /* OWSMessageDecrypter.h */, + C33FDB90255A581200E217F9 /* OWSMessageDecrypter.m */, + C33FDAAC255A580000E217F9 /* OWSMessageHandler.h */, + C33FDBFC255A581C00E217F9 /* OWSMessageHandler.m */, + C33FDB7D255A581100E217F9 /* OWSMessageManager.h */, + C33FDA6A255A57F900E217F9 /* OWSMessageManager.m */, + C33FDBC6255A581700E217F9 /* OWSMessageReceiver.h */, + C33FDAB4255A580000E217F9 /* OWSMessageReceiver.m */, + C33FDACC255A580200E217F9 /* OWSMessageSend.swift */, + C33FDA92255A57FE00E217F9 /* OWSMessageSender.h */, + C33FDB3E255A580B00E217F9 /* OWSMessageSender.m */, + C33FDBCE255A581800E217F9 /* OWSMessageServiceParams.h */, + C33FDB70255A580F00E217F9 /* OWSMessageServiceParams.m */, + C33FDAE8255A580500E217F9 /* OWSMessageUtils.h */, + C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */, + C33FDBA1255A581400E217F9 /* OWSOperation.h */, + C33FDB78255A581000E217F9 /* OWSOperation.m */, + C33FDA66255A57F900E217F9 /* OWSOutgoingCallMessage.h */, + C33FDB28255A580A00E217F9 /* OWSOutgoingCallMessage.m */, + C33FDBB3255A581500E217F9 /* OWSOutgoingNullMessage.h */, + C33FDBE6255A581A00E217F9 /* OWSOutgoingNullMessage.m */, + C33FDABD255A580100E217F9 /* OWSOutgoingReceiptManager.h */, + C33FDB6F255A580F00E217F9 /* OWSOutgoingReceiptManager.m */, + C33FDB24255A580900E217F9 /* OWSOutgoingSentMessageTranscript.h */, + C33FDBD5255A581900E217F9 /* OWSOutgoingSentMessageTranscript.m */, + C33FDBE4255A581A00E217F9 /* OWSOutgoingSyncMessage.h */, + C33FDBFF255A581C00E217F9 /* OWSOutgoingSyncMessage.m */, + C33FDA67255A57F900E217F9 /* OWSPrimaryStorage.h */, + C33FDC02255A581D00E217F9 /* OWSPrimaryStorage.m */, + C33FDB1A255A580900E217F9 /* OWSPrimaryStorage+Calling.h */, + C33FDBFD255A581C00E217F9 /* OWSPrimaryStorage+Calling.m */, + C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */, + C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */, + C33FDBBB255A581600E217F9 /* OWSPrimaryStorage+Loki.h */, + C33FDB58255A580E00E217F9 /* OWSPrimaryStorage+Loki.m */, + C33FDA85255A57FC00E217F9 /* OWSPrimaryStorage+Loki.swift */, + C33FDB47255A580C00E217F9 /* OWSPrimaryStorage+PreKeyStore.h */, + C33FDB50255A580D00E217F9 /* OWSPrimaryStorage+PreKeyStore.m */, + C33FDB03255A580700E217F9 /* OWSPrimaryStorage+SessionStore.h */, + C33FDAF3255A580500E217F9 /* OWSPrimaryStorage+SessionStore.m */, + C33FDAA7255A57FF00E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.h */, + C33FDAF7255A580600E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.m */, + C33FDB08255A580700E217F9 /* OWSProfileKeyMessage.h */, + C33FDB3D255A580B00E217F9 /* OWSProfileKeyMessage.m */, + C33FDB62255A580E00E217F9 /* OWSProvisioningCipher.h */, + C33FDB2B255A580A00E217F9 /* OWSProvisioningCipher.m */, + C33FDC0D255A581E00E217F9 /* OWSProvisioningMessage.h */, + C33FDA9B255A57FE00E217F9 /* OWSProvisioningMessage.m */, + C33FDC19255A581F00E217F9 /* OWSQueues.h */, + C33FDB1D255A580900E217F9 /* OWSReadReceiptManager.h */, + C33FDA71255A57FA00E217F9 /* OWSReadReceiptManager.m */, + C33FDAE2255A580400E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.h */, + C33FDB96255A581300E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.m */, + C33FDAE1255A580400E217F9 /* OWSReadTracking.h */, + C33FDA8D255A57FD00E217F9 /* OWSReceiptsForSenderMessage.h */, + C33FDAAE255A580000E217F9 /* OWSReceiptsForSenderMessage.m */, + C33FDAA0255A57FF00E217F9 /* OWSRecipientIdentity.h */, + C33FDBEC255A581B00E217F9 /* OWSRecipientIdentity.m */, + C33FDBAD255A581500E217F9 /* OWSRecordTranscriptJob.h */, + C33FDA7F255A57FC00E217F9 /* OWSRecordTranscriptJob.m */, + C38EEFD5255B5BA2007E1867 /* OldSnodeAPI.swift */, + C33FDB06255A580700E217F9 /* OWSRequestBuilder.h */, + C33FDB00255A580600E217F9 /* OWSRequestBuilder.m */, + C33FDBC0255A581700E217F9 /* OWSRequestFactory.h */, + C33FDB86255A581100E217F9 /* OWSRequestFactory.m */, + C33FDAA5255A57FF00E217F9 /* OWSRequestMaker.swift */, + C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */, + C33FDAC7255A580200E217F9 /* OWSSignalService.h */, + C33FDB18255A580800E217F9 /* OWSSignalService.m */, + C33FDAFE255A580600E217F9 /* OWSStorage.h */, + C33FDAB1255A580000E217F9 /* OWSStorage.m */, + C33FDAB9255A580100E217F9 /* OWSStorage+Subclass.h */, + C33FDB65255A580F00E217F9 /* OWSSyncConfigurationMessage.h */, + C33FDC1D255A581F00E217F9 /* OWSSyncConfigurationMessage.m */, + C33FDB57255A580D00E217F9 /* OWSSyncContactsMessage.h */, + C33FDA9C255A57FE00E217F9 /* OWSSyncContactsMessage.m */, + C33FDBB5255A581600E217F9 /* OWSSyncGroupsMessage.h */, + C33FDC00255A581C00E217F9 /* OWSSyncGroupsMessage.m */, + C33FDAF8255A580600E217F9 /* OWSSyncGroupsRequestMessage.h */, + C33FDA76255A57FB00E217F9 /* OWSSyncGroupsRequestMessage.m */, + C33FDA75255A57FB00E217F9 /* OWSSyncManagerProtocol.h */, + C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */, + C33FDB5A255A580E00E217F9 /* OWSUDManager.swift */, + C33FDBD2255A581800E217F9 /* OWSUnknownContactBlockOfferMessage.h */, + C33FDBE2255A581A00E217F9 /* OWSUnknownContactBlockOfferMessage.m */, + C33FDBD6255A581900E217F9 /* OWSUploadOperation.h */, + C33FDC1E255A581F00E217F9 /* OWSUploadOperation.m */, + C33FDB7C255A581000E217F9 /* OWSVerificationStateChangeMessage.h */, + C33FDB0B255A580700E217F9 /* OWSVerificationStateChangeMessage.m */, + C33FDBD9255A581900E217F9 /* OWSVerificationStateSyncMessage.h */, + C33FDACE255A580300E217F9 /* OWSVerificationStateSyncMessage.m */, + C33FDABA255A580100E217F9 /* OWSWebSocket.h */, + C33FDAAB255A580000E217F9 /* OWSWebSocket.m */, + C33FDB8F255A581200E217F9 /* ParamParser.swift */, + C33FDBA2255A581400E217F9 /* PhoneNumber.h */, + C33FDA8A255A57FD00E217F9 /* PhoneNumber.m */, + C33FDBFA255A581C00E217F9 /* PhoneNumberUtil.h */, + C33FDB79255A581000E217F9 /* PhoneNumberUtil.m */, + C33FDB3A255A580B00E217F9 /* Poller.swift */, + C33FDB53255A580D00E217F9 /* PreKeyBundle+jsonDict.h */, + C33FDB93255A581200E217F9 /* PreKeyBundle+jsonDict.m */, + C33FDB64255A580E00E217F9 /* PreKeyRefreshOperation.swift */, + C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */, + C33FDA83255A57FC00E217F9 /* Promise+retainUntilComplete.swift */, + C33FDC10255A581E00E217F9 /* ProofOfWork.swift */, + C33FDB91255A581200E217F9 /* ProtoUtils.h */, + C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */, + C33FDB33255A580B00E217F9 /* Provisioning.pb.swift */, + C33FDAB0255A580000E217F9 /* ProvisioningProto.swift */, + C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */, + C33FDADF255A580400E217F9 /* PublicChatManager.swift */, + C33FDA8C255A57FD00E217F9 /* PublicChatPoller.swift */, + C33FDA6F255A57FA00E217F9 /* ReachabilityManager.swift */, + C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */, + C33FDA98255A57FE00E217F9 /* RotateSignedKeyOperation.swift */, + C33FDBA3255A581400E217F9 /* SessionManagementProtocol.swift */, + C33FDAFB255A580600E217F9 /* SessionMetaProtocol.swift */, + C33FDBEA255A581A00E217F9 /* SessionRequestMessage.swift */, + C33FDC17255A581F00E217F9 /* SharedSenderKeysImplementation.swift */, + C33FDBAE255A581500E217F9 /* SignalAccount.h */, + C33FDC06255A581D00E217F9 /* SignalAccount.m */, + C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */, + C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */, + C33FDB6A255A580F00E217F9 /* SignalMessage.swift */, + C33FDAEC255A580500E217F9 /* SignalRecipient.h */, + C33FDBB7255A581600E217F9 /* SignalRecipient.m */, + C33FDC08255A581D00E217F9 /* SignalService.pb.swift */, + C33FDB4A255A580C00E217F9 /* SignalServiceClient.swift */, + C33FDBA5255A581400E217F9 /* SignalServiceProfile.swift */, + C33FDBC2255A581700E217F9 /* SSKAsserts.h */, + C33FDB31255A580A00E217F9 /* SSKEnvironment.h */, + C33FDAF4255A580600E217F9 /* SSKEnvironment.m */, + C33FDB32255A580A00E217F9 /* SSKIncrementingIdFinder.swift */, + C33FDB4F255A580D00E217F9 /* SSKJobRecord.h */, + C33FDACD255A580200E217F9 /* SSKJobRecord.m */, + C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */, + C33FDAED255A580500E217F9 /* SSKMessageSenderJobRecord.h */, + C33FDAE9255A580500E217F9 /* SSKMessageSenderJobRecord.m */, + C33FDA69255A57F900E217F9 /* SSKPreferences.swift */, + C33FDAA4255A57FF00E217F9 /* SSKProto.swift */, + C33FDB6E255A580F00E217F9 /* SSKProtoPrekeyBundleMessage+Loki.swift */, + C33FDA78255A57FB00E217F9 /* SSKWebSocket.swift */, + C33FDB36255A580B00E217F9 /* Storage.swift */, + C33FDC07255A581D00E217F9 /* Storage+ClosedGroups.swift */, + C33FDB95255A581300E217F9 /* Storage+Collections.swift */, + C33FDBE8255A581A00E217F9 /* Storage+OnionRequests.swift */, + C33FDBFB255A581C00E217F9 /* Storage+PublicChats.swift */, + C33FDB8B255A581200E217F9 /* Storage+SessionManagement.swift */, + C33FDB37255A580B00E217F9 /* Storage+SnodeAPI.swift */, + C33FDB3F255A580C00E217F9 /* String+SSK.swift */, + C33FDAAF255A580000E217F9 /* String+Trimming.swift */, + C33FDADE255A580400E217F9 /* SwiftSingletons.swift */, + C33FDAB6255A580100E217F9 /* SyncMessagesProtocol.swift */, + C33FDB94255A581300E217F9 /* TSAccountManager.h */, + C33FDB88255A581200E217F9 /* TSAccountManager.m */, + C33FDC15255A581E00E217F9 /* TSAttachment.h */, + C33FDAC2255A580200E217F9 /* TSAttachment.m */, + C33FDC18255A581F00E217F9 /* TSAttachmentPointer.h */, + C33FDB9E255A581400E217F9 /* TSAttachmentPointer.m */, + C33FDAE4255A580400E217F9 /* TSAttachmentStream.h */, + C33FDAC4255A580200E217F9 /* TSAttachmentStream.m */, + C33FDB02255A580700E217F9 /* TSCall.h */, + C33FDC1C255A581F00E217F9 /* TSCall.m */, + C33FDC12255A581E00E217F9 /* TSConstants.h */, + C33FDABE255A580100E217F9 /* TSConstants.m */, + C33FDAB3255A580000E217F9 /* TSContactThread.h */, + C33FDAF9255A580600E217F9 /* TSContactThread.m */, + C33FDB25255A580900E217F9 /* TSDatabaseSecondaryIndexes.h */, + C33FDB20255A580900E217F9 /* TSDatabaseSecondaryIndexes.m */, + C33FDB2C255A580A00E217F9 /* TSDatabaseView.h */, + C33FDB46255A580C00E217F9 /* TSDatabaseView.m */, + C33FDB5D255A580E00E217F9 /* TSErrorMessage_privateConstructor.h */, + C33FDBB0255A581500E217F9 /* TSErrorMessage.h */, + C33FDAE7255A580500E217F9 /* TSErrorMessage.m */, + C33FDB0A255A580700E217F9 /* TSGroupModel.h */, + C33FDB73255A581000E217F9 /* TSGroupModel.m */, + C33FDA79255A57FB00E217F9 /* TSGroupThread.h */, + C33FDC01255A581C00E217F9 /* TSGroupThread.m */, + C33FDB9C255A581300E217F9 /* TSIncomingMessage.h */, + C33FDA97255A57FE00E217F9 /* TSIncomingMessage.m */, + C33FDADD255A580400E217F9 /* TSInfoMessage.h */, + C33FDC0C255A581E00E217F9 /* TSInfoMessage.m */, + C33FDAE6255A580400E217F9 /* TSInteraction.h */, + C33FDBE9255A581A00E217F9 /* TSInteraction.m */, + C33FDB8C255A581200E217F9 /* TSInvalidIdentityKeyErrorMessage.h */, + C33FDB74255A581000E217F9 /* TSInvalidIdentityKeyErrorMessage.m */, + C33FDA7B255A57FB00E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.h */, + C33FDBCB255A581800E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.m */, + C33FDBA6255A581400E217F9 /* TSInvalidIdentityKeySendingErrorMessage.h */, + C33FDB55255A580D00E217F9 /* TSInvalidIdentityKeySendingErrorMessage.m */, + C33FDA70255A57FA00E217F9 /* TSMessage.h */, + C33FDB60255A580E00E217F9 /* TSMessage.m */, + C33FDAB5255A580000E217F9 /* TSNetworkManager.h */, + C33FDAB2255A580000E217F9 /* TSNetworkManager.m */, + C33FDB48255A580C00E217F9 /* TSOutgoingMessage.h */, + C33FDB56255A580D00E217F9 /* TSOutgoingMessage.m */, + C33FDAE3255A580400E217F9 /* TSPrefix.h */, + C33FDB6D255A580F00E217F9 /* TSPreKeyManager.h */, + C33FDB7E255A581100E217F9 /* TSPreKeyManager.m */, + C33FDAD5255A580300E217F9 /* TSQuotedMessage.h */, + C33FDB83255A581100E217F9 /* TSQuotedMessage.m */, + C33FDBB1255A581500E217F9 /* TSSocketManager.h */, + C33FDC11255A581E00E217F9 /* TSSocketManager.m */, + C33FDBA0255A581400E217F9 /* TSStorageHeaders.h */, + C33FDBEF255A581B00E217F9 /* TSStorageKeys.h */, + C33FDAD3255A580300E217F9 /* TSThread.h */, + C33FDBB8255A581600E217F9 /* TSThread.m */, + C33FDAA1255A57FF00E217F9 /* TSYapDatabaseObject.h */, + C33FDA90255A57FD00E217F9 /* TSYapDatabaseObject.m */, + C33FDB9F255A581400E217F9 /* TTLUtilities.swift */, + C33FDBC4255A581700E217F9 /* TypingIndicatorMessage.swift */, + C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */, + C33FDB1C255A580900E217F9 /* UIImage+OWS.h */, + C33FDB81255A581100E217F9 /* UIImage+OWS.m */, + C33FDB49255A580C00E217F9 /* WeakTimer.swift */, + C33FDA6D255A57FA00E217F9 /* YapDatabase+Promise.swift */, + C33FDB5F255A580E00E217F9 /* YapDatabaseConnection+OWS.h */, + C33FDB43255A580C00E217F9 /* YapDatabaseConnection+OWS.m */, + C33FDA88255A57FD00E217F9 /* YapDatabaseTransaction+OWS.h */, + C33FDB5B255A580E00E217F9 /* YapDatabaseTransaction+OWS.m */, + C33FD9B7255A54A300E217F9 /* Meta */, + C38EEF09255B49A8007E1867 /* SSKProtoEnvelope+Conversion.swift */, + ); + path = SignalUtilitiesKit; + sourceTree = ""; + }; + C33FD9B7255A54A300E217F9 /* Meta */ = { + isa = PBXGroup; + children = ( + C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */, + C33FD9AE255A548A00E217F9 /* Info.plist */, + ); + path = Meta; + sourceTree = ""; + }; C352A2F325574B3300338F3E /* Jobs */ = { isa = PBXGroup; children = ( @@ -3530,7 +4853,6 @@ isa = PBXGroup; children = ( C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */, - C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */, C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */, C3C2A5D02553860800C340D1 /* Promise+Threading.swift */, C3C2A5D22553860900C340D1 /* String+Utilities.swift */, @@ -3560,6 +4882,7 @@ C300A6312554B6D100555489 /* NSDate+Timestamp.mm */, C352A3762557859C00338F3E /* NSTimer+Proxying.h */, C352A36C2557858D00338F3E /* NSTimer+Proxying.m */, + C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */, C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */, C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */, C352A3A42557B5F000338F3E /* TSRequest.h */, @@ -3772,8 +5095,6 @@ C3C2A9E32553B9C300C340D1 /* OWSLogs.h */, C3C2A9D62553B9C200C340D1 /* OWSLogs.m */, C3C2A9DF2553B9C200C340D1 /* OWSSwiftUtils.swift */, - C3C2A9EB2553B9C400C340D1 /* Randomness.h */, - C3C2A9E82553B9C300C340D1 /* Randomness.m */, C3C2A9E22553B9C300C340D1 /* SCKExceptionWrapper.h */, C3C2A9EA2553B9C300C340D1 /* SCKExceptionWrapper.m */, C3C2A96C2553B63C00C340D1 /* SerializationUtilities.h */, @@ -3822,6 +5143,7 @@ 453518691FC635DD00210559 /* SignalShareExtension */, 453518931FC63DBF00210559 /* SignalMessaging */, 7BC01A3C241F40AB00BC7C55 /* LokiPushNotificationService */, + C33FD9AC255A548A00E217F9 /* SignalUtilitiesKit */, C331FF1C2558F9D300070591 /* SessionUIKit */, C3C2A6F125539DE700C340D1 /* SessionMessagingKit */, C3C2A8632553B41A00C340D1 /* SessionProtocolKit */, @@ -3846,6 +5168,7 @@ C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */, C3C2A8622553B41A00C340D1 /* SessionProtocolKit.framework */, C331FF1B2558F9D300070591 /* SessionUIKit.framework */, + C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */, ); name = Products; sourceTree = ""; @@ -3897,6 +5220,8 @@ FB523C549815DE935E98151E /* Pods_SessionMessagingKit.framework */, 2183DCA28E0620BC73FCC554 /* Pods_SessionProtocolKit.framework */, 9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */, + 71CFEDD2D3C54277731012DF /* Pods_SessionUIKit.framework */, + 53D547348A367C8A14D37FC0 /* Pods_SignalUtilitiesKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -4022,6 +5347,178 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C33FD9A6255A548A00E217F9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C33FDD74255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.h in Headers */, + C33FDCDB255A582000E217F9 /* OWS2FAManager.h in Headers */, + C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */, + C33FDD6B255A582000E217F9 /* TSSocketManager.h in Headers */, + C33FDCCE255A582000E217F9 /* OWSMath.h in Headers */, + C33FDC47255A581F00E217F9 /* OWSReceiptsForSenderMessage.h in Headers */, + C33FDD46255A582000E217F9 /* TSInvalidIdentityKeyErrorMessage.h in Headers */, + C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */, + C33FDCE3255A582000E217F9 /* NSData+Image.h in Headers */, + C33FDC5D255A582000E217F9 /* OWSAddToContactsOfferMessage.h in Headers */, + C33FDC77255A582000E217F9 /* OWSOutgoingReceiptManager.h in Headers */, + C33FDCBB255A582000E217F9 /* AppReadiness.h in Headers */, + C33FDD82255A582000E217F9 /* OWSFingerprint.h in Headers */, + C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */, + C33FDD6F255A582000E217F9 /* OWSSyncGroupsMessage.h in Headers */, + C33FDC89255A582000E217F9 /* OWSAttachmentDownloads.h in Headers */, + C33FDD73255A582000E217F9 /* ProfileManagerProtocol.h in Headers */, + C33FDD11255A582000E217F9 /* OWSSyncContactsMessage.h in Headers */, + C33FDCB6255A582000E217F9 /* MIMETypeUtil.h in Headers */, + C33FDCAF255A582000E217F9 /* OWSAnalyticsEvents.h in Headers */, + C33FDD17255A582000E217F9 /* TSErrorMessage_privateConstructor.h in Headers */, + C33FDC93255A582000E217F9 /* OWSDisappearingMessagesConfiguration.h in Headers */, + C33FDCCC255A582000E217F9 /* NSString+SSK.h in Headers */, + C33FDCB0255A582000E217F9 /* OWSIncomingSentMessageTranscript.h in Headers */, + C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */, + C33FDC9B255A582000E217F9 /* OWSReadTracking.h in Headers */, + C33FDD44255A582000E217F9 /* AppContext.h in Headers */, + C33FDC97255A582000E217F9 /* TSInfoMessage.h in Headers */, + C33FDD65255A582000E217F9 /* OWSFileSystem.h in Headers */, + C33FDCD6255A582000E217F9 /* UIImage+OWS.h in Headers */, + C33FDD80255A582000E217F9 /* OWSMessageReceiver.h in Headers */, + C33FDC6D255A582000E217F9 /* TSContactThread.h in Headers */, + C33FDD7A255A582000E217F9 /* OWSRequestFactory.h in Headers */, + C33FDC3A255A581F00E217F9 /* OWSDisappearingMessagesJob.h in Headers */, + C33FDD6A255A582000E217F9 /* TSErrorMessage.h in Headers */, + C33FDDA9255A582000E217F9 /* TSStorageKeys.h in Headers */, + C33FDC7A255A582000E217F9 /* OWSIncomingMessageFinder.h in Headers */, + C33FDC8C255A582000E217F9 /* Contact.h in Headers */, + C33FDCB8255A582000E217F9 /* OWSStorage.h in Headers */, + C33FDD4E255A582000E217F9 /* TSAccountManager.h in Headers */, + C33FDD77255A582000E217F9 /* OWSAddToProfileWhitelistOfferMessage.h in Headers */, + C33FDDAB255A582000E217F9 /* OWSIdentityManager.h in Headers */, + C33FDC82255A582000E217F9 /* OWSCensorshipConfiguration.h in Headers */, + C33FDC2C255A581F00E217F9 /* OWSFailedAttachmentDownloadsJob.h in Headers */, + C33FDCD5255A582000E217F9 /* OWSDeviceProvisioningCodeService.h in Headers */, + C33FDDB4255A582000E217F9 /* PhoneNumberUtil.h in Headers */, + C33FDD67255A582000E217F9 /* OWSRecordTranscriptJob.h in Headers */, + C33FDCAA255A582000E217F9 /* ContactsUpdater.h in Headers */, + C33FDDCF255A582000E217F9 /* TSAttachment.h in Headers */, + C33FDDB8255A582000E217F9 /* NSSet+Functional.h in Headers */, + C33FDC2F255A581F00E217F9 /* OWSSyncManagerProtocol.h in Headers */, + C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */, + C33FDCDF255A582000E217F9 /* TSDatabaseSecondaryIndexes.h in Headers */, + C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */, + C33FDCBC255A582000E217F9 /* TSCall.h in Headers */, + C33FDD37255A582000E217F9 /* OWSMessageManager.h in Headers */, + C33FDD95255A582000E217F9 /* OWSDevicesService.h in Headers */, + C33FDCD4255A582000E217F9 /* OWSPrimaryStorage+Calling.h in Headers */, + C33FDC5B255A582000E217F9 /* TSYapDatabaseObject.h in Headers */, + C33FDD8C255A582000E217F9 /* OWSUnknownContactBlockOfferMessage.h in Headers */, + C33FDD93255A582000E217F9 /* OWSVerificationStateSyncMessage.h in Headers */, + C33FDD88255A582000E217F9 /* OWSMessageServiceParams.h in Headers */, + C33FDDBF255A582000E217F9 /* OWSDisappearingMessagesFinder.h in Headers */, + C33FDCC4255A582000E217F9 /* TSGroupModel.h in Headers */, + C33FDC6F255A582000E217F9 /* TSNetworkManager.h in Headers */, + C33FDD4B255A582000E217F9 /* ProtoUtils.h in Headers */, + C33FDD19255A582000E217F9 /* YapDatabaseConnection+OWS.h in Headers */, + C33FDD1F255A582000E217F9 /* OWSSyncConfigurationMessage.h in Headers */, + C33FDD0C255A582000E217F9 /* OWSHTTPSecurityPolicy.h in Headers */, + C33FDC57255A582000E217F9 /* OWSContactsOutputStream.h in Headers */, + C33FDCBD255A582000E217F9 /* OWSPrimaryStorage+SessionStore.h in Headers */, + C33FDDD2255A582000E217F9 /* TSAttachmentPointer.h in Headers */, + C33FDD1D255A582000E217F9 /* OWSDynamicOutgoingMessage.h in Headers */, + C33FDCD7255A582000E217F9 /* OWSReadReceiptManager.h in Headers */, + C33FDD21255A582000E217F9 /* OWSMediaGalleryFinder.h in Headers */, + C33FDCC6255A582000E217F9 /* CDSQuote.h in Headers */, + C33FDD02255A582000E217F9 /* TSOutgoingMessage.h in Headers */, + C33FDC95255A582000E217F9 /* OWSFailedMessagesJob.h in Headers */, + C33FDD20255A582000E217F9 /* ContactDiscoveryService.h in Headers */, + C33FDD1B255A582000E217F9 /* LKUnlinkDeviceMessage.h in Headers */, + C33FDD75255A582000E217F9 /* OWSPrimaryStorage+Loki.h in Headers */, + C33FDD8E255A582000E217F9 /* OWSBatchMessageProcessor.h in Headers */, + C33FDC5A255A582000E217F9 /* OWSRecipientIdentity.h in Headers */, + C33FDD1C255A582000E217F9 /* OWSProvisioningCipher.h in Headers */, + C33FDC42255A581F00E217F9 /* YapDatabaseTransaction+OWS.h in Headers */, + C33FDD48255A582000E217F9 /* OWSContact+Private.h in Headers */, + C33FDCA6255A582000E217F9 /* SignalRecipient.h in Headers */, + C33FDC66255A582000E217F9 /* OWSMessageHandler.h in Headers */, + C33FDDA1255A582000E217F9 /* NSTimer+OWS.h in Headers */, + C33FDC74255A582000E217F9 /* OWSWebSocket.h in Headers */, + C33FDC9E255A582000E217F9 /* TSAttachmentStream.h in Headers */, + C33FDC20255A581F00E217F9 /* OWSOutgoingCallMessage.h in Headers */, + C33FDC83255A582000E217F9 /* OWSContact.h in Headers */, + C33FDCB2255A582000E217F9 /* OWSSyncGroupsRequestMessage.h in Headers */, + C33FDCA7255A582000E217F9 /* SSKMessageSenderJobRecord.h in Headers */, + C33FDDC8255A582000E217F9 /* OWSMessageDecrypter.h in Headers */, + C33FDCC2255A582000E217F9 /* OWSProfileKeyMessage.h in Headers */, + C33FDCDE255A582000E217F9 /* OWSOutgoingSentMessageTranscript.h in Headers */, + C33FDD84255A582000E217F9 /* LKGroupUtilities.h in Headers */, + C33FDDC7255A582000E217F9 /* OWSProvisioningMessage.h in Headers */, + C33FDC8D255A582000E217F9 /* TSThread.h in Headers */, + C33FDC81255A582000E217F9 /* OWSSignalService.h in Headers */, + C33FDC4B255A582000E217F9 /* LKSyncOpenGroupsMessage.h in Headers */, + C33FDDA5255A582000E217F9 /* OWSBlockingManager.h in Headers */, + C33FDD9D255A582000E217F9 /* CDSSigningCertificate.h in Headers */, + C33FDD16255A582000E217F9 /* NSArray+Functional.h in Headers */, + C33FDCE9255A582000E217F9 /* ContactsManagerProtocol.h in Headers */, + C33FDCD0255A582000E217F9 /* OWSDisappearingMessagesConfigurationMessage.h in Headers */, + C33FDCE4255A582000E217F9 /* OWSIncompleteCallsJob.h in Headers */, + C33FDCC0255A582000E217F9 /* OWSRequestBuilder.h in Headers */, + C33FDDD3255A582000E217F9 /* OWSQueues.h in Headers */, + C33FDC96255A582000E217F9 /* NSObject+Casting.h in Headers */, + C33FDC9D255A582000E217F9 /* TSPrefix.h in Headers */, + C33FDC75255A582000E217F9 /* OWSGroupsOutputStream.h in Headers */, + C33FDDB3255A582000E217F9 /* OWSError.h in Headers */, + C33FDD57255A582000E217F9 /* OWSCallMessageHandler.h in Headers */, + C33FDCC8255A582000E217F9 /* NSError+MessageSending.h in Headers */, + C33FDC4C255A582000E217F9 /* OWSMessageSender.h in Headers */, + C33FDD6D255A582000E217F9 /* OWSOutgoingNullMessage.h in Headers */, + C33FDCA0255A582000E217F9 /* TSInteraction.h in Headers */, + C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */, + C33FDC4F255A582000E217F9 /* OWSChunkedOutputStream.h in Headers */, + C33FDCE7255A582000E217F9 /* OWSDevice.h in Headers */, + C33FDD0E255A582000E217F9 /* DataSource.h in Headers */, + C33FDCF2255A582000E217F9 /* OWSBackgroundTask.h in Headers */, + C33FDD5C255A582000E217F9 /* PhoneNumber.h in Headers */, + C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */, + C33FDC2A255A581F00E217F9 /* TSMessage.h in Headers */, + C33FDD09255A582000E217F9 /* SSKJobRecord.h in Headers */, + C33FDC94255A582000E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.h in Headers */, + C33FDC21255A581F00E217F9 /* OWSPrimaryStorage.h in Headers */, + C33FDC61255A582000E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.h in Headers */, + C33FDCEB255A582000E217F9 /* SSKEnvironment.h in Headers */, + C33FDC35255A581F00E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.h in Headers */, + C33FDDC9255A582000E217F9 /* LKDeviceLinkMessage.h in Headers */, + C33FDD89255A582000E217F9 /* OWSFingerprintBuilder.h in Headers */, + C33FDD0D255A582000E217F9 /* PreKeyBundle+jsonDict.h in Headers */, + C33FDD60255A582000E217F9 /* TSInvalidIdentityKeySendingErrorMessage.h in Headers */, + C33FDCF5255A582000E217F9 /* NSNotificationCenter+OWS.h in Headers */, + C33FDD34255A582000E217F9 /* NotificationsProtocol.h in Headers */, + C33FDC90255A582000E217F9 /* OWSAnalytics.h in Headers */, + C33FDD36255A582000E217F9 /* OWSVerificationStateChangeMessage.h in Headers */, + C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */, + C33FDD9E255A582000E217F9 /* OWSOutgoingSyncMessage.h in Headers */, + C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */, + C33FDD06255A582000E217F9 /* AppVersion.h in Headers */, + C33FDD90255A582000E217F9 /* OWSUploadOperation.h in Headers */, + C33FDC8F255A582000E217F9 /* TSQuotedMessage.h in Headers */, + C33FDC73255A582000E217F9 /* OWSStorage+Subclass.h in Headers */, + C33FDCA4255A582000E217F9 /* OWSBackupFragment.h in Headers */, + C33FDCFC255A582000E217F9 /* OWSCountryMetadata.h in Headers */, + C33FDC60255A582000E217F9 /* OWSLinkedDeviceReadReceipt.h in Headers */, + C33FDD27255A582000E217F9 /* TSPreKeyManager.h in Headers */, + C33FDC67255A582000E217F9 /* OWSDeviceProvisioner.h in Headers */, + C33FDCE8255A582000E217F9 /* OWSEndSessionMessage.h in Headers */, + C33FDDAD255A582000E217F9 /* OWSBlockedPhoneNumbersMessage.h in Headers */, + C33FDD0B255A582000E217F9 /* NSUserDefaults+OWS.h in Headers */, + C33FDD56255A582000E217F9 /* TSIncomingMessage.h in Headers */, + C33FDC9C255A582000E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.h in Headers */, + C33FDDAF255A582000E217F9 /* OWSDeviceProvisioningService.h in Headers */, + C33FDCE6255A582000E217F9 /* TSDatabaseView.h in Headers */, + C33FDD5A255A582000E217F9 /* TSStorageHeaders.h in Headers */, + C33FDCA2255A582000E217F9 /* OWSMessageUtils.h in Headers */, + C33FDC33255A581F00E217F9 /* TSGroupThread.h in Headers */, + C33FDD01255A582000E217F9 /* OWSPrimaryStorage+PreKeyStore.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C3C2A59A255385C100C340D1 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -4059,7 +5556,6 @@ C3C2A95D2553B62400C340D1 /* PreKeyStore.h in Headers */, C3C2A91F2553B5B200C340D1 /* AxolotlParameters.h in Headers */, C3C2A95F2553B62400C340D1 /* SessionStore.h in Headers */, - C3C2AA062553B9C400C340D1 /* Randomness.h in Headers */, C3C2A8D52553B57C00C340D1 /* PreKeyBundle.h in Headers */, C3C2A9092553B5B200C340D1 /* RatchetingSession.h in Headers */, C3C2A96F2553B63C00C340D1 /* NSData+keyVersionByte.h in Headers */, @@ -4169,6 +5665,7 @@ isa = PBXNativeTarget; buildConfigurationList = C331FF262558F9D400070591 /* Build configuration list for PBXNativeTarget "SessionUIKit" */; buildPhases = ( + E185AC3DC0F55CFE87DEC852 /* [CP] Check Pods Manifest.lock */, C331FF162558F9D300070591 /* Headers */, C331FF172558F9D300070591 /* Sources */, C331FF182558F9D300070591 /* Frameworks */, @@ -4183,6 +5680,25 @@ productReference = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; productType = "com.apple.product-type.framework"; }; + C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = C33FD9B6255A548A00E217F9 /* Build configuration list for PBXNativeTarget "SignalUtilitiesKit" */; + buildPhases = ( + 7E2D14F857C70F98DED3B8E9 /* [CP] Check Pods Manifest.lock */, + C33FD9A6255A548A00E217F9 /* Headers */, + C33FD9A7255A548A00E217F9 /* Sources */, + C33FD9A8255A548A00E217F9 /* Frameworks */, + C33FD9A9255A548A00E217F9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SignalUtilitiesKit; + productName = SignalUtilitiesKit; + productReference = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; + productType = "com.apple.product-type.framework"; + }; C3C2A59E255385C100C340D1 /* SessionSnodeKit */ = { isa = PBXNativeTarget; buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */; @@ -4284,6 +5800,7 @@ C3C2A6F625539DE700C340D1 /* PBXTargetDependency */, C3C2A8682553B41A00C340D1 /* PBXTargetDependency */, C331FF212558F9D300070591 /* PBXTargetDependency */, + C33FD9B1255A548A00E217F9 /* PBXTargetDependency */, ); name = Signal; productName = RedPhone; @@ -4360,6 +5877,12 @@ DevelopmentTeam = SUQ8J2PCT7; ProvisioningStyle = Automatic; }; + C33FD9AA255A548A00E217F9 = { + CreatedOnToolsVersion = 12.1; + DevelopmentTeam = SUQ8J2PCT7; + LastSwiftMigration = 1210; + ProvisioningStyle = Automatic; + }; C3C2A59E255385C100C340D1 = { CreatedOnToolsVersion = 12.1; DevelopmentTeam = SUQ8J2PCT7; @@ -4453,6 +5976,7 @@ 453518671FC635DD00210559 /* SignalShareExtension */, 453518911FC63DBF00210559 /* SignalMessaging */, 7BC01A3A241F40AB00BC7C55 /* LokiPushNotificationService */, + C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */, C331FF1A2558F9D300070591 /* SessionUIKit */, C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */, C3C2A8612553B41A00C340D1 /* SessionProtocolKit */, @@ -4499,6 +6023,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C33FD9A9255A548A00E217F9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C3C2A59D255385C100C340D1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4857,6 +6388,28 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 7E2D14F857C70F98DED3B8E9 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SignalUtilitiesKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 83DABC75697364620557C68B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -4961,6 +6514,28 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SignalTests/Pods-SignalTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + E185AC3DC0F55CFE87DEC852 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SessionUIKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; F4C416F20E3CB0B25DC10C56 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -5168,6 +6743,271 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C33FD9A7255A548A00E217F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C33FDD40255A582000E217F9 /* OWSRequestFactory.m in Sources */, + C33FDD96255A582000E217F9 /* ContactDiscoveryService.m in Sources */, + C33FDCA9255A582000E217F9 /* NSData+Image.m in Sources */, + C33FDC8B255A582000E217F9 /* OWSDynamicOutgoingMessage.m in Sources */, + C38EEFD6255B5BA2007E1867 /* OldSnodeAPI.swift in Sources */, + C33FDD45255A582000E217F9 /* Storage+SessionManagement.swift in Sources */, + C33FDCA3255A582000E217F9 /* SSKMessageSenderJobRecord.m in Sources */, + C33FDDB5255A582000E217F9 /* Storage+PublicChats.swift in Sources */, + C33FDC41255A581F00E217F9 /* TypingIndicators.swift in Sources */, + C33FDC7D255A582000E217F9 /* OWSDispatch.m in Sources */, + C33FDC2E255A581F00E217F9 /* ClosedGroupsProtocol.swift in Sources */, + C33FDC99255A582000E217F9 /* PublicChatManager.swift in Sources */, + C33FDCB9255A582000E217F9 /* DisplayNameUtilities.swift in Sources */, + C33FDDA7255A582000E217F9 /* ClosedGroupParser.swift in Sources */, + C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */, + C33FDDCD255A582000E217F9 /* OWSAttachmentDownloads.m in Sources */, + C33FDC32255A581F00E217F9 /* SSKWebSocket.swift in Sources */, + C33FDD97255A582000E217F9 /* OWSDisappearingMessagesJob.m in Sources */, + C33FDD0F255A582000E217F9 /* TSInvalidIdentityKeySendingErrorMessage.m in Sources */, + C33FDC44255A581F00E217F9 /* PhoneNumber.m in Sources */, + C33FDD79255A582000E217F9 /* OWSHTTPSecurityPolicy.m in Sources */, + C33FDDB7255A582000E217F9 /* OWSPrimaryStorage+Calling.m in Sources */, + C33FDD69255A582000E217F9 /* OWSAnalyticsEvents.m in Sources */, + C33FDD3D255A582000E217F9 /* TSQuotedMessage.m in Sources */, + C33FDCB4255A582000E217F9 /* LKDeviceLinkMessage.m in Sources */, + C33FDC70255A582000E217F9 /* SyncMessagesProtocol.swift in Sources */, + C33FDD12255A582000E217F9 /* OWSPrimaryStorage+Loki.m in Sources */, + C33FDC5C255A582000E217F9 /* OWSAddToContactsOfferMessage.m in Sources */, + C33FDCFB255A582000E217F9 /* MIMETypeUtil.m in Sources */, + C33FDC87255A582000E217F9 /* SSKJobRecord.m in Sources */, + C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */, + C33FDD8A255A582000E217F9 /* OnionRequestAPI+Encryption.swift in Sources */, + C33FDD3C255A582000E217F9 /* MessageWrapper.swift in Sources */, + C33FDCF1255A582000E217F9 /* Storage+SnodeAPI.swift in Sources */, + C33FDCAE255A582000E217F9 /* SSKEnvironment.m in Sources */, + C33FDDA8255A582000E217F9 /* ClosedGroupUpdateMessage.swift in Sources */, + C33FDCF9255A582000E217F9 /* String+SSK.swift in Sources */, + C33FDDAC255A582000E217F9 /* MessageSender+Promise.swift in Sources */, + C33FDD0A255A582000E217F9 /* OWSPrimaryStorage+PreKeyStore.m in Sources */, + C33FDDC3255A582000E217F9 /* AccountServiceClient.swift in Sources */, + C33FDC9F255A582000E217F9 /* OWSAddToProfileWhitelistOfferMessage.m in Sources */, + C33FDC4E255A582000E217F9 /* Data+Streaming.swift in Sources */, + C33FDCE5255A582000E217F9 /* OWSProvisioningCipher.m in Sources */, + C33FDDD6255A582000E217F9 /* TSCall.m in Sources */, + C33FDD1A255A582000E217F9 /* TSMessage.m in Sources */, + C33FDD26255A582000E217F9 /* NSNotificationCenter+OWS.m in Sources */, + C33FDDB6255A582000E217F9 /* OWSMessageHandler.m in Sources */, + C33FDC80255A582000E217F9 /* Fingerprint.pb.swift in Sources */, + C33FDD42255A582000E217F9 /* TSAccountManager.m in Sources */, + C33FDC53255A582000E217F9 /* OutageDetection.swift in Sources */, + C33FDD00255A582000E217F9 /* TSDatabaseView.m in Sources */, + C33FDD3B255A582000E217F9 /* UIImage+OWS.m in Sources */, + C33FDD5F255A582000E217F9 /* SignalServiceProfile.swift in Sources */, + C33FDD83255A582000E217F9 /* CreatePreKeysOperation.swift in Sources */, + C33FDDD5255A582000E217F9 /* OWSBackgroundTask.m in Sources */, + C33FDC6E255A582000E217F9 /* OWSMessageReceiver.m in Sources */, + C33FDC7E255A582000E217F9 /* TSAttachmentStream.m in Sources */, + C33FDC62255A582000E217F9 /* BuildConfiguration.swift in Sources */, + C33FDCD1255A582000E217F9 /* FunctionalUtil.m in Sources */, + C33FDD51255A582000E217F9 /* OWSDisappearingMessagesConfigurationMessage.m in Sources */, + C33FDD3F255A582000E217F9 /* AppContext.m in Sources */, + C33FDCC7255A582000E217F9 /* NSArray+OWS.m in Sources */, + C33FDD25255A582000E217F9 /* LKUserDefaults.swift in Sources */, + C33FDD72255A582000E217F9 /* TSThread.m in Sources */, + C33FDC71255A582000E217F9 /* OWSFailedMessagesJob.m in Sources */, + C33FDD32255A582000E217F9 /* OWSOperation.m in Sources */, + C33FDD9C255A582000E217F9 /* OWSUnknownContactBlockOfferMessage.m in Sources */, + C33FDC4A255A582000E217F9 /* TSYapDatabaseObject.m in Sources */, + C33FDC65255A582000E217F9 /* OWSWebSocket.m in Sources */, + C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */, + C33FDCEF255A582000E217F9 /* OWSContactDiscoveryOperation.swift in Sources */, + C33FDCB1255A582000E217F9 /* OWSPrimaryStorage+SignedPreKeyStore.m in Sources */, + C33FDD59255A582000E217F9 /* TTLUtilities.swift in Sources */, + C33FDD7E255A582000E217F9 /* TypingIndicatorMessage.swift in Sources */, + C33FDC64255A582000E217F9 /* NSObject+Casting.m in Sources */, + C33FDC85255A582000E217F9 /* CDSSigningCertificate.m in Sources */, + C33FDC68255A582000E217F9 /* OWSReceiptsForSenderMessage.m in Sources */, + C33FDD53255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.m in Sources */, + C33FDD66255A582000E217F9 /* Data+SecureRandom.swift in Sources */, + C33FDC72255A582000E217F9 /* NSArray+Functional.m in Sources */, + C33FDD38255A582000E217F9 /* TSPreKeyManager.m in Sources */, + C33FDCCD255A582000E217F9 /* LKUnlinkDeviceMessage.m in Sources */, + C33FDD94255A582000E217F9 /* Dictionary+Description.swift in Sources */, + C33FDD18255A582000E217F9 /* ContactParser.swift in Sources */, + C33FDD2C255A582000E217F9 /* DeviceLinkIndex.swift in Sources */, + C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */, + C33FDDA2255A582000E217F9 /* Storage+OnionRequests.swift in Sources */, + C33FDDBE255A582000E217F9 /* DecryptionUtilities.swift in Sources */, + C33FDC3F255A581F00E217F9 /* OWSPrimaryStorage+Loki.swift in Sources */, + C33FDDB9255A582000E217F9 /* OWSOutgoingSyncMessage.m in Sources */, + C33FDC52255A582000E217F9 /* RotateSignedKeyOperation.swift in Sources */, + C33FDCDA255A582000E217F9 /* TSDatabaseSecondaryIndexes.m in Sources */, + C33FDC6C255A582000E217F9 /* TSNetworkManager.m in Sources */, + C33FDD71255A582000E217F9 /* SignalRecipient.m in Sources */, + C33FDD58255A582000E217F9 /* TSAttachmentPointer.m in Sources */, + C33FDD39255A582000E217F9 /* FullTextSearchFinder.swift in Sources */, + C33FDDA4255A582000E217F9 /* SessionRequestMessage.swift in Sources */, + C33FDC5F255A582000E217F9 /* OWSRequestMaker.swift in Sources */, + C38EEF0A255B49A8007E1867 /* SSKProtoEnvelope+Conversion.swift in Sources */, + C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */, + C33FDCB3255A582000E217F9 /* TSContactThread.m in Sources */, + C33FDC45255A581F00E217F9 /* AppVersion.m in Sources */, + C33FDD2A255A582000E217F9 /* OWSMessageServiceParams.m in Sources */, + C33FDC7F255A582000E217F9 /* OWSCountryMetadata.m in Sources */, + C33FDC84255A582000E217F9 /* LokiMessage.swift in Sources */, + C33FDC7C255A582000E217F9 /* TSAttachment.m in Sources */, + C33FDC24255A581F00E217F9 /* OWSMessageManager.m in Sources */, + C33FDC46255A581F00E217F9 /* PublicChatPoller.swift in Sources */, + C33FDCD9255A582000E217F9 /* DeviceLinkingUtilities.swift in Sources */, + C33FDC34255A581F00E217F9 /* NSRegularExpression+SSK.swift in Sources */, + C33FDD7D255A582000E217F9 /* AnyPromise+Conversion.swift in Sources */, + C33FDD8B255A582000E217F9 /* DeviceLink.swift in Sources */, + C33FDD5D255A582000E217F9 /* SessionManagementProtocol.swift in Sources */, + C33FDD76255A582000E217F9 /* SSKKeychainStorage.swift in Sources */, + C33FDD33255A582000E217F9 /* PhoneNumberUtil.m in Sources */, + C33FDDA6255A582000E217F9 /* OWSRecipientIdentity.m in Sources */, + C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, + C33FDD3E255A582000E217F9 /* OWSIncomingSentMessageTranscript.m in Sources */, + C33FDD05255A582000E217F9 /* OWSChunkedOutputStream.m in Sources */, + C33FDCC5255A582000E217F9 /* OWSVerificationStateChangeMessage.m in Sources */, + C33FDD10255A582000E217F9 /* TSOutgoingMessage.m in Sources */, + C33FDCD2255A582000E217F9 /* OWSSignalService.m in Sources */, + C33FDCFA255A582000E217F9 /* SignalIOSProto.swift in Sources */, + C33FDC28255A581F00E217F9 /* Array+Description.swift in Sources */, + C33FDCC3255A582000E217F9 /* NSError+MessageSending.m in Sources */, + C33FDCED255A582000E217F9 /* Provisioning.pb.swift in Sources */, + C33FDCF0255A582000E217F9 /* Storage.swift in Sources */, + C33FDD2D255A582000E217F9 /* TSGroupModel.m in Sources */, + C33FDD50255A582000E217F9 /* OWSReadReceiptsForLinkedDevicesMessage.m in Sources */, + C33FDD9B255A582000E217F9 /* LKGroupUtilities.m in Sources */, + C33FDDC2255A582000E217F9 /* SignalService.pb.swift in Sources */, + C33FDD13255A582000E217F9 /* OWSFailedAttachmentDownloadsJob.m in Sources */, + C33FDCB5255A582000E217F9 /* SessionMetaProtocol.swift in Sources */, + C33FDC91255A582000E217F9 /* OWSDeviceProvisioner.m in Sources */, + C33FDC6A255A582000E217F9 /* ProvisioningProto.swift in Sources */, + C33FDC37255A581F00E217F9 /* OWSCensorshipConfiguration.m in Sources */, + C33FDD4D255A582000E217F9 /* PreKeyBundle+jsonDict.m in Sources */, + C33FDC51255A582000E217F9 /* TSIncomingMessage.m in Sources */, + C33FDCFF255A582000E217F9 /* NSString+SSK.m in Sources */, + C33FDC36255A581F00E217F9 /* Debugging.swift in Sources */, + C33FDD22255A582000E217F9 /* ContentProxy.swift in Sources */, + C33FDD98255A582000E217F9 /* LokiPushNotificationManager.swift in Sources */, + C33FDCF8255A582000E217F9 /* OWSMessageSender.m in Sources */, + C33FDD70255A582000E217F9 /* DataSource.m in Sources */, + C33FDD2F255A582000E217F9 /* AppReadiness.m in Sources */, + C33FDCEC255A582000E217F9 /* SSKIncrementingIdFinder.swift in Sources */, + C33FDC7B255A582000E217F9 /* NSSet+Functional.m in Sources */, + C33FDC69255A582000E217F9 /* String+Trimming.swift in Sources */, + C33FDCB7255A582000E217F9 /* LRUCache.swift in Sources */, + C33FDDB1255A582000E217F9 /* OWSIncompleteCallsJob.m in Sources */, + C33FDC3B255A581F00E217F9 /* MentionsManager.swift in Sources */, + C33FDC49255A581F00E217F9 /* NSTimer+OWS.m in Sources */, + C33FDD55255A582000E217F9 /* MessageSenderJobQueue.swift in Sources */, + C33FDCFD255A582000E217F9 /* YapDatabaseConnection+OWS.m in Sources */, + C33FDC23255A581F00E217F9 /* SSKPreferences.swift in Sources */, + C33FDD4C255A582000E217F9 /* OWSDeviceProvisioningService.m in Sources */, + C33FDC31255A581F00E217F9 /* Contact.m in Sources */, + C33FDC8A255A582000E217F9 /* CDSQuote.m in Sources */, + C33FDCA5255A582000E217F9 /* OWSDeviceProvisioningCodeService.m in Sources */, + C33FDC38255A581F00E217F9 /* Mention.swift in Sources */, + C33FDD8F255A582000E217F9 /* OWSOutgoingSentMessageTranscript.m in Sources */, + C33FDDC1255A582000E217F9 /* Storage+ClosedGroups.swift in Sources */, + C33FDC63255A582000E217F9 /* Mnemonic.swift in Sources */, + C33FDC25255A581F00E217F9 /* OWSDisappearingConfigurationUpdateInfoMessage.m in Sources */, + C33FDC59255A582000E217F9 /* NetworkManager.swift in Sources */, + C33FDDCB255A582000E217F9 /* TSSocketManager.m in Sources */, + C33FDC39255A581F00E217F9 /* OWSRecordTranscriptJob.m in Sources */, + C33FDCCA255A582000E217F9 /* OWSBatchMessageProcessor.m in Sources */, + C33FDD1E255A582000E217F9 /* PreKeyRefreshOperation.swift in Sources */, + C33FDDCA255A582000E217F9 /* ProofOfWork.swift in Sources */, + C33FDCF4255A582000E217F9 /* Poller.swift in Sources */, + C33FDDBA255A582000E217F9 /* OWSSyncGroupsMessage.m in Sources */, + C33FDDD7255A582000E217F9 /* OWSSyncConfigurationMessage.m in Sources */, + C33FDC43255A581F00E217F9 /* OWSAnalytics.m in Sources */, + C33FDD04255A582000E217F9 /* SignalServiceClient.swift in Sources */, + C33FDC26255A581F00E217F9 /* ProtoUtils.m in Sources */, + C33FDC48255A581F00E217F9 /* OWSFileSystem.m in Sources */, + C33FDC5E255A582000E217F9 /* SSKProto.swift in Sources */, + C33FDD85255A582000E217F9 /* TSInvalidIdentityKeyReceivingErrorMessage.m in Sources */, + C33FDD2E255A582000E217F9 /* TSInvalidIdentityKeyErrorMessage.m in Sources */, + C33FDDBB255A582000E217F9 /* TSGroupThread.m in Sources */, + C33FDD4A255A582000E217F9 /* OWSMessageDecrypter.m in Sources */, + C33FDD14255A582000E217F9 /* OWSUDManager.swift in Sources */, + C33FDC6B255A582000E217F9 /* OWSStorage.m in Sources */, + C33FDD7B255A582000E217F9 /* GeneralUtilities.swift in Sources */, + C33FDD47255A582000E217F9 /* DeviceLinkingSessionDelegate.swift in Sources */, + C33FDCC1255A582000E217F9 /* OWSBackupFragment.m in Sources */, + C33FDDAE255A582000E217F9 /* DisplayNameUtilities2.swift in Sources */, + C33FDD4F255A582000E217F9 /* Storage+Collections.swift in Sources */, + C33FDD15255A582000E217F9 /* YapDatabaseTransaction+OWS.m in Sources */, + C33FDD63255A582000E217F9 /* OWSIdentityManager.m in Sources */, + C33FDCC9255A582000E217F9 /* DeviceLinkingSession.swift in Sources */, + C33FDC54255A582000E217F9 /* OWSLinkedDeviceReadReceipt.m in Sources */, + C33FDDD9255A582000E217F9 /* LokiSessionResetImplementation.swift in Sources */, + C33FDD31255A582000E217F9 /* NSUserDefaults+OWS.m in Sources */, + C33FDCA1255A582000E217F9 /* TSErrorMessage.m in Sources */, + C33FDDD1255A582000E217F9 /* SharedSenderKeysImplementation.swift in Sources */, + C33FDCBF255A582000E217F9 /* OWSFingerprint.m in Sources */, + C33FDC22255A581F00E217F9 /* OWSBlockingManager.m in Sources */, + C33FDCA8255A582000E217F9 /* OWSFingerprintBuilder.m in Sources */, + C33FDC40255A581F00E217F9 /* OWSDisappearingMessagesFinder.m in Sources */, + C33FDD28255A582000E217F9 /* SSKProtoPrekeyBundleMessage+Loki.swift in Sources */, + C33FDD24255A582000E217F9 /* SignalMessage.swift in Sources */, + C33FDCEE255A582000E217F9 /* ClosedGroupPoller.swift in Sources */, + C33FDC8E255A582000E217F9 /* EncryptionUtilities.swift in Sources */, + C33FDC9A255A582000E217F9 /* ByteParser.m in Sources */, + C33FDC55255A582000E217F9 /* OWSProvisioningMessage.m in Sources */, + C33FDDC0255A582000E217F9 /* SignalAccount.m in Sources */, + C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */, + C33FDD29255A582000E217F9 /* OWSOutgoingReceiptManager.m in Sources */, + C33FDC78255A582000E217F9 /* TSConstants.m in Sources */, + C33FDD52255A582000E217F9 /* DeviceNames.swift in Sources */, + C33FDCE0255A582000E217F9 /* FingerprintProto.swift in Sources */, + C33FDDA0255A582000E217F9 /* OWSOutgoingNullMessage.m in Sources */, + C33FDD5E255A582000E217F9 /* OWSDisappearingMessagesConfiguration.m in Sources */, + C33FDCD8255A582000E217F9 /* OWSIncomingMessageFinder.m in Sources */, + C33FDCE2255A582000E217F9 /* OWSOutgoingCallMessage.m in Sources */, + C33FDD9A255A582000E217F9 /* OWSBlockedPhoneNumbersMessage.m in Sources */, + C33FDD99255A582000E217F9 /* LKSyncOpenGroupsMessage.m in Sources */, + C33FDCAC255A582000E217F9 /* ProxiedContentDownloader.swift in Sources */, + C33FDD03255A582000E217F9 /* WeakTimer.swift in Sources */, + C33FDCDC255A582000E217F9 /* OWSMediaUtils.swift in Sources */, + C33FDD41255A582000E217F9 /* JobQueue.swift in Sources */, + C33FDC30255A581F00E217F9 /* OWSSyncGroupsRequestMessage.m in Sources */, + C33FDD9F255A582000E217F9 /* OWSDevicesService.m in Sources */, + C33FDD43255A582000E217F9 /* FileServerAPI+Deprecated.swift in Sources */, + C33FDD30255A582000E217F9 /* ClosedGroupUtilities.swift in Sources */, + C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */, + C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */, + C33FDC3E255A581F00E217F9 /* ContactsUpdater.m in Sources */, + C33FDCFE255A582000E217F9 /* OWSContactsOutputStream.m in Sources */, + C33FDC88255A582000E217F9 /* OWSVerificationStateSyncMessage.m in Sources */, + C33FDDBC255A582000E217F9 /* OWSPrimaryStorage.m in Sources */, + C33FDD54255A582000E217F9 /* OWS2FAManager.m in Sources */, + C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */, + C33FDDAA255A582000E217F9 /* LokiDatabaseUtilities.swift in Sources */, + C33FDCBA255A582000E217F9 /* OWSRequestBuilder.m in Sources */, + C33FDC2B255A581F00E217F9 /* OWSReadReceiptManager.m in Sources */, + C33FDC86255A582000E217F9 /* OWSMessageSend.swift in Sources */, + C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */, + C33FDCF7255A582000E217F9 /* OWSProfileKeyMessage.m in Sources */, + C33FDD86255A582000E217F9 /* MultiDeviceProtocol.swift in Sources */, + C33FDCAD255A582000E217F9 /* OWSPrimaryStorage+SessionStore.m in Sources */, + C33FDC92255A582000E217F9 /* OWSGroupsOutputStream.m in Sources */, + C33FDC56255A582000E217F9 /* OWSSyncContactsMessage.m in Sources */, + C33FDC3D255A581F00E217F9 /* Promise+retainUntilComplete.swift in Sources */, + C33FDD62255A582000E217F9 /* OWSLinkPreview.swift in Sources */, + C33FDDA3255A582000E217F9 /* TSInteraction.m in Sources */, + C33FDDD8255A582000E217F9 /* OWSUploadOperation.m in Sources */, + C33FDDC5255A582000E217F9 /* OWSError.m in Sources */, + C33FDCAB255A582000E217F9 /* OWSThumbnailService.swift in Sources */, + C33FDC2D255A581F00E217F9 /* ECKeyPair+Hexadecimal.swift in Sources */, + C33FDDC6255A582000E217F9 /* TSInfoMessage.m in Sources */, + C33FDDC4255A582000E217F9 /* OWSContact.m in Sources */, + C33FDCE1255A582000E217F9 /* OWSEndSessionMessage.m in Sources */, + C33FDCEA255A582000E217F9 /* OWSDevice.m in Sources */, + C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */, + C33FDD2B255A582000E217F9 /* OWSMediaGalleryFinder.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C3C2A59B255385C100C340D1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -5177,7 +7017,6 @@ C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */, C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */, C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */, - C3C2A5DF2553860B00C340D1 /* Promise+Delaying.swift in Sources */, C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */, C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */, C3C2A5DE2553860B00C340D1 /* String+Utilities.swift in Sources */, @@ -5205,6 +7044,7 @@ C352A3A62557B60D00338F3E /* TSRequest.m in Sources */, C3471ED42555386B00297E91 /* AESGCM.swift in Sources */, C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, + C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */, C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */, C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, @@ -5301,7 +7141,6 @@ C3C2A91A2553B5B200C340D1 /* TSDerivedSecrets.m in Sources */, C3A71D5025589FF30043A11F /* SMKUDAccessKey.swift in Sources */, C3C2A91C2553B5B200C340D1 /* ReceivingChain.m in Sources */, - C3C2AA032553B9C400C340D1 /* Randomness.m in Sources */, C3C2A9EF2553B9C400C340D1 /* OWSDataParser.swift in Sources */, C3C2A8A32553B4F600C340D1 /* FallbackMessage.m in Sources */, C3A71D5425589FF30043A11F /* NSData+messagePadding.m in Sources */, @@ -5656,6 +7495,11 @@ target = C331FF1A2558F9D300070591 /* SessionUIKit */; targetProxy = C331FF202558F9D300070591 /* PBXContainerItemProxy */; }; + C33FD9B1255A548A00E217F9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */; + targetProxy = C33FD9B0255A548A00E217F9 /* PBXContainerItemProxy */; + }; C36B8706243C50B00049991D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 453518911FC63DBF00210559 /* SignalMessaging */; @@ -6080,6 +7924,7 @@ }; C331FF242558F9D400070591 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C88965DE4F4EC4FC919BEC4E /* Pods-SessionUIKit.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -6130,6 +7975,7 @@ }; C331FF252558F9D400070591 /* App Store Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C1A746BC424B531D8ED478F6 /* Pods-SessionUIKit.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -6201,6 +8047,132 @@ }; name = "App Store Release"; }; + C33FD9B4255A548A00E217F9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5F3070F3395081DD0EB4F933 /* Pods-SignalUtilitiesKit.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = SUQ8J2PCT7; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = SignalUtilitiesKit/Meta/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SignalUtilitiesKit"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + C33FD9B5255A548A00E217F9 /* App Store Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9C0469AC557930C01552CC83 /* Pods-SignalUtilitiesKit.app store release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = SUQ8J2PCT7; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = SignalUtilitiesKit/Meta/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SignalUtilitiesKit"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = "App Store Release"; + }; C3C2A5A8255385C100C340D1 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = A6344D429FFAC3B44E6A06FA /* Pods-SessionSnodeKit.debug.xcconfig */; @@ -7168,6 +9140,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = "App Store Release"; }; + C33FD9B6255A548A00E217F9 /* Build configuration list for PBXNativeTarget "SignalUtilitiesKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C33FD9B4255A548A00E217F9 /* Debug */, + C33FD9B5255A548A00E217F9 /* App Store Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "App Store Release"; + }; C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/SignalUtilitiesKit/AccountServiceClient.swift b/SignalUtilitiesKit/AccountServiceClient.swift new file mode 100644 index 000000000..da9c5ee06 --- /dev/null +++ b/SignalUtilitiesKit/AccountServiceClient.swift @@ -0,0 +1,34 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +// TODO define actual type, and validate length +public typealias IdentityKey = Data + +/// based on libsignal-service-java's AccountManager class +@objc(SSKAccountServiceClient) +public class AccountServiceClient: NSObject { + + static var shared = AccountServiceClient() + + private let serviceClient: SignalServiceClient + + override init() { + self.serviceClient = SignalServiceRestClient() + } + + public func getPreKeysCount() -> Promise { + return serviceClient.getAvailablePreKeys() + } + + public func setPreKeys(identityKey: IdentityKey, signedPreKeyRecord: SignedPreKeyRecord, preKeyRecords: [PreKeyRecord]) -> Promise { + return serviceClient.registerPreKeys(identityKey: identityKey, signedPreKeyRecord: signedPreKeyRecord, preKeyRecords: preKeyRecords) + } + + public func setSignedPreKey(_ signedPreKey: SignedPreKeyRecord) -> Promise { + return serviceClient.setCurrentSignedPreKey(signedPreKey) + } +} diff --git a/SignalUtilitiesKit/AnyPromise+Conversion.swift b/SignalUtilitiesKit/AnyPromise+Conversion.swift new file mode 100644 index 000000000..1c15fc554 --- /dev/null +++ b/SignalUtilitiesKit/AnyPromise+Conversion.swift @@ -0,0 +1,10 @@ +import PromiseKit + +public extension AnyPromise { + + public static func from(_ promise: Promise) -> AnyPromise { + let result = AnyPromise(promise) + result.retainUntilComplete() + return result + } +} diff --git a/SignalUtilitiesKit/AppContext.h b/SignalUtilitiesKit/AppContext.h new file mode 100755 index 000000000..c1674864c --- /dev/null +++ b/SignalUtilitiesKit/AppContext.h @@ -0,0 +1,130 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +static inline BOOL OWSIsDebugBuild() +{ +#ifdef DEBUG + return YES; +#else + return NO; +#endif +} + +// These are fired whenever the corresponding "main app" or "app extension" +// notification is fired. +// +// 1. This saves you the work of observing both. +// 2. This allows us to ensure that any critical work (e.g. re-opening +// databases) has been done before app re-enters foreground, etc. +extern NSString *const OWSApplicationDidEnterBackgroundNotification; +extern NSString *const OWSApplicationWillEnterForegroundNotification; +extern NSString *const OWSApplicationWillResignActiveNotification; +extern NSString *const OWSApplicationDidBecomeActiveNotification; + +typedef void (^BackgroundTaskExpirationHandler)(void); +typedef void (^AppActiveBlock)(void); + +NSString *NSStringForUIApplicationState(UIApplicationState value); + +@class OWSAES256Key; + +@protocol SSKKeychainStorage; + +@protocol AppContext + +@property (nonatomic, readonly) BOOL isMainApp; +@property (nonatomic, readonly) BOOL isMainAppAndActive; +/// Whether the app was woken up by a silent push notification. This is important for +/// determining whether attachments should be downloaded or not. +@property (nonatomic) BOOL wasWokenUpByPushNotification; + +// Whether the user is using a right-to-left language like Arabic. +@property (nonatomic, readonly) BOOL isRTL; + +@property (nonatomic, readonly) BOOL isRunningTests; + +@property (atomic, nullable) UIWindow *mainWindow; + +// Unlike UIApplication.applicationState, this is thread-safe. +// It contains the "last known" application state. +// +// Because it is updated in response to "will/did-style" events, it is +// conservative and skews toward less-active and not-foreground: +// +// * It doesn't report "is active" until the app is active +// and reports "inactive" as soon as it _will become_ inactive. +// * It doesn't report "is foreground (but inactive)" until the app is +// foreground & inactive and reports "background" as soon as it _will +// enter_ background. +// +// This conservatism is useful, since we want to err on the side of +// caution when, for example, we do work that should only be done +// when the app is foreground and active. +@property (atomic, readonly) UIApplicationState reportedApplicationState; + +// A convenience accessor for reportedApplicationState. +// +// This method is thread-safe. +- (BOOL)isInBackground; + +// A convenience accessor for reportedApplicationState. +// +// This method is thread-safe. +- (BOOL)isAppForegroundAndActive; + +// Should start a background task if isMainApp is YES. +// Should just return UIBackgroundTaskInvalid if isMainApp is NO. +- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler: + (BackgroundTaskExpirationHandler)expirationHandler; + +// Should be a NOOP if isMainApp is NO. +- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier; + +// Should be a NOOP if isMainApp is NO. +- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray *)blockingObjects; + +// Should only be called if isMainApp is YES. +- (void)setMainAppBadgeNumber:(NSInteger)value; + +- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated; + +@property (nonatomic, readonly) CGFloat statusBarHeight; + +// Returns the VC that should be used to present alerts, modals, etc. +- (nullable UIViewController *)frontmostViewController; + +// Returns nil if isMainApp is NO +@property (nullable, nonatomic, readonly) UIAlertAction *openSystemSettingsAction; + +// Should be a NOOP if isMainApp is NO. +- (void)setNetworkActivityIndicatorVisible:(BOOL)value; + +- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block; + +@property (atomic, readonly) NSDate *appLaunchTime; + +- (id)keychainStorage; + +- (NSString *)appDocumentDirectoryPath; + +- (NSString *)appSharedDataDirectoryPath; + +- (NSUserDefaults *)appUserDefaults; + +@end + +id CurrentAppContext(void); +void SetCurrentAppContext(id appContext); + +void ExitShareExtension(void); + +#ifdef DEBUG +void ClearCurrentAppContextForTests(void); +#endif + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/AppContext.m b/SignalUtilitiesKit/AppContext.m new file mode 100755 index 000000000..ece6f2865 --- /dev/null +++ b/SignalUtilitiesKit/AppContext.m @@ -0,0 +1,61 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "AppContext.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSApplicationDidEnterBackgroundNotification = @"OWSApplicationDidEnterBackgroundNotification"; +NSString *const OWSApplicationWillEnterForegroundNotification = @"OWSApplicationWillEnterForegroundNotification"; +NSString *const OWSApplicationWillResignActiveNotification = @"OWSApplicationWillResignActiveNotification"; +NSString *const OWSApplicationDidBecomeActiveNotification = @"OWSApplicationDidBecomeActiveNotification"; + +NSString *NSStringForUIApplicationState(UIApplicationState value) +{ + switch (value) { + case UIApplicationStateActive: + return @"UIApplicationStateActive"; + case UIApplicationStateInactive: + return @"UIApplicationStateInactive"; + case UIApplicationStateBackground: + return @"UIApplicationStateBackground"; + } +} + +static id currentAppContext = nil; + +id CurrentAppContext(void) +{ + OWSCAssertDebug(currentAppContext); + + return currentAppContext; +} + +void SetCurrentAppContext(id appContext) +{ + // The main app context should only be set once. + // + // App extensions may be opened multiple times in the same process, + // so statics will persist. + OWSCAssertDebug(!currentAppContext || !currentAppContext.isMainApp); + + currentAppContext = appContext; +} + +#ifdef DEBUG +void ClearCurrentAppContextForTests() +{ + currentAppContext = nil; +} +#endif + +void ExitShareExtension(void) +{ + OWSLogInfo(@"ExitShareExtension"); + [DDLog flushLog]; + exit(0); +} + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/AppReadiness.h b/SignalUtilitiesKit/AppReadiness.h new file mode 100755 index 000000000..617892b00 --- /dev/null +++ b/SignalUtilitiesKit/AppReadiness.h @@ -0,0 +1,38 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^AppReadyBlock)(void); + +@interface AppReadiness : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +// This method can be called on any thread. ++ (BOOL)isAppReady; + +// This method should only be called on the main thread. ++ (void)setAppIsReady; + +// If the app is ready, the block is called immediately; +// otherwise it is called when the app becomes ready. +// +// This method should only be called on the main thread. +// The block will always be called on the main thread. +// +// * The "will become ready" blocks are called before the "did become ready" blocks. +// * The "will become ready" blocks should be used for internal setup of components +// so that they are ready to interact with other components of the system. +// * The "did become ready" blocks should be used for any work that should be done +// on app launch, especially work that uses other components. +// * We should usually use "did become ready" blocks since they are safer. ++ (void)runNowOrWhenAppWillBecomeReady:(AppReadyBlock)block NS_SWIFT_NAME(runNowOrWhenAppWillBecomeReady(_:)); ++ (void)runNowOrWhenAppDidBecomeReady:(AppReadyBlock)block NS_SWIFT_NAME(runNowOrWhenAppDidBecomeReady(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/AppReadiness.m b/SignalUtilitiesKit/AppReadiness.m new file mode 100755 index 000000000..0153594ab --- /dev/null +++ b/SignalUtilitiesKit/AppReadiness.m @@ -0,0 +1,144 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "AppReadiness.h" +#import +#import "AppContext.h" +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface AppReadiness () + +@property (atomic) BOOL isAppReady; + +@property (nonatomic) NSMutableArray *appWillBecomeReadyBlocks; +@property (nonatomic) NSMutableArray *appDidBecomeReadyBlocks; + +@end + +#pragma mark - + +@implementation AppReadiness + ++ (instancetype)sharedManager +{ + static AppReadiness *sharedMyManager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedMyManager = [[self alloc] initDefault]; + }); + return sharedMyManager; +} + +- (instancetype)initDefault +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSSingletonAssert(); + + self.appWillBecomeReadyBlocks = [NSMutableArray new]; + self.appDidBecomeReadyBlocks = [NSMutableArray new]; + + return self; +} + ++ (BOOL)isAppReady +{ + return [self.sharedManager isAppReady]; +} + ++ (void)runNowOrWhenAppWillBecomeReady:(AppReadyBlock)block +{ + DispatchMainThreadSafe(^{ + [self.sharedManager runNowOrWhenAppWillBecomeReady:block]; + }); +} + +- (void)runNowOrWhenAppWillBecomeReady:(AppReadyBlock)block +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(block); + + if (CurrentAppContext().isRunningTests) { + // We don't need to do any "on app ready" work in the tests. + return; + } + + if (self.isAppReady) { + block(); + return; + } + + [self.appWillBecomeReadyBlocks addObject:block]; +} + ++ (void)runNowOrWhenAppDidBecomeReady:(AppReadyBlock)block +{ + DispatchMainThreadSafe(^{ + [self.sharedManager runNowOrWhenAppDidBecomeReady:block]; + }); +} + +- (void)runNowOrWhenAppDidBecomeReady:(AppReadyBlock)block +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(block); + + if (CurrentAppContext().isRunningTests) { + // We don't need to do any "on app ready" work in the tests. + return; + } + + if (self.isAppReady) { + block(); + return; + } + + [self.appDidBecomeReadyBlocks addObject:block]; +} + ++ (void)setAppIsReady +{ + [self.sharedManager setAppIsReady]; +} + +- (void)setAppIsReady +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(!self.isAppReady); + + OWSLogInfo(@""); + + self.isAppReady = YES; + + [self runAppReadyBlocks]; +} + +- (void)runAppReadyBlocks +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(self.isAppReady); + + NSArray *appWillBecomeReadyBlocks = [self.appWillBecomeReadyBlocks copy]; + [self.appWillBecomeReadyBlocks removeAllObjects]; + NSArray *appDidBecomeReadyBlocks = [self.appDidBecomeReadyBlocks copy]; + [self.appDidBecomeReadyBlocks removeAllObjects]; + + // We invoke the _will become_ blocks before the _did become_ blocks. + for (AppReadyBlock block in appWillBecomeReadyBlocks) { + block(); + } + for (AppReadyBlock block in appDidBecomeReadyBlocks) { + block(); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/AppVersion.h b/SignalUtilitiesKit/AppVersion.h new file mode 100755 index 000000000..38702e56c --- /dev/null +++ b/SignalUtilitiesKit/AppVersion.h @@ -0,0 +1,32 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface AppVersion : NSObject + +// The properties are updated immediately after launch. +@property (atomic, readonly) NSString *firstAppVersion; +@property (atomic, nullable, readonly) NSString *lastAppVersion; +@property (atomic, readonly) NSString *currentAppVersion; + +// There properties aren't updated until appLaunchDidComplete is called. +@property (atomic, nullable, readonly) NSString *lastCompletedLaunchAppVersion; +@property (atomic, nullable, readonly) NSString *lastCompletedLaunchMainAppVersion; +@property (atomic, nullable, readonly) NSString *lastCompletedLaunchSAEAppVersion; + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)sharedInstance; + +- (void)mainAppLaunchDidComplete; +- (void)saeLaunchDidComplete; + +- (BOOL)isFirstLaunch; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/AppVersion.m b/SignalUtilitiesKit/AppVersion.m new file mode 100755 index 000000000..a671e59b6 --- /dev/null +++ b/SignalUtilitiesKit/AppVersion.m @@ -0,0 +1,133 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "AppVersion.h" +#import "NSUserDefaults+OWS.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kNSUserDefaults_FirstAppVersion = @"kNSUserDefaults_FirstAppVersion"; +NSString *const kNSUserDefaults_LastAppVersion = @"kNSUserDefaults_LastVersion"; +NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion = @"kNSUserDefaults_LastCompletedLaunchAppVersion"; +NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_MainApp + = @"kNSUserDefaults_LastCompletedLaunchAppVersion_MainApp"; +NSString *const kNSUserDefaults_LastCompletedLaunchAppVersion_SAE + = @"kNSUserDefaults_LastCompletedLaunchAppVersion_SAE"; + +@interface AppVersion () + +@property (atomic) NSString *firstAppVersion; +@property (atomic, nullable) NSString *lastAppVersion; +@property (atomic) NSString *currentAppVersion; + +@property (atomic, nullable) NSString *lastCompletedLaunchAppVersion; +@property (atomic, nullable) NSString *lastCompletedLaunchMainAppVersion; +@property (atomic, nullable) NSString *lastCompletedLaunchSAEAppVersion; + +@end + +#pragma mark - + +@implementation AppVersion + ++ (instancetype)sharedInstance +{ + static AppVersion *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [AppVersion new]; + [instance configure]; + }); + return instance; +} + +- (void)configure { + OWSAssertIsOnMainThread(); + + self.currentAppVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + + // The version of the app when it was first launched. + // nil if the app has never been launched before. + self.firstAppVersion = [[NSUserDefaults appUserDefaults] objectForKey:kNSUserDefaults_FirstAppVersion]; + // The version of the app the last time it was launched. + // nil if the app has never been launched before. + self.lastAppVersion = [[NSUserDefaults appUserDefaults] objectForKey:kNSUserDefaults_LastAppVersion]; + self.lastCompletedLaunchAppVersion = + [[NSUserDefaults appUserDefaults] objectForKey:kNSUserDefaults_LastCompletedLaunchAppVersion]; + self.lastCompletedLaunchMainAppVersion = + [[NSUserDefaults appUserDefaults] objectForKey:kNSUserDefaults_LastCompletedLaunchAppVersion_MainApp]; + self.lastCompletedLaunchSAEAppVersion = + [[NSUserDefaults appUserDefaults] objectForKey:kNSUserDefaults_LastCompletedLaunchAppVersion_SAE]; + + // Ensure the value for the "first launched version". + if (!self.firstAppVersion) { + self.firstAppVersion = self.currentAppVersion; + [[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion forKey:kNSUserDefaults_FirstAppVersion]; + } + + // Update the value for the "most recently launched version". + [[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion forKey:kNSUserDefaults_LastAppVersion]; + [[NSUserDefaults appUserDefaults] synchronize]; + + // The long version string looks like an IPv4 address. + // To prevent the log scrubber from scrubbing it, + // we replace . with _. + NSString *longVersionString = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] + stringByReplacingOccurrencesOfString:@"." + withString:@"_"]; + + OWSLogInfo(@"firstAppVersion: %@", self.firstAppVersion); + OWSLogInfo(@"lastAppVersion: %@", self.lastAppVersion); + OWSLogInfo(@"currentAppVersion: %@ (%@)", self.currentAppVersion, longVersionString); + + OWSLogInfo(@"lastCompletedLaunchAppVersion: %@", self.lastCompletedLaunchAppVersion); + OWSLogInfo(@"lastCompletedLaunchMainAppVersion: %@", self.lastCompletedLaunchMainAppVersion); + OWSLogInfo(@"lastCompletedLaunchSAEAppVersion: %@", self.lastCompletedLaunchSAEAppVersion); +} + +- (void)appLaunchDidComplete +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@"appLaunchDidComplete"); + + self.lastCompletedLaunchAppVersion = self.currentAppVersion; + + // Update the value for the "most recently launch-completed version". + [[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion + forKey:kNSUserDefaults_LastCompletedLaunchAppVersion]; + [[NSUserDefaults appUserDefaults] synchronize]; +} + +- (void)mainAppLaunchDidComplete +{ + OWSAssertIsOnMainThread(); + + self.lastCompletedLaunchMainAppVersion = self.currentAppVersion; + [[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion + forKey:kNSUserDefaults_LastCompletedLaunchAppVersion_MainApp]; + + [self appLaunchDidComplete]; +} + +- (void)saeLaunchDidComplete +{ + OWSAssertIsOnMainThread(); + + self.lastCompletedLaunchSAEAppVersion = self.currentAppVersion; + [[NSUserDefaults appUserDefaults] setObject:self.currentAppVersion + forKey:kNSUserDefaults_LastCompletedLaunchAppVersion_SAE]; + + [self appLaunchDidComplete]; +} + +- (BOOL)isFirstLaunch +{ + return self.firstAppVersion != nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Array+Description.swift b/SignalUtilitiesKit/Array+Description.swift new file mode 100644 index 000000000..3d58476dc --- /dev/null +++ b/SignalUtilitiesKit/Array+Description.swift @@ -0,0 +1,7 @@ + +public extension Array where Element : CustomStringConvertible { + + public var prettifiedDescription: String { + return "[ " + map { $0.description }.joined(separator: ", ") + " ]" + } +} diff --git a/SignalUtilitiesKit/BuildConfiguration.swift b/SignalUtilitiesKit/BuildConfiguration.swift new file mode 100644 index 000000000..807a3ab42 --- /dev/null +++ b/SignalUtilitiesKit/BuildConfiguration.swift @@ -0,0 +1,21 @@ + +public enum BuildConfiguration : String, CustomStringConvertible { + case debug, production + + public static let current: BuildConfiguration = { + #if DEBUG + return .debug + #else + return .production + #endif + }() + + public var description: String { return rawValue } +} + +@objc public final class LKBuildConfiguration : NSObject { + + override private init() { } + + @objc public static var current: String { return BuildConfiguration.current.description } +} diff --git a/SignalUtilitiesKit/ByteParser.h b/SignalUtilitiesKit/ByteParser.h new file mode 100644 index 000000000..c30c8c86c --- /dev/null +++ b/SignalUtilitiesKit/ByteParser.h @@ -0,0 +1,40 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ByteParser : NSObject + +@property (nonatomic, readonly) BOOL hasError; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithData:(NSData *)data littleEndian:(BOOL)littleEndian; + +#pragma mark - Short + +- (uint16_t)shortAtIndex:(NSUInteger)index; +- (uint16_t)nextShort; + +#pragma mark - Int + +- (uint32_t)intAtIndex:(NSUInteger)index; +- (uint32_t)nextInt; + +#pragma mark - Long + +- (uint64_t)longAtIndex:(NSUInteger)index; +- (uint64_t)nextLong; + +#pragma mark - + +- (BOOL)readZero:(NSUInteger)length; + +- (nullable NSData *)readBytes:(NSUInteger)length; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ByteParser.m b/SignalUtilitiesKit/ByteParser.m new file mode 100644 index 000000000..f70baa02a --- /dev/null +++ b/SignalUtilitiesKit/ByteParser.m @@ -0,0 +1,143 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "ByteParser.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ByteParser () + +@property (nonatomic, readonly) BOOL littleEndian; +@property (nonatomic, readonly) NSData *data; +@property (nonatomic) NSUInteger cursor; +@property (nonatomic) BOOL hasError; + +@end + +#pragma mark - + +@implementation ByteParser + +- (instancetype)initWithData:(NSData *)data littleEndian:(BOOL)littleEndian +{ + if (self = [super init]) { + _littleEndian = littleEndian; + _data = data; + } + + return self; +} + +#pragma mark - Short + +- (uint16_t)shortAtIndex:(NSUInteger)index +{ + uint16_t value; + const size_t valueSize = sizeof(value); + OWSAssertDebug(valueSize == 2); + if (index + valueSize > self.data.length) { + self.hasError = YES; + return 0; + } + [self.data getBytes:&value range:NSMakeRange(index, valueSize)]; + if (self.littleEndian) { + return CFSwapInt16LittleToHost(value); + } else { + return CFSwapInt16BigToHost(value); + } +} + +- (uint16_t)nextShort +{ + uint16_t value = [self shortAtIndex:self.cursor]; + self.cursor += sizeof(value); + return value; +} + +#pragma mark - Int + +- (uint32_t)intAtIndex:(NSUInteger)index +{ + uint32_t value; + const size_t valueSize = sizeof(value); + OWSAssertDebug(valueSize == 4); + if (index + valueSize > self.data.length) { + self.hasError = YES; + return 0; + } + [self.data getBytes:&value range:NSMakeRange(index, valueSize)]; + if (self.littleEndian) { + return CFSwapInt32LittleToHost(value); + } else { + return CFSwapInt32BigToHost(value); + } +} + +- (uint32_t)nextInt +{ + uint32_t value = [self intAtIndex:self.cursor]; + self.cursor += sizeof(value); + return value; +} + +#pragma mark - Long + +- (uint64_t)longAtIndex:(NSUInteger)index +{ + uint64_t value; + const size_t valueSize = sizeof(value); + OWSAssertDebug(valueSize == 8); + if (index + valueSize > self.data.length) { + self.hasError = YES; + return 0; + } + [self.data getBytes:&value range:NSMakeRange(index, valueSize)]; + if (self.littleEndian) { + return CFSwapInt64LittleToHost(value); + } else { + return CFSwapInt64BigToHost(value); + } +} + +- (uint64_t)nextLong +{ + uint64_t value = [self longAtIndex:self.cursor]; + self.cursor += sizeof(value); + return value; +} + +#pragma mark - + +- (BOOL)readZero:(NSUInteger)length +{ + NSData *_Nullable subdata = [self readBytes:length]; + if (!subdata) { + return NO; + } + uint8_t bytes[length]; + [subdata getBytes:bytes range:NSMakeRange(0, length)]; + for (int i = 0; i < length; i++) { + if (bytes[i] != 0) { + return NO; + } + } + return YES; +} + +- (nullable NSData *)readBytes:(NSUInteger)length +{ + NSUInteger index = self.cursor; + if (index + length > self.data.length) { + self.hasError = YES; + return nil; + } + NSData *_Nullable subdata = [self.data subdataWithRange:NSMakeRange(index, length)]; + self.cursor += length; + return subdata; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/CDSQuote.h b/SignalUtilitiesKit/CDSQuote.h new file mode 100644 index 000000000..7ba472c3b --- /dev/null +++ b/SignalUtilitiesKit/CDSQuote.h @@ -0,0 +1,34 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CDSQuote : NSObject + +@property (nonatomic, readonly) uint16_t version; +@property (nonatomic, readonly) uint16_t signType; +@property (nonatomic, readonly) BOOL isSigLinkable; +@property (nonatomic, readonly) uint32_t gid; +@property (nonatomic, readonly) uint16_t qeSvn; +@property (nonatomic, readonly) uint16_t pceSvn; +@property (nonatomic, readonly) NSData *basename; +@property (nonatomic, readonly) NSData *cpuSvn; +@property (nonatomic, readonly) uint64_t flags; +@property (nonatomic, readonly) uint64_t xfrm; +@property (nonatomic, readonly) NSData *mrenclave; +@property (nonatomic, readonly) NSData *mrsigner; +@property (nonatomic, readonly) uint16_t isvProdId; +@property (nonatomic, readonly) uint16_t isvSvn; +@property (nonatomic, readonly) NSData *reportData; +@property (nonatomic, readonly) NSData *signature; + ++ (nullable CDSQuote *)parseQuoteFromData:(NSData *)quoteData; + +- (BOOL)isDebugQuote; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/CDSQuote.m b/SignalUtilitiesKit/CDSQuote.m new file mode 100644 index 000000000..08d6d59e6 --- /dev/null +++ b/SignalUtilitiesKit/CDSQuote.m @@ -0,0 +1,192 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "CDSQuote.h" +#import "ByteParser.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +static const long SGX_FLAGS_INITTED = 0x0000000000000001L; +static const long SGX_FLAGS_DEBUG = 0x0000000000000002L; +static const long SGX_FLAGS_MODE64BIT = 0x0000000000000004L; +static const long __unused SGX_FLAGS_PROVISION_KEY = 0x0000000000000004L; +static const long __unused SGX_FLAGS_EINITTOKEN_KEY = 0x0000000000000004L; +static const long SGX_FLAGS_RESERVED = 0xFFFFFFFFFFFFFFC8L; +static const long __unused SGX_XFRM_LEGACY = 0x0000000000000003L; +static const long __unused SGX_XFRM_AVX = 0x0000000000000006L; +static const long SGX_XFRM_RESERVED = 0xFFFFFFFFFFFFFFF8L; + +#pragma mark - + +@interface CDSQuote () + +@property (nonatomic) uint16_t version; +@property (nonatomic) uint16_t signType; +@property (nonatomic) BOOL isSigLinkable; +@property (nonatomic) uint32_t gid; +@property (nonatomic) uint16_t qeSvn; +@property (nonatomic) uint16_t pceSvn; +@property (nonatomic) NSData *basename; +@property (nonatomic) NSData *cpuSvn; +@property (nonatomic) uint64_t flags; +@property (nonatomic) uint64_t xfrm; +@property (nonatomic) NSData *mrenclave; +@property (nonatomic) NSData *mrsigner; +@property (nonatomic) uint16_t isvProdId; +@property (nonatomic) uint16_t isvSvn; +@property (nonatomic) NSData *reportData; +@property (nonatomic) NSData *signature; + +@end + +#pragma mark - + +@implementation CDSQuote + ++ (nullable CDSQuote *)parseQuoteFromData:(NSData *)quoteData +{ + ByteParser *_Nullable parser = [[ByteParser alloc] initWithData:quoteData littleEndian:YES]; + + // NOTE: This version is separate from and does _NOT_ match the signature body entity version. + uint16_t version = parser.nextShort; + if (version < 1 || version > 2) { + OWSFailDebug(@"unexpected quote version: %d", (int)version); + return nil; + } + + uint16_t signType = parser.nextShort; + if ((signType & ~1) != 0) { + OWSFailDebug(@"invalid signType: %d", (int)signType); + return nil; + } + + BOOL isSigLinkable = signType == 1; + uint32_t gid = parser.nextInt; + uint16_t qeSvn = parser.nextShort; + + uint16_t pceSvn = 0; + if (version > 1) { + pceSvn = parser.nextShort; + } else { + if (![parser readZero:2]) { + OWSFailDebug(@"non-zero pceSvn."); + return nil; + } + } + + if (![parser readZero:4]) { + OWSFailDebug(@"non-zero xeid."); + return nil; + } + + NSData *_Nullable basename = [parser readBytes:32]; + if (!basename) { + OWSFailDebug(@"couldn't read basename."); + return nil; + } + + // report_body + + NSData *_Nullable cpuSvn = [parser readBytes:16]; + if (!cpuSvn) { + OWSFailDebug(@"couldn't read cpuSvn."); + return nil; + } + if (![parser readZero:4]) { + OWSFailDebug(@"non-zero misc_select."); + return nil; + } + if (![parser readZero:28]) { + OWSFailDebug(@"non-zero reserved1."); + return nil; + } + + uint64_t flags = parser.nextLong; + if ((flags & SGX_FLAGS_RESERVED) != 0 || (flags & SGX_FLAGS_INITTED) == 0 || (flags & SGX_FLAGS_MODE64BIT) == 0) { + OWSFailDebug(@"invalid flags."); + return nil; + } + + uint64_t xfrm = parser.nextLong; + if ((xfrm & SGX_XFRM_RESERVED) != 0) { + OWSFailDebug(@"invalid xfrm."); + return nil; + } + + NSData *_Nullable mrenclave = [parser readBytes:32]; + if (!mrenclave) { + OWSFailDebug(@"couldn't read mrenclave."); + return nil; + } + if (![parser readZero:32]) { + OWSFailDebug(@"non-zero reserved2."); + return nil; + } + NSData *_Nullable mrsigner = [parser readBytes:32]; + if (!mrsigner) { + OWSFailDebug(@"couldn't read mrsigner."); + return nil; + } + if (![parser readZero:96]) { + OWSFailDebug(@"non-zero reserved3."); + return nil; + } + uint16_t isvProdId = parser.nextShort; + uint16_t isvSvn = parser.nextShort; + if (![parser readZero:60]) { + OWSFailDebug(@"non-zero reserved4."); + return nil; + } + NSData *_Nullable reportData = [parser readBytes:64]; + if (!reportData) { + OWSFailDebug(@"couldn't read reportData."); + return nil; + } + + // quote signature + uint32_t signatureLength = parser.nextInt; + if (signatureLength != quoteData.length - 436) { + OWSFailDebug(@"invalid signatureLength."); + return nil; + } + NSData *_Nullable signature = [parser readBytes:signatureLength]; + if (!signature) { + OWSFailDebug(@"couldn't read signature."); + return nil; + } + + if (parser.hasError) { + return nil; + } + + CDSQuote *quote = [CDSQuote new]; + quote.version = version; + quote.signType = signType; + quote.isSigLinkable = isSigLinkable; + quote.gid = gid; + quote.qeSvn = qeSvn; + quote.pceSvn = pceSvn; + quote.basename = basename; + quote.cpuSvn = cpuSvn; + quote.flags = flags; + quote.xfrm = xfrm; + quote.mrenclave = mrenclave; + quote.mrsigner = mrsigner; + quote.isvProdId = isvProdId; + quote.isvSvn = isvSvn; + quote.reportData = reportData; + quote.signature = signature; + + return quote; +} + +- (BOOL)isDebugQuote +{ + return (self.flags & SGX_FLAGS_DEBUG) != 0; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/CDSSigningCertificate.h b/SignalUtilitiesKit/CDSSigningCertificate.h new file mode 100644 index 000000000..3fd3ccf29 --- /dev/null +++ b/SignalUtilitiesKit/CDSSigningCertificate.h @@ -0,0 +1,32 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, CDSSigningCertificateErrorCode) { + // AssertionError's indicate either developer or some serious system error that should never happen. + // + // Do not use this for an "expected" error, e.g. something that could be induced by user input which + // we specifically need to handle gracefull. + CDSSigningCertificateError_AssertionError = 1, + + CDSSigningCertificateError_InvalidPEMSupplied, + CDSSigningCertificateError_CouldNotExtractLeafCertificate, + CDSSigningCertificateError_InvalidDistinguishedName, + CDSSigningCertificateError_UntrustedCertificate +}; + +NSError *CDSSigningCertificateErrorMake(CDSSigningCertificateErrorCode code, NSString *localizedDescription); + +@interface CDSSigningCertificate : NSObject + ++ (nullable CDSSigningCertificate *)parseCertificateFromPem:(NSString *)certificatePem error:(NSError **)error; + +- (BOOL)verifySignatureOfBody:(NSString *)body signature:(NSData *)theirSignature; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/CDSSigningCertificate.m b/SignalUtilitiesKit/CDSSigningCertificate.m new file mode 100644 index 000000000..edaa1e51b --- /dev/null +++ b/SignalUtilitiesKit/CDSSigningCertificate.m @@ -0,0 +1,391 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "CDSSigningCertificate.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSError *CDSSigningCertificateErrorMake(CDSSigningCertificateErrorCode code, NSString *localizedDescription) +{ + return [NSError errorWithDomain:@"CDSSigningCertificate" + code:code + userInfo:@{ NSLocalizedDescriptionKey : localizedDescription }]; +} + +@interface CDSSigningCertificate () + +@property (nonatomic) SecPolicyRef policy; +@property (nonatomic) SecTrustRef trust; +@property (nonatomic) SecKeyRef publicKey; + +@end + +#pragma mark - + +@implementation CDSSigningCertificate + +- (instancetype)init +{ + if (self = [super init]) { + _policy = NULL; + _trust = NULL; + _publicKey = NULL; + } + + return self; +} + +- (void)dealloc +{ + if (_policy) { + CFRelease(_policy); + _policy = NULL; + } + if (_trust) { + CFRelease(_trust); + _trust = NULL; + } + if (_publicKey) { + CFRelease(_publicKey); + _publicKey = NULL; + } +} + ++ (nullable CDSSigningCertificate *)parseCertificateFromPem:(NSString *)certificatePem error:(NSError **)error +{ + OWSAssertDebug(certificatePem); + *error = nil; + + CDSSigningCertificate *signingCertificate = [CDSSigningCertificate new]; + + NSArray *_Nullable anchorCertificates = [self anchorCertificates]; + if (anchorCertificates.count < 1) { + OWSFailDebug(@"Could not load anchor certificates."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"Could not load anchor certificates."); + return nil; + } + + NSArray *_Nullable certificateDerDatas = [self convertPemToDer:certificatePem]; + + if (certificateDerDatas.count < 1) { + OWSFailDebug(@"Could not parse PEM."); + *error = CDSSigningCertificateErrorMake(CDSSigningCertificateError_InvalidPEMSupplied, @"Could not parse PEM."); + return nil; + } + + // The leaf is always the first certificate. + NSData *_Nullable leafCertificateData = [certificateDerDatas firstObject]; + if (!leafCertificateData) { + OWSFailDebug(@"Could not extract leaf certificate data."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_CouldNotExtractLeafCertificate, @"Could not extract leaf certificate data."); + return nil; + } + if (![self verifyDistinguishedNameOfCertificate:leafCertificateData]) { + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_InvalidDistinguishedName, @"Could not extract leaf certificate data."); + return nil; + } + + NSMutableArray *certificates = [NSMutableArray new]; + for (NSData *certificateDerData in certificateDerDatas) { + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(certificateDerData)); + if (!certificate) { + OWSFailDebug(@"Could not create SecCertificate."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"Could not create SecCertificate."); + return nil; + } + [certificates addObject:(__bridge_transfer id)certificate]; + } + + SecPolicyRef policy = SecPolicyCreateBasicX509(); + signingCertificate.policy = policy; + if (!policy) { + OWSFailDebug(@"Could not create policy."); + *error = CDSSigningCertificateErrorMake(CDSSigningCertificateError_AssertionError, @"Could not create policy."); + return nil; + } + + SecTrustRef trust; + OSStatus status = SecTrustCreateWithCertificates((__bridge CFTypeRef)certificates, policy, &trust); + signingCertificate.trust = trust; + if (status != errSecSuccess) { + OWSFailDebug(@"Creating trust did not succeed."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"Creating trust did not succeed."); + return nil; + } + if (!trust) { + OWSFailDebug(@"Could not create trust."); + *error = CDSSigningCertificateErrorMake(CDSSigningCertificateError_AssertionError, @"Could not create trust."); + return nil; + } + + status = SecTrustSetNetworkFetchAllowed(trust, NO); + if (status != errSecSuccess) { + OWSFailDebug(@"trust fetch could not be configured."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"trust fetch could not be configured."); + return nil; + } + + status = SecTrustSetAnchorCertificatesOnly(trust, YES); + if (status != errSecSuccess) { + OWSFailDebug(@"trust anchor certs could not be configured."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"trust anchor certs could not be configured."); + return nil; + } + + NSMutableArray *pinnedCertificates = [NSMutableArray array]; + for (NSData *certificateData in anchorCertificates) { + SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(certificateData)); + if (!certificate) { + OWSFailDebug(@"Could not create pinned SecCertificate."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"Could not create pinned SecCertificate."); + return nil; + } + + [pinnedCertificates addObject:(__bridge_transfer id)certificate]; + } + status = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates); + if (status != errSecSuccess) { + OWSFailDebug(@"The anchor certificates couldn't be set."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"The anchor certificates couldn't be set."); + return nil; + } + + SecTrustResultType result; + status = SecTrustEvaluate(trust, &result); + if (status != errSecSuccess) { + OWSFailDebug(@"Could not evaluate certificates."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"Could not evaluate certificates."); + return nil; + } + + // `kSecTrustResultUnspecified` is confusingly named. It indicates success. + // See the comments in the header where it is defined. + BOOL isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed); + if (!isValid) { + OWSFailDebug(@"Certificate was not trusted."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_UntrustedCertificate, @"Certificate was not trusted."); + return nil; + } + + SecKeyRef publicKey = SecTrustCopyPublicKey(trust); + signingCertificate.publicKey = publicKey; + if (!publicKey) { + OWSFailDebug(@"Could not extract public key."); + *error = CDSSigningCertificateErrorMake( + CDSSigningCertificateError_AssertionError, @"Could not extract public key."); + return nil; + } + + return signingCertificate; +} + +// PEM is just a series of blocks of base-64 encoded DER data. +// +// https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail ++ (nullable NSArray *)convertPemToDer:(NSString *)pemString +{ + NSMutableArray *certificateDatas = [NSMutableArray new]; + + NSError *error; + // We use ? for non-greedy matching. + NSRegularExpression *_Nullable regex = [NSRegularExpression + regularExpressionWithPattern:@"-----BEGIN.*?-----(.+?)-----END.*?-----" + options:NSRegularExpressionCaseInsensitive | NSRegularExpressionDotMatchesLineSeparators + error:&error]; + if (!regex || error) { + OWSFailDebug(@"could parse regex: %@.", error); + return nil; + } + + [regex enumerateMatchesInString:pemString + options:0 + range:NSMakeRange(0, pemString.length) + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *stop) { + if (result.numberOfRanges != 2) { + OWSFailDebug(@"invalid PEM regex match."); + return; + } + NSString *_Nullable derString = [pemString substringWithRange:[result rangeAtIndex:1]]; + if (derString.length < 1) { + OWSFailDebug(@"empty PEM match."); + return; + } + // dataFromBase64String will ignore whitespace, which is + // necessary. + NSData *_Nullable derData = [NSData dataFromBase64String:derString]; + if (derData.length < 1) { + return; + } + [certificateDatas addObject:derData]; + }]; + + return certificateDatas; +} + ++ (nullable NSArray *)anchorCertificates +{ + static NSArray *anchorCertificates = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // We need to use an Intel certificate as the anchor for IAS verification. + NSData *_Nullable anchorCertificate = [self certificateDataForService:@"ias-root"]; + if (!anchorCertificate) { + OWSFail(@"could not load anchor certificate."); + } else { + anchorCertificates = @[ anchorCertificate ]; + } + }); + return anchorCertificates; +} + ++ (nullable NSData *)certificateDataForService:(NSString *)service +{ + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + NSString *path = [bundle pathForResource:service ofType:@"cer"]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + OWSFailDebug(@"could not locate certificate file."); + return nil; + } + + NSData *_Nullable certificateData = [NSData dataWithContentsOfFile:path]; + return certificateData; +} + +- (BOOL)verifySignatureOfBody:(NSString *)body signature:(NSData *)signature +{ + OWSAssertDebug(self.publicKey); + + NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding]; + + size_t signedHashBytesSize = SecKeyGetBlockSize(self.publicKey); + const void *signedHashBytes = [signature bytes]; + + NSData *_Nullable hashData = [Cryptography computeSHA256Digest:bodyData]; + if (hashData.length != CC_SHA256_DIGEST_LENGTH) { + OWSFailDebug(@"could not SHA256 for signature verification."); + return NO; + } + size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH; + const void *hashBytes = [hashData bytes]; + + OSStatus status = SecKeyRawVerify( + self.publicKey, kSecPaddingPKCS1SHA256, hashBytes, hashBytesSize, signedHashBytes, signedHashBytesSize); + + BOOL isValid = status == errSecSuccess; + if (!isValid) { + return NO; + } + return YES; +} + ++ (BOOL)verifyDistinguishedNameOfCertificate:(NSData *)certificateData +{ + OWSAssertDebug(certificateData); + + // The Security framework doesn't offer access to certificate properties + // with API available on iOS 9. We use OpenSSL to extract the name. + NSDictionary *_Nullable properties = [self propertiesForCertificate:certificateData]; + if (!properties) { + OWSFailDebug(@"Could not retrieve certificate properties."); + return NO; + } + // NSString *expectedDistinguishedName + // = @"CN=Intel SGX Attestation Report Signing,O=Intel Corporation,L=Santa Clara,ST=CA,C=US"; + NSDictionary *expectedProperties = @{ + @(SN_commonName) : // "CN" + @"Intel SGX Attestation Report Signing", + @(SN_organizationName) : // "O" + @"Intel Corporation", + @(SN_localityName) : // "L" + @"Santa Clara", + @(SN_stateOrProvinceName) : // "ST" + @"CA", + @(SN_countryName) : // "C" + @"US", + }; + + if (![properties isEqualToDictionary:expectedProperties]) { + return NO; + } + return YES; +} + ++ (nullable NSDictionary *)propertiesForCertificate:(NSData *)certificateData +{ + OWSAssertDebug(certificateData); + + if (certificateData.length >= UINT32_MAX) { + OWSFailDebug(@"certificate data is too long."); + return nil; + } + const unsigned char *certificateDataBytes = (const unsigned char *)[certificateData bytes]; + X509 *_Nullable certificateX509 = d2i_X509(NULL, &certificateDataBytes, [certificateData length]); + if (!certificateX509) { + OWSFailDebug(@"could not parse certificate."); + return nil; + } + + X509_NAME *_Nullable subjectName = X509_get_subject_name(certificateX509); + if (!subjectName) { + OWSFailDebug(@"could not extract subject name."); + return nil; + } + + NSMutableDictionary *certificateProperties = [NSMutableDictionary new]; + for (NSString *oid in @[ + @(SN_commonName), // "CN" + @(SN_organizationName), // "O" + @(SN_localityName), // "L" + @(SN_stateOrProvinceName), // "ST" + @(SN_countryName), // "C" + ]) { + int nid = OBJ_txt2nid(oid.UTF8String); + int index = X509_NAME_get_index_by_NID(subjectName, nid, -1); + + X509_NAME_ENTRY *_Nullable entry = X509_NAME_get_entry(subjectName, index); + if (!entry) { + OWSFailDebug(@"could not extract entry."); + return nil; + } + + ASN1_STRING *_Nullable entryData = X509_NAME_ENTRY_get_data(entry); + if (!entryData) { + OWSFailDebug(@"could not extract entry data."); + return nil; + } + + unsigned char *entryName = ASN1_STRING_data(entryData); + if (entryName == NULL) { + OWSFailDebug(@"could not extract entry string."); + return nil; + } + NSString *_Nullable entryString = [NSString stringWithUTF8String:(char *)entryName]; + if (!entryString) { + OWSFailDebug(@"could not parse entry name data."); + return nil; + } + certificateProperties[oid] = entryString; + } + return certificateProperties; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ClosedGroupParser.swift b/SignalUtilitiesKit/ClosedGroupParser.swift new file mode 100644 index 000000000..3118f569d --- /dev/null +++ b/SignalUtilitiesKit/ClosedGroupParser.swift @@ -0,0 +1,30 @@ + +@objc public final class ClosedGroupParser : NSObject { + private let data: Data + + @objc public init(data: Data) { + self.data = data + } + + @objc public func parseGroupModels() -> [TSGroupModel] { + var index = 0 + var result: [TSGroupModel] = [] + while index < data.endIndex { + var uncheckedSize: UInt32? = try? data[index..<(index+4)].withUnsafeBytes { $0.pointee } + if let size = uncheckedSize, size >= data.count, let intermediate = try? data[index..<(index+4)].reversed() { + uncheckedSize = Data(intermediate).withUnsafeBytes { $0.pointee } + } + guard let size = uncheckedSize, size < data.count else { break } + let sizeAsInt = Int(size) + index += 4 + guard index + sizeAsInt <= data.count else { break } + let protoAsData = data[index..<(index+sizeAsInt)] + guard let proto = try? SSKProtoGroupDetails.parseData(protoAsData) else { break } + index += sizeAsInt + var groupModel = TSGroupModel(title: proto.name, memberIds: proto.members, image: nil, + groupId: proto.id, groupType: GroupType.closedGroup, adminIds: proto.admins) + result.append(groupModel) + } + return result + } +} diff --git a/SignalUtilitiesKit/ClosedGroupPoller.swift b/SignalUtilitiesKit/ClosedGroupPoller.swift new file mode 100644 index 000000000..c9ec1c9b5 --- /dev/null +++ b/SignalUtilitiesKit/ClosedGroupPoller.swift @@ -0,0 +1,79 @@ +import PromiseKit + +@objc(LKClosedGroupPoller) +public final class ClosedGroupPoller : NSObject { + private var isPolling = false + private var timer: Timer? + + // MARK: Settings + private static let pollInterval: TimeInterval = 2 + + // MARK: Error + private enum Error : LocalizedError { + case insufficientSnodes + case pollingCanceled + + internal var errorDescription: String? { + switch self { + case .insufficientSnodes: return "No snodes left to poll." + case .pollingCanceled: return "Polling canceled." + } + } + } + + // MARK: Public API + @objc public func startIfNeeded() { + AssertIsOnMainThread() // Timers don't do well on background queues + guard !isPolling else { return } + isPolling = true + timer = Timer.scheduledTimer(withTimeInterval: ClosedGroupPoller.pollInterval, repeats: true) { [weak self] _ in + self?.poll() + } + } + + public func pollOnce() -> [Promise] { + guard !isPolling else { return [] } + isPolling = true + return poll() + } + + @objc public func stop() { + isPolling = false + timer?.invalidate() + } + + // MARK: Private API + private func poll() -> [Promise] { + guard isPolling else { return [] } + let publicKeys = Storage.getUserClosedGroupPublicKeys() + return publicKeys.map { publicKey in + let promise = SnodeAPI.getSwarm(for: publicKey).then2 { [weak self] swarm -> Promise<[JSON]> in + // randomElement() uses the system's default random generator, which is cryptographically secure + guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) } + guard let self = self, self.isPolling else { return Promise(error: Error.pollingCanceled) } + return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).map2 { + SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: publicKey) + } + } + promise.done2 { [weak self] messages in + guard let self = self, self.isPolling else { return } + if !messages.isEmpty { + print("[Loki] Received \(messages.count) new message(s) in closed group with public key: \(publicKey).") + } + messages.forEach { json in + guard let envelope = SSKProtoEnvelope.from(json) else { return } + do { + let data = try envelope.serializedData() + SSKEnvironment.shared.messageReceiver.handleReceivedEnvelopeData(data) + } catch { + print("[Loki] Failed to deserialize envelope due to error: \(error).") + } + } + } + promise.catch2 { error in + print("[Loki] Polling failed for closed group with public key: \(publicKey) due to error: \(error).") + } + return promise.map { _ in } + } + } +} diff --git a/SignalUtilitiesKit/ClosedGroupUpdateMessage.swift b/SignalUtilitiesKit/ClosedGroupUpdateMessage.swift new file mode 100644 index 000000000..41006d707 --- /dev/null +++ b/SignalUtilitiesKit/ClosedGroupUpdateMessage.swift @@ -0,0 +1,132 @@ + +@objc(LKClosedGroupUpdateMessage) +internal final class ClosedGroupUpdateMessage : TSOutgoingMessage { + private let kind: Kind + + // MARK: Settings + @objc internal override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .closedGroupUpdate)) } + + @objc internal override func shouldBeSaved() -> Bool { return false } + @objc internal override func shouldSyncTranscript() -> Bool { return false } + + // MARK: Kind + internal enum Kind { + case new(groupPublicKey: Data, name: String, groupPrivateKey: Data, senderKeys: [ClosedGroupSenderKey], members: [Data], admins: [Data]) + case info(groupPublicKey: Data, name: String, senderKeys: [ClosedGroupSenderKey], members: [Data], admins: [Data]) + case senderKeyRequest(groupPublicKey: Data) + case senderKey(groupPublicKey: Data, senderKey: ClosedGroupSenderKey) + } + + // MARK: Initialization + internal init(thread: TSThread, kind: Kind) { + self.kind = kind + super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "", + attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false, + groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil) + } + + required init(dictionary: [String:Any]) throws { + preconditionFailure("Use init(thread:kind:) instead.") + } + + // MARK: Coding + internal required init?(coder: NSCoder) { + guard let thread = coder.decodeObject(forKey: "thread") as? TSThread, + let timestamp = coder.decodeObject(forKey: "timestamp") as? UInt64, + let groupPublicKey = coder.decodeObject(forKey: "groupPublicKey") as? Data, + let rawKind = coder.decodeObject(forKey: "kind") as? String else { return nil } + switch rawKind { + case "new": + guard let name = coder.decodeObject(forKey: "name") as? String, + let groupPrivateKey = coder.decodeObject(forKey: "groupPrivateKey") as? Data, + let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey], + let members = coder.decodeObject(forKey: "members") as? [Data], + let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil } + self.kind = .new(groupPublicKey: groupPublicKey, name: name, groupPrivateKey: groupPrivateKey, senderKeys: senderKeys, members: members, admins: admins) + case "info": + guard let name = coder.decodeObject(forKey: "name") as? String, + let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey], + let members = coder.decodeObject(forKey: "members") as? [Data], + let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil } + self.kind = .info(groupPublicKey: groupPublicKey, name: name, senderKeys: senderKeys, members: members, admins: admins) + case "senderKeyRequest": + self.kind = .senderKeyRequest(groupPublicKey: groupPublicKey) + case "senderKey": + guard let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey], + let senderKey = senderKeys.first else { return nil } + self.kind = .senderKey(groupPublicKey: groupPublicKey, senderKey: senderKey) + default: return nil + } + super.init(outgoingMessageWithTimestamp: timestamp, in: thread, messageBody: "", + attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false, + groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil) + } + + internal override func encode(with coder: NSCoder) { + coder.encode(thread, forKey: "thread") + coder.encode(timestamp, forKey: "timestamp") + switch kind { + case .new(let groupPublicKey, let name, let groupPrivateKey, let senderKeys, let members, let admins): + coder.encode("new", forKey: "kind") + coder.encode(groupPublicKey, forKey: "groupPublicKey") + coder.encode(name, forKey: "name") + coder.encode(groupPrivateKey, forKey: "groupPrivateKey") + coder.encode(senderKeys, forKey: "senderKeys") + coder.encode(members, forKey: "members") + coder.encode(admins, forKey: "admins") + case .info(let groupPublicKey, let name, let senderKeys, let members, let admins): + coder.encode("info", forKey: "kind") + coder.encode(groupPublicKey, forKey: "groupPublicKey") + coder.encode(name, forKey: "name") + coder.encode(senderKeys, forKey: "senderKeys") + coder.encode(members, forKey: "members") + coder.encode(admins, forKey: "admins") + case .senderKeyRequest(let groupPublicKey): + coder.encode(groupPublicKey, forKey: "groupPublicKey") + case .senderKey(let groupPublicKey, let senderKey): + coder.encode("senderKey", forKey: "kind") + coder.encode(groupPublicKey, forKey: "groupPublicKey") + coder.encode([ senderKey ], forKey: "senderKeys") + } + } + + // MARK: Building + @objc internal override func dataMessageBuilder() -> Any? { + guard let builder = super.dataMessageBuilder() as? SSKProtoDataMessage.SSKProtoDataMessageBuilder else { return nil } + do { + let closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate.SSKProtoDataMessageClosedGroupUpdateBuilder + switch kind { + case .new(let groupPublicKey, let name, let groupPrivateKey, let senderKeys, let members, let admins): + closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .new) + closedGroupUpdate.setName(name) + closedGroupUpdate.setGroupPrivateKey(groupPrivateKey) + closedGroupUpdate.setSenderKeys(try senderKeys.map { try $0.toProto() }) + closedGroupUpdate.setMembers(members) + closedGroupUpdate.setAdmins(admins) + case .info(let groupPublicKey, let name, let senderKeys, let members, let admins): + closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .info) + closedGroupUpdate.setName(name) + closedGroupUpdate.setSenderKeys(try senderKeys.map { try $0.toProto() }) + closedGroupUpdate.setMembers(members) + closedGroupUpdate.setAdmins(admins) + case .senderKeyRequest(let groupPublicKey): + closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .senderKeyRequest) + case .senderKey(let groupPublicKey, let senderKey): + closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .senderKey) + closedGroupUpdate.setSenderKeys([ try senderKey.toProto() ]) + } + builder.setClosedGroupUpdate(try closedGroupUpdate.build()) + } catch { + owsFailDebug("Failed to build closed group update due to error: \(error).") + return nil + } + return builder + } +} + +private extension ClosedGroupSenderKey { + + func toProto() throws -> SSKProtoDataMessageClosedGroupUpdateSenderKey { + return try SSKProtoDataMessageClosedGroupUpdateSenderKey.builder(chainKey: chainKey, keyIndex: UInt32(keyIndex), publicKey: publicKey).build() + } +} diff --git a/SignalUtilitiesKit/ClosedGroupUtilities.swift b/SignalUtilitiesKit/ClosedGroupUtilities.swift new file mode 100644 index 000000000..c80383cf3 --- /dev/null +++ b/SignalUtilitiesKit/ClosedGroupUtilities.swift @@ -0,0 +1,71 @@ +import CryptoSwift + + +@objc(LKClosedGroupUtilities) +public final class ClosedGroupUtilities : NSObject { + + @objc(LKSSKDecryptionError) + public class SSKDecryptionError : NSError { // Not called `Error` for Obj-C interoperablity + + @objc public static let invalidGroupPublicKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Invalid group public key." ]) + @objc public static let noData = SSKDecryptionError(domain: "SSKErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Received an empty envelope." ]) + @objc public static let noGroupPrivateKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Missing group private key." ]) + @objc public static let selfSend = SSKDecryptionError(domain: "SSKErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Message addressed at self." ]) + } + + @objc(encryptData:usingGroupPublicKey:transaction:error:) + public static func encrypt(data: Data, groupPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Data { + // 1. ) Encrypt the data with the user's sender key + guard let userPublicKey = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey else { + throw SMKError.assertionError(description: "[Loki] Couldn't find user key pair.") + } + let ciphertextAndKeyIndex = try SharedSenderKeysImplementation.shared.encrypt(data, forGroupWithPublicKey: groupPublicKey, + senderPublicKey: userPublicKey, protocolContext: transaction) + let ivAndCiphertext = ciphertextAndKeyIndex[0] as! Data + let keyIndex = ciphertextAndKeyIndex[1] as! UInt + let encryptedMessage = ClosedGroupCiphertextMessage(_throws_withIVAndCiphertext: ivAndCiphertext, senderPublicKey: Data(hex: userPublicKey), keyIndex: UInt32(keyIndex)) + // 2. ) Encrypt the result for the group's public key to hide the sender public key and key index + let (ciphertext, _, ephemeralPublicKey) = try EncryptionUtilities.encrypt(encryptedMessage.serialized, using: groupPublicKey.removing05PrefixIfNeeded()) + // 3. ) Wrap the result + return try SSKProtoClosedGroupCiphertextMessageWrapper.builder(ciphertext: ciphertext, ephemeralPublicKey: ephemeralPublicKey).build().serializedData() + } + + @objc(decryptEnvelope:transaction:error:) + public static func decrypt(envelope: SSKProtoEnvelope, transaction: YapDatabaseReadWriteTransaction) throws -> [Any] { + let (plaintext, senderPublicKey) = try decrypt(envelope: envelope, transaction: transaction) + return [ plaintext, senderPublicKey ] + } + + public static func decrypt(envelope: SSKProtoEnvelope, transaction: YapDatabaseReadWriteTransaction) throws -> (plaintext: Data, senderPublicKey: String) { + // 1. ) Check preconditions + guard let groupPublicKey = envelope.source, SharedSenderKeysImplementation.shared.isClosedGroup(groupPublicKey) else { + throw SSKDecryptionError.invalidGroupPublicKey + } + guard let data = envelope.content else { + throw SSKDecryptionError.noData + } + guard let hexEncodedGroupPrivateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey) else { + throw SSKDecryptionError.noGroupPrivateKey + } + let groupPrivateKey = Data(hex: hexEncodedGroupPrivateKey) + // 2. ) Parse the wrapper + let wrapper = try SSKProtoClosedGroupCiphertextMessageWrapper.parseData(data) + let ivAndCiphertext = wrapper.ciphertext + let ephemeralPublicKey = wrapper.ephemeralPublicKey + // 3. ) Decrypt the data inside + let groupKeyPair = ECKeyPair(publicKey: Data(hex: groupPublicKey), privateKey: groupPrivateKey) + let ephemeralSharedSecret = Curve25519.generateSharedSecret(fromPublicKey: ephemeralPublicKey, andKeyPair: groupKeyPair)! + let salt = "LOKI" + let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes) + let closedGroupCiphertextMessageAsData = try DecryptionUtilities.decrypt(ivAndCiphertext, usingAESGCMWithSymmetricKey: Data(symmetricKey)) + // 4. ) Parse the closed group ciphertext message + let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData) + let senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() + guard senderPublicKey != getUserHexEncodedPublicKey() else { throw SSKDecryptionError.selfSend } + // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content + let plaintext = try SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, forGroupWithPublicKey: groupPublicKey, + senderPublicKey: senderPublicKey, keyIndex: UInt(closedGroupCiphertextMessage.keyIndex), protocolContext: transaction) + // 6. ) Return + return (plaintext, senderPublicKey) + } +} diff --git a/SignalUtilitiesKit/ClosedGroupsProtocol.swift b/SignalUtilitiesKit/ClosedGroupsProtocol.swift new file mode 100644 index 000000000..f1fb0f478 --- /dev/null +++ b/SignalUtilitiesKit/ClosedGroupsProtocol.swift @@ -0,0 +1,486 @@ +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. + +/// See [the documentation](https://github.com/loki-project/session-protocol-docs/wiki/Medium-Size-Groups) for more information. +@objc(LKClosedGroupsProtocol) +public final class ClosedGroupsProtocol : NSObject { + public static let isSharedSenderKeysEnabled = true + public static let groupSizeLimit = 20 + public static let maxNameSize = 64 + + public enum Error : LocalizedError { + case noThread + case noPrivateKey + case invalidUpdate + + public var errorDescription: String? { + switch self { + case .noThread: return "Couldn't find a thread associated with the given group public key." + case .noPrivateKey: return "Couldn't find a private key associated with the given group public key." + case .invalidUpdate: return "Invalid group update." + } + } + } + + // MARK: - Sending + + /// - Note: It's recommended to batch fetch the device links for the given set of members before invoking this, to avoid the message sending pipeline + /// making a request for each member. + public static func createClosedGroup(name: String, members: Set, transaction: YapDatabaseReadWriteTransaction) -> Promise { + // Prepare + var members = members + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + let userPublicKey = getUserHexEncodedPublicKey() + // Generate a key pair for the group + let groupKeyPair = Curve25519.generateKeyPair()! + let groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix + // Ensure the current user's master device is the one that's included in the member list + members.remove(userPublicKey) + members.insert(UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey) + let membersAsData = members.map { Data(hex: $0) } + // Create ratchets for all members (and their linked devices) + var membersAndLinkedDevices: Set = members + for member in members { + let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction) + membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }) + } + let senderKeys: [ClosedGroupSenderKey] = membersAndLinkedDevices.map { publicKey in + let ratchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) + return ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) + } + // Create the group + let admins = [ UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey ] + let adminsAsData = admins.map { Data(hex: $0) } + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let group = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins) + let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction) + thread.usesSharedSenderKeys = true + thread.save(with: transaction) + SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread) + // Establish sessions if needed + establishSessionsIfNeeded(with: [String](members), using: transaction) // Not `membersAndLinkedDevices` as this internally takes care of multi device already + // Send a closed group update message to all members (and their linked devices) using established channels + var promises: [Promise] = [] + for member in members { // Not `membersAndLinkedDevices` as this internally takes care of multi device already + guard member != userPublicKey else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, + groupPrivateKey: groupKeyPair.privateKey(), senderKeys: senderKeys, members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + promises.append(SSKEnvironment.shared.messageSender.sendPromise(message: closedGroupUpdateMessage)) + } + // Add the group to the user's set of public keys to poll for + Storage.setClosedGroupPrivateKey(groupKeyPair.privateKey().toHexString(), for: groupPublicKey, using: transaction) + // Notify the PN server + promises.append(LokiPushNotificationManager.performOperation(.subscribe, for: groupPublicKey, publicKey: userPublicKey)) + // Notify the user + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate) + infoMessage.save(with: transaction) + // Return + return when(fulfilled: promises).map2 { thread } + } + + /// - Note: The returned promise is only relevant for group leaving. + public static func update(_ groupPublicKey: String, with members: Set, name: String, transaction: YapDatabaseReadWriteTransaction) -> Promise { + let (promise, seal) = Promise.pending() + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + let userPublicKey = getUserHexEncodedPublicKey() + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { + print("[Loki] Can't update nonexistent closed group.") + return Promise(error: Error.noThread) + } + let group = thread.groupModel + let oldMembers = Set(group.groupMemberIds) + let newMembers = members.subtracting(oldMembers) + let membersAsData = members.map { Data(hex: $0) } + let admins = group.groupAdminIds + let adminsAsData = admins.map { Data(hex: $0) } + guard let groupPrivateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey) else { + print("[Loki] Couldn't get private key for closed group.") + return Promise(error: Error.noPrivateKey) + } + let wasAnyUserRemoved = Set(members).intersection(oldMembers) != oldMembers + let removedMembers = oldMembers.subtracting(members) + let isUserLeaving = removedMembers.contains(userPublicKey) + var newSenderKeys: [ClosedGroupSenderKey] = [] + if wasAnyUserRemoved { + if isUserLeaving && removedMembers.count != 1 { + print("[Loki] Can't remove self and others simultaneously.") + return Promise(error: Error.invalidUpdate) + } + // Establish sessions if needed + establishSessionsIfNeeded(with: [String](members), using: transaction) + // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) + let promises: [Promise] = oldMembers.map { member in + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], + members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + return SSKEnvironment.shared.messageSender.sendPromise(message: closedGroupUpdateMessage) + } + when(resolved: promises).done2 { _ in seal.fulfill(()) }.catch2 { seal.reject($0) } + promise.done { + Storage.writeSync { transaction in + let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey) + for (senderPublicKey, oldRatchet) in allOldRatchets { + let collection = Storage.ClosedGroupRatchetCollectionType.old + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction) + } + // Delete all ratchets (it's important that this happens * after * sending out the update) + Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) + // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and + // send it out to all members (minus the removed ones) using established channels. + if isUserLeaving { + Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) + // Notify the PN server + LokiPushNotificationManager.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) + } else { + // Send closed group update messages to any new members using established channels + for member in newMembers { + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, + groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: [], members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + } + // Send out the user's new ratchet to all members (minus the removed ones) using established channels + let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) + let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) + for member in members { + guard member != userPublicKey else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + } + } + } + } + } else if !newMembers.isEmpty { + seal.fulfill(()) + // Generate ratchets for any new members + newSenderKeys = newMembers.map { publicKey in + let ratchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) + return ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) + } + // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: newSenderKeys, + members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + // Establish sessions if needed + establishSessionsIfNeeded(with: [String](newMembers), using: transaction) + // Send closed group update messages to the new members using established channels + var allSenderKeys = Storage.getAllClosedGroupSenderKeys(for: groupPublicKey) + allSenderKeys.formUnion(newSenderKeys) + for member in newMembers { + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, + groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: [ClosedGroupSenderKey](allSenderKeys), members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + } + } else { + seal.fulfill(()) + let allSenderKeys = Storage.getAllClosedGroupSenderKeys(for: groupPublicKey) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, + senderKeys: [ClosedGroupSenderKey](allSenderKeys), members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + } + // Update the group + let newGroupModel = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins) + thread.setGroupModel(newGroupModel, with: transaction) + // Notify the user + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: SSKEnvironment.shared.contactsManager) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + // Return + return promise + } + + /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. + @objc(leaveGroupWithPublicKey:transaction:) + public static func objc_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { + return AnyPromise.from(leave(groupPublicKey, using: transaction)) + } + + /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. + public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { + print("[Loki] Can't leave nonexistent closed group.") + return Promise(error: Error.noThread) + } + let group = thread.groupModel + var newMembers = Set(group.groupMemberIds) + newMembers.remove(userPublicKey) + return update(groupPublicKey, with: newMembers, name: group.groupName!, transaction: transaction) + } + + public static func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + print("[Loki] Requesting sender key for group public key: \(groupPublicKey), sender public key: \(senderPublicKey).") + // Establish session if needed + SessionManagementProtocol.sendSessionRequestIfNeeded(to: senderPublicKey, using: transaction) + // Send the request + let thread = TSContactThread.getOrCreateThread(withContactId: senderPublicKey, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKeyRequest(groupPublicKey: Data(hex: groupPublicKey)) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + } + + // MARK: - Receiving + + @objc(handleSharedSenderKeysUpdateIfNeeded:from:transaction:) + public static func handleSharedSenderKeysUpdateIfNeeded(_ dataMessage: SSKProtoDataMessage, from publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + // Note that `publicKey` is either the public key of the group or the public key of the + // sender, depending on how the message was sent + guard let closedGroupUpdate = dataMessage.closedGroupUpdate, isValid(closedGroupUpdate) else { return } + switch closedGroupUpdate.type { + case .new: handleNewGroupMessage(closedGroupUpdate, using: transaction) + case .info: handleInfoMessage(closedGroupUpdate, from: publicKey, using: transaction) + case .senderKeyRequest: handleSenderKeyRequestMessage(closedGroupUpdate, from: publicKey, using: transaction) + case .senderKey: handleSenderKeyMessage(closedGroupUpdate, from: publicKey, using: transaction) + } + } + + private static func isValid(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate) -> Bool { + guard !closedGroupUpdate.groupPublicKey.isEmpty else { return false } + switch closedGroupUpdate.type { + case .new: return !(closedGroupUpdate.name ?? "").isEmpty && !(closedGroupUpdate.groupPrivateKey ?? Data()).isEmpty && !closedGroupUpdate.members.isEmpty + && !closedGroupUpdate.admins.isEmpty // senderKeys may be empty + case .info: return !(closedGroupUpdate.name ?? "").isEmpty && !closedGroupUpdate.members.isEmpty && !closedGroupUpdate.admins.isEmpty // senderKeys may be empty + case .senderKeyRequest: return true + case .senderKey: return !closedGroupUpdate.senderKeys.isEmpty + } + } + + private static func handleNewGroupMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, using transaction: YapDatabaseReadWriteTransaction) { + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + // Unwrap the message + let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString() + let name = closedGroupUpdate.name + let groupPrivateKey = closedGroupUpdate.groupPrivateKey! + let senderKeys = closedGroupUpdate.senderKeys + let members = closedGroupUpdate.members.map { $0.toHexString() } + let admins = closedGroupUpdate.admins.map { $0.toHexString() } + // Persist the ratchets + senderKeys.forEach { senderKey in + guard members.contains(senderKey.publicKey.toHexString()) else { return } // TODO: This currently doesn't take into account multi device + let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: []) + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderKey.publicKey.toHexString(), ratchet: ratchet, using: transaction) + } + // Sort out any discrepancies between the provided sender keys and what's required + let missingSenderKeys = Set(members).subtracting(senderKeys.map { $0.publicKey.toHexString() }) + let userPublicKey = getUserHexEncodedPublicKey() + if missingSenderKeys.contains(userPublicKey) { + establishSessionsIfNeeded(with: [String](members), using: transaction) + let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) + let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) + for member in members { + guard member != userPublicKey else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + } + } + for publicKey in missingSenderKeys.subtracting([ userPublicKey ]) { + requestSenderKey(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) + } + // Create the group + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins) + let thread: TSGroupThread + if let t = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) { + thread = t + thread.setGroupModel(group, with: transaction) + } else { + thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction) + thread.usesSharedSenderKeys = true + thread.save(with: transaction) + } + SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread) + // Add the group to the user's set of public keys to poll for + Storage.setClosedGroupPrivateKey(groupPrivateKey.toHexString(), for: groupPublicKey, using: transaction) + // Notify the PN server + LokiPushNotificationManager.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey()) + // Notify the user + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate) + infoMessage.save(with: transaction) + // Establish sessions if needed + establishSessionsIfNeeded(with: members, using: transaction) // This internally takes care of multi device + } + + /// Invoked upon receiving a group update. A group update is sent out when a group's name is changed, when new users are added, when users leave or are + /// kicked, or if the group admins are changed. + private static func handleInfoMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, from senderPublicKey: String, + using transaction: YapDatabaseReadWriteTransaction) { + // Unwrap the message + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString() + let name = closedGroupUpdate.name + let senderKeys = closedGroupUpdate.senderKeys + let members = closedGroupUpdate.members.map { $0.toHexString() } + let admins = closedGroupUpdate.admins.map { $0.toHexString() } + // Get the group + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { + return print("[Loki] Ignoring closed group info message for nonexistent group.") + } + let group = thread.groupModel + // Check that the sender is a member of the group (before the update) + var membersAndLinkedDevices: Set = Set(group.groupMemberIds) + for member in group.groupMemberIds { + let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction) + membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }) + } + guard membersAndLinkedDevices.contains(senderPublicKey) else { + return print("[Loki] Ignoring closed group info message from non-member.") + } + // Store the ratchets for any new members (it's important that this happens before the code below) + senderKeys.forEach { senderKey in + let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: []) + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderKey.publicKey.toHexString(), ratchet: ratchet, using: transaction) + } + // Delete all ratchets and either: + // • Send out the user's new ratchet using established channels if other members of the group left or were removed + // • Remove the group from the user's set of public keys to poll for if the current user was among the members that were removed + let oldMembers = group.groupMemberIds + let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() + let wasUserRemoved = !members.contains(userPublicKey) + if Set(members).intersection(oldMembers) != Set(oldMembers) { + let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey) + for (senderPublicKey, oldRatchet) in allOldRatchets { + let collection = Storage.ClosedGroupRatchetCollectionType.old + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction) + } + Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) + if wasUserRemoved { + Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) + // Notify the PN server + LokiPushNotificationManager.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) + } else { + establishSessionsIfNeeded(with: members, using: transaction) // This internally takes care of multi device + let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) + let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) + for member in members { + guard member != userPublicKey else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) // This internally takes care of multi device + } + } + } + // Update the group + let newGroupModel = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins) + thread.setGroupModel(newGroupModel, with: transaction) + // Notify the user if needed (don't notify them if the message just contained linked device sender keys) + if Set(members) != Set(oldMembers) || Set(admins) != Set(group.groupAdminIds) || name != group.groupName { + let infoMessageType: TSInfoMessageType = wasUserRemoved ? .typeGroupQuit : .typeGroupUpdate + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: SSKEnvironment.shared.contactsManager) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: infoMessageType, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + } + + private static func handleSenderKeyRequestMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, from senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + // Prepare + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + let userPublicKey = getUserHexEncodedPublicKey() + let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString() + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + guard let groupThread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { + return print("[Loki] Ignoring closed group sender key request for nonexistent group.") + } + let group = groupThread.groupModel + // Check that the requesting user is a member of the group + var membersAndLinkedDevices: Set = Set(group.groupMemberIds) + for member in group.groupMemberIds { + let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction) + membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }) + } + guard membersAndLinkedDevices.contains(senderPublicKey) else { + return print("[Loki] Ignoring closed group sender key request from non-member.") + } + // Respond to the request + print("[Loki] Responding to sender key request from: \(senderPublicKey).") + SessionManagementProtocol.sendSessionRequestIfNeeded(to: senderPublicKey, using: transaction) // This internally takes care of multi device + let userRatchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: userPublicKey) + ?? SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) + let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) + let thread = TSContactThread.getOrCreateThread(withContactId: senderPublicKey, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) // This internally takes care of multi device + } + + /// Invoked upon receiving a sender key from another user. + private static func handleSenderKeyMessage(_ closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate, from senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + // Prepare + let groupPublicKey = closedGroupUpdate.groupPublicKey.toHexString() + guard let senderKey = closedGroupUpdate.senderKeys.first else { + return print("[Loki] Ignoring invalid closed group sender key.") + } + guard senderKey.publicKey.toHexString() == senderPublicKey else { + return print("[Loki] Ignoring invalid closed group sender key.") + } + // Store the sender key + print("[Loki] Received a sender key from: \(senderPublicKey).") + let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: []) + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction) + } + + // MARK: - General + + @objc(establishSessionsIfNeededWithClosedGroupMembers:transaction:) + public static func establishSessionsIfNeeded(with closedGroupMembers: [String], using transaction: YapDatabaseReadWriteTransaction) { + closedGroupMembers.forEach { publicKey in + SessionManagementProtocol.sendSessionRequestIfNeeded(to: publicKey, using: transaction) + } + } + + @objc(shouldIgnoreClosedGroupMessage:inThread:wrappedIn:) + public static func shouldIgnoreClosedGroupMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSGroupThread, wrappedIn envelope: SSKProtoEnvelope) -> Bool { + guard thread.groupModel.groupType == .closedGroup else { return true } + let publicKey = envelope.source! // Set during UD decryption + var result = false + Storage.read { transaction in + result = !thread.isUserMember(inGroup: publicKey, transaction: transaction) + } + return result + } + + /// - Note: Deprecated. + @objc(shouldIgnoreClosedGroupUpdateMessage:inThread:wrappedIn:) + public static func shouldIgnoreClosedGroupUpdateMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSGroupThread, wrappedIn envelope: SSKProtoEnvelope) -> Bool { + guard thread.groupModel.groupType == .closedGroup else { return true } + let publicKey = envelope.source! // Set during UD decryption + var result = false + Storage.read { transaction in + result = !thread.isUserAdmin(inGroup: publicKey, transaction: transaction) + } + return result + } +} diff --git a/SignalUtilitiesKit/Contact.h b/SignalUtilitiesKit/Contact.h new file mode 100644 index 000000000..ff7455f8d --- /dev/null +++ b/SignalUtilitiesKit/Contact.h @@ -0,0 +1,58 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * An adapter for the system contacts + */ + +@class CNContact; +@class PhoneNumber; +@class SignalRecipient; +@class UIImage; +@class YapDatabaseReadTransaction; + +@interface Contact : MTLModel + +@property (nullable, readonly, nonatomic) NSString *firstName; +@property (nullable, readonly, nonatomic) NSString *lastName; +@property (readonly, nonatomic) NSString *fullName; +@property (readonly, nonatomic) NSString *comparableNameFirstLast; +@property (readonly, nonatomic) NSString *comparableNameLastFirst; +@property (readonly, nonatomic) NSArray *parsedPhoneNumbers; +@property (readonly, nonatomic) NSArray *userTextPhoneNumbers; +@property (readonly, nonatomic) NSArray *emails; +@property (readonly, nonatomic) NSString *uniqueId; +@property (nonatomic, readonly) BOOL isSignalContact; +@property (nonatomic, readonly) NSString *cnContactId; + +- (NSArray *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction; +// TODO: Remove this method. +- (NSArray *)textSecureIdentifiers; + +#if TARGET_OS_IOS + +- (instancetype)initWithSystemContact:(CNContact *)cnContact NS_AVAILABLE_IOS(9_0); ++ (nullable Contact *)contactWithVCardData:(NSData *)data; ++ (nullable CNContact *)cnContactWithVCardData:(NSData *)data; + +- (NSString *)nameForPhoneNumber:(NSString *)recipientId; + +#endif // TARGET_OS_IOS + ++ (NSComparator)comparatorSortingNamesByFirstThenLast:(BOOL)firstNameOrdering; ++ (NSString *)formattedFullNameWithCNContact:(CNContact *)cnContact NS_SWIFT_NAME(formattedFullName(cnContact:)); ++ (nullable NSString *)localizedStringForCNLabel:(nullable NSString *)cnLabel; + ++ (CNContact *)mergeCNContact:(CNContact *)oldCNContact + newCNContact:(CNContact *)newCNContact NS_SWIFT_NAME(merge(cnContact:newCNContact:)); + ++ (nullable NSData *)avatarDataForCNContact:(nullable CNContact *)cnContact; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Contact.m b/SignalUtilitiesKit/Contact.m new file mode 100644 index 000000000..f679758d8 --- /dev/null +++ b/SignalUtilitiesKit/Contact.m @@ -0,0 +1,434 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "Contact.h" +#import "OWSPrimaryStorage.h" +#import "PhoneNumber.h" +#import "SSKEnvironment.h" +#import "SignalRecipient.h" +#import "TSAccountManager.h" +#import +#import +#import + +@import Contacts; + +NS_ASSUME_NONNULL_BEGIN + +@interface Contact () + +@property (nonatomic, readonly) NSMutableDictionary *phoneNumberNameMap; +@property (nonatomic, readonly) NSUInteger imageHash; + +@end + +#pragma mark - + +@implementation Contact + +@synthesize comparableNameFirstLast = _comparableNameFirstLast; +@synthesize comparableNameLastFirst = _comparableNameLastFirst; + +#if TARGET_OS_IOS + +- (instancetype)initWithSystemContact:(CNContact *)cnContact +{ + self = [super init]; + if (!self) { + return self; + } + + _cnContactId = cnContact.identifier; + _firstName = cnContact.givenName.ows_stripped; + _lastName = cnContact.familyName.ows_stripped; + _fullName = [Contact formattedFullNameWithCNContact:cnContact]; + + NSMutableArray *phoneNumbers = [NSMutableArray new]; + NSMutableDictionary *phoneNumberNameMap = [NSMutableDictionary new]; + const NSUInteger kMaxPhoneNumbersConsidered = 50; + + NSArray *consideredPhoneNumbers; + if (cnContact.phoneNumbers.count <= kMaxPhoneNumbersConsidered) { + consideredPhoneNumbers = cnContact.phoneNumbers; + } else { + OWSLogInfo(@"For perf, only considering the first %lu phone numbers for contact with many numbers.", (unsigned long)kMaxPhoneNumbersConsidered); + consideredPhoneNumbers = [cnContact.phoneNumbers subarrayWithRange:NSMakeRange(0, kMaxPhoneNumbersConsidered)]; + } + for (CNLabeledValue *phoneNumberField in consideredPhoneNumbers) { + if ([phoneNumberField.value isKindOfClass:[CNPhoneNumber class]]) { + CNPhoneNumber *phoneNumber = (CNPhoneNumber *)phoneNumberField.value; + [phoneNumbers addObject:phoneNumber.stringValue]; + if ([phoneNumberField.label isEqualToString:CNLabelHome]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_HOME", @"Label for 'Home' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelWork]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_WORK", @"Label for 'Work' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberiPhone]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_IPHONE", @"Label for 'iPhone' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberMobile]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_MOBILE", @"Label for 'Mobile' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberMain]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_MAIN", @"Label for 'Main' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberHomeFax]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_HOME_FAX", @"Label for 'HomeFAX' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberWorkFax]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_WORK_FAX", @"Label for 'Work FAX' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberOtherFax]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_OTHER_FAX", @"Label for 'Other FAX' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberPager]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_PAGER", @"Label for 'Pager' phone numbers."); + } else if ([phoneNumberField.label isEqualToString:CNLabelOther]) { + phoneNumberNameMap[phoneNumber.stringValue] + = NSLocalizedString(@"PHONE_NUMBER_TYPE_OTHER", @"Label for 'Other' phone numbers."); + } else if (phoneNumberField.label.length > 0 && ![phoneNumberField.label hasPrefix:@"_$"]) { + // We'll reach this case for: + // + // * User-defined custom labels, which we want to display. + // * Labels like "_$!!$_", which I'm guessing are synced from other platforms. + // We don't want to display these labels. Even some of iOS' default labels (like Radio) show + // up this way. + phoneNumberNameMap[phoneNumber.stringValue] = phoneNumberField.label; + } + } + } + + _userTextPhoneNumbers = [phoneNumbers copy]; + _phoneNumberNameMap = [NSMutableDictionary new]; + _parsedPhoneNumbers = + [self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:phoneNumberNameMap]; + + NSMutableArray *emailAddresses = [NSMutableArray new]; + for (CNLabeledValue *emailField in cnContact.emailAddresses) { + if ([emailField.value isKindOfClass:[NSString class]]) { + [emailAddresses addObject:(NSString *)emailField.value]; + } + } + _emails = [emailAddresses copy]; + + NSData *_Nullable avatarData = [Contact avatarDataForCNContact:cnContact]; + if (avatarData) { + NSUInteger hashValue = 0; + NSData *_Nullable hashData = [Cryptography computeSHA256Digest:avatarData truncatedToBytes:sizeof(hashValue)]; + if (!hashData) { + OWSFailDebug(@"could not compute hash for avatar."); + } + [hashData getBytes:&hashValue length:sizeof(hashValue)]; + _imageHash = hashValue; + } else { + _imageHash = 0; + } + + return self; +} + +- (NSString *)uniqueId +{ + return self.cnContactId; +} + ++ (nullable Contact *)contactWithVCardData:(NSData *)data +{ + CNContact *_Nullable cnContact = [self cnContactWithVCardData:data]; + + if (!cnContact) { + OWSLogError(@"Could not parse vcard data."); + return nil; + } + + return [[self alloc] initWithSystemContact:cnContact]; +} + +#endif // TARGET_OS_IOS + +- (NSArray *)parsedPhoneNumbersFromUserTextPhoneNumbers:(NSArray *)userTextPhoneNumbers + phoneNumberNameMap:(nullable NSDictionary *) + phoneNumberNameMap +{ + OWSAssertDebug(self.phoneNumberNameMap); + + NSMutableDictionary *parsedPhoneNumberMap = [NSMutableDictionary new]; + NSMutableArray *parsedPhoneNumbers = [NSMutableArray new]; + for (NSString *phoneNumberString in userTextPhoneNumbers) { + for (PhoneNumber *phoneNumber in + [PhoneNumber tryParsePhoneNumbersFromsUserSpecifiedText:phoneNumberString + clientPhoneNumber:[TSAccountManager localNumber]]) { + [parsedPhoneNumbers addObject:phoneNumber]; + parsedPhoneNumberMap[phoneNumber.toE164] = phoneNumber; + NSString *phoneNumberName = phoneNumberNameMap[phoneNumberString]; + if (phoneNumberName) { + self.phoneNumberNameMap[phoneNumber.toE164] = phoneNumberName; + } + } + } + return [parsedPhoneNumbers sortedArrayUsingSelector:@selector(compare:)]; +} + +- (NSString *)comparableNameFirstLast { + if (_comparableNameFirstLast == nil) { + // Combine the two names with a tab separator, which has a lower ascii code than space, so that first names + // that contain a space ("Mary Jo\tCatlett") will sort after those that do not ("Mary\tOliver") + _comparableNameFirstLast = [self combineLeftName:_firstName withRightName:_lastName usingSeparator:@"\t"]; + } + + return _comparableNameFirstLast; +} + +- (NSString *)comparableNameLastFirst { + if (_comparableNameLastFirst == nil) { + // Combine the two names with a tab separator, which has a lower ascii code than space, so that last names + // that contain a space ("Van Der Beek\tJames") will sort after those that do not ("Van\tJames") + _comparableNameLastFirst = [self combineLeftName:_lastName withRightName:_firstName usingSeparator:@"\t"]; + } + + return _comparableNameLastFirst; +} + +- (NSString *)combineLeftName:(NSString *)leftName withRightName:(NSString *)rightName usingSeparator:(NSString *)separator { + const BOOL leftNameNonEmpty = (leftName.length > 0); + const BOOL rightNameNonEmpty = (rightName.length > 0); + + if (leftNameNonEmpty && rightNameNonEmpty) { + return [NSString stringWithFormat:@"%@%@%@", leftName, separator, rightName]; + } else if (leftNameNonEmpty) { + return [leftName copy]; + } else if (rightNameNonEmpty) { + return [rightName copy]; + } else { + return @""; + } +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@: %@", self.fullName, self.userTextPhoneNumbers]; +} + +- (BOOL)isSignalContact { + NSArray *identifiers = [self textSecureIdentifiers]; + + return [identifiers count] > 0; +} + +- (NSArray *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + __block NSMutableArray *result = [NSMutableArray array]; + + for (PhoneNumber *number in [self.parsedPhoneNumbers sortedArrayUsingSelector:@selector(compare:)]) { + SignalRecipient *_Nullable signalRecipient = [SignalRecipient registeredRecipientForRecipientId:number.toE164 + mustHaveDevices:YES + transaction:transaction]; + if (signalRecipient) { + [result addObject:signalRecipient]; + } + } + + return [result copy]; +} + +- (NSArray *)textSecureIdentifiers { + __block NSMutableArray *identifiers = [NSMutableArray array]; + + [OWSPrimaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + for (PhoneNumber *number in self.parsedPhoneNumbers) { + if ([SignalRecipient isRegisteredRecipient:number.toE164 transaction:transaction]) { + [identifiers addObject:number.toE164]; + } + } + }]; + return [identifiers copy]; +} + ++ (NSComparator)comparatorSortingNamesByFirstThenLast:(BOOL)firstNameOrdering { + return ^NSComparisonResult(id obj1, id obj2) { + Contact *contact1 = (Contact *)obj1; + Contact *contact2 = (Contact *)obj2; + + if (firstNameOrdering) { + return [contact1.comparableNameFirstLast caseInsensitiveCompare:contact2.comparableNameFirstLast]; + } else { + return [contact1.comparableNameLastFirst caseInsensitiveCompare:contact2.comparableNameLastFirst]; + } + }; +} + ++ (NSString *)formattedFullNameWithCNContact:(CNContact *)cnContact +{ + return [CNContactFormatter stringFromContact:cnContact style:CNContactFormatterStyleFullName].ows_stripped; +} + +- (NSString *)nameForPhoneNumber:(NSString *)recipientId +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug([self.textSecureIdentifiers containsObject:recipientId]); + + NSString *value = self.phoneNumberNameMap[recipientId]; + OWSAssertDebug(value); + if (!value) { + return NSLocalizedString(@"PHONE_NUMBER_TYPE_UNKNOWN", + @"Label used when we don't what kind of phone number it is (e.g. mobile/work/home)."); + } + return value; +} + ++ (nullable NSData *)avatarDataForCNContact:(nullable CNContact *)cnContact +{ + if (cnContact.thumbnailImageData) { + return cnContact.thumbnailImageData.copy; + } else if (cnContact.imageData) { + // This only occurs when sharing a contact via the share extension + return cnContact.imageData.copy; + } else { + return nil; + } +} + +// This method is used to de-bounce system contact fetch notifications +// by checking for changes in the contact data. +- (NSUInteger)hash +{ + // base hash is some arbitrary number + NSUInteger hash = 1825038313; + + hash = hash ^ self.fullName.hash; + + hash = hash ^ self.imageHash; + + for (PhoneNumber *phoneNumber in self.parsedPhoneNumbers) { + hash = hash ^ phoneNumber.toE164.hash; + } + + for (NSString *email in self.emails) { + hash = hash ^ email.hash; + } + + return hash; +} + +#pragma mark - CNContactConverters + ++ (nullable CNContact *)cnContactWithVCardData:(NSData *)data +{ + OWSAssertDebug(data); + + NSError *error; + NSArray *_Nullable contacts = [CNContactVCardSerialization contactsWithData:data error:&error]; + if (!contacts || error) { + OWSFailDebug(@"could not parse vcard: %@", error); + return nil; + } + if (contacts.count < 1) { + OWSFailDebug(@"empty vcard: %@", error); + return nil; + } + if (contacts.count > 1) { + OWSFailDebug(@"more than one contact in vcard: %@", error); + } + return contacts.firstObject; +} + ++ (CNContact *)mergeCNContact:(CNContact *)oldCNContact newCNContact:(CNContact *)newCNContact +{ + OWSAssertDebug(oldCNContact); + OWSAssertDebug(newCNContact); + + Contact *oldContact = [[Contact alloc] initWithSystemContact:oldCNContact]; + + CNMutableContact *_Nullable mergedCNContact = [oldCNContact mutableCopy]; + if (!mergedCNContact) { + OWSFailDebug(@"mergedCNContact was unexpectedly nil"); + return [CNContact new]; + } + + // Name + NSString *formattedFullName = [self.class formattedFullNameWithCNContact:mergedCNContact]; + + // merged all or nothing - do not try to piece-meal merge. + if (formattedFullName.length == 0) { + mergedCNContact.namePrefix = newCNContact.namePrefix.ows_stripped; + mergedCNContact.givenName = newCNContact.givenName.ows_stripped; + mergedCNContact.middleName = newCNContact.middleName.ows_stripped; + mergedCNContact.familyName = newCNContact.familyName.ows_stripped; + mergedCNContact.nameSuffix = newCNContact.nameSuffix.ows_stripped; + } + + if (mergedCNContact.organizationName.ows_stripped.length < 1) { + mergedCNContact.organizationName = newCNContact.organizationName.ows_stripped; + } + + // Phone Numbers + NSSet *existingParsedPhoneNumberSet = [NSSet setWithArray:oldContact.parsedPhoneNumbers]; + NSSet *existingUnparsedPhoneNumberSet = [NSSet setWithArray:oldContact.userTextPhoneNumbers]; + + NSMutableArray *> *mergedPhoneNumbers = [mergedCNContact.phoneNumbers mutableCopy]; + for (CNLabeledValue *labeledPhoneNumber in newCNContact.phoneNumbers) { + NSString *_Nullable unparsedPhoneNumber = labeledPhoneNumber.value.stringValue; + if ([existingUnparsedPhoneNumberSet containsObject:unparsedPhoneNumber]) { + // Skip phone number if "unparsed" form is a duplicate. + continue; + } + PhoneNumber *_Nullable parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:labeledPhoneNumber.value.stringValue]; + if (parsedPhoneNumber && [existingParsedPhoneNumberSet containsObject:parsedPhoneNumber]) { + // Skip phone number if "parsed" form is a duplicate. + continue; + } + [mergedPhoneNumbers addObject:labeledPhoneNumber]; + } + mergedCNContact.phoneNumbers = mergedPhoneNumbers; + + // Emails + NSSet *existingEmailSet = [NSSet setWithArray:oldContact.emails]; + NSMutableArray *> *mergedEmailAddresses = [mergedCNContact.emailAddresses mutableCopy]; + for (CNLabeledValue *labeledEmail in newCNContact.emailAddresses) { + NSString *normalizedValue = labeledEmail.value.ows_stripped; + if (![existingEmailSet containsObject:normalizedValue]) { + [mergedEmailAddresses addObject:labeledEmail]; + } + } + mergedCNContact.emailAddresses = mergedEmailAddresses; + + // Address + // merged all or nothing - do not try to piece-meal merge. + if (mergedCNContact.postalAddresses.count == 0) { + mergedCNContact.postalAddresses = newCNContact.postalAddresses; + } + + // Avatar + if (!mergedCNContact.imageData) { + mergedCNContact.imageData = newCNContact.imageData; + } + + return [mergedCNContact copy]; +} + ++ (nullable NSString *)localizedStringForCNLabel:(nullable NSString *)cnLabel +{ + if (cnLabel.length == 0) { + return nil; + } + + NSString *_Nullable localizedLabel = [CNLabeledValue localizedStringForLabel:cnLabel]; + + // Docs for localizedStringForLabel say it returns: + // > The localized string if a Contacts framework defined label, otherwise just returns the label. + // But in practice, at least on iOS11, if the label is not one of CNContacts known labels (like CNLabelHome) + // kUnlocalizedStringLabel is returned, rather than the unadultered label. + NSString *const kUnlocalizedStringLabel = @"__ABUNLOCALIZEDSTRING"; + + if ([localizedLabel isEqual:kUnlocalizedStringLabel]) { + return cnLabel; + } + + return localizedLabel; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ContactDiscoveryService.h b/SignalUtilitiesKit/ContactDiscoveryService.h new file mode 100644 index 000000000..da414fbfd --- /dev/null +++ b/SignalUtilitiesKit/ContactDiscoveryService.h @@ -0,0 +1,65 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSErrorUserInfoKey const ContactDiscoveryServiceErrorKey_Reason; +extern NSErrorDomain const ContactDiscoveryServiceErrorDomain; +typedef NS_ERROR_ENUM(ContactDiscoveryServiceErrorDomain, ContactDiscoveryServiceError){ + ContactDiscoveryServiceErrorAttestationFailed = 100, ContactDiscoveryServiceErrorAssertionError = 101 +}; + +@class ECKeyPair; +@class OWSAES256Key; + +@interface RemoteAttestationAuth : NSObject + +@property (nonatomic, readonly) NSString *username; +@property (nonatomic, readonly) NSString *password; + +@end + +#pragma mark - + +@interface RemoteAttestationKeys : NSObject + +@property (nonatomic, readonly) ECKeyPair *keyPair; +@property (nonatomic, readonly) NSData *serverEphemeralPublic; +@property (nonatomic, readonly) NSData *serverStaticPublic; + +@property (nonatomic, readonly) OWSAES256Key *clientKey; +@property (nonatomic, readonly) OWSAES256Key *serverKey; + +@end + +#pragma mark - + +@interface RemoteAttestation : NSObject + +@property (nonatomic, readonly) RemoteAttestationKeys *keys; +@property (nonatomic, readonly) NSArray *cookies; +@property (nonatomic, readonly) NSData *requestId; +@property (nonatomic, readonly) NSString *enclaveId; +@property (nonatomic, readonly) RemoteAttestationAuth *auth; + +@end + +#pragma mark - + +@interface ContactDiscoveryService : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initDefault NS_DESIGNATED_INITIALIZER; + ++ (instancetype)shared; + +- (void)testService; +- (void)performRemoteAttestationWithSuccess:(void (^)(RemoteAttestation *_Nonnull remoteAttestation))successHandler + failure:(void (^)(NSError *_Nonnull error))failureHandler; +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ContactDiscoveryService.m b/SignalUtilitiesKit/ContactDiscoveryService.m new file mode 100644 index 000000000..17c609298 --- /dev/null +++ b/SignalUtilitiesKit/ContactDiscoveryService.m @@ -0,0 +1,774 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "ContactDiscoveryService.h" +#import "CDSQuote.h" +#import "CDSSigningCertificate.h" +#import "NSError+MessageSending.h" +#import "OWSError.h" +#import "OWSRequestFactory.h" +#import "SSKEnvironment.h" +#import "TSNetworkManager.h" +#import +#import +#import "SSKAsserts.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSErrorUserInfoKey const ContactDiscoveryServiceErrorKey_Reason = @"ContactDiscoveryServiceErrorKey_Reason"; +NSErrorDomain const ContactDiscoveryServiceErrorDomain = @"SignalServiceKit.ContactDiscoveryService"; + +NSError *ContactDiscoveryServiceErrorMakeWithReason(NSInteger code, NSString *reason) +{ + OWSCFailDebug(@"Error: %@", reason); + + return [NSError errorWithDomain:ContactDiscoveryServiceErrorDomain + code:code + userInfo:@{ ContactDiscoveryServiceErrorKey_Reason : reason }]; +} + +@interface RemoteAttestationAuth () + +@property (nonatomic) NSString *username; +@property (nonatomic) NSString *password; + +@end + +#pragma mark - + +@implementation RemoteAttestationAuth + +@end + +#pragma mark - + +@interface RemoteAttestationKeys () + +@property (nonatomic) ECKeyPair *keyPair; +@property (nonatomic) NSData *serverEphemeralPublic; +@property (nonatomic) NSData *serverStaticPublic; + +@property (nonatomic) OWSAES256Key *clientKey; +@property (nonatomic) OWSAES256Key *serverKey; + +@end + +#pragma mark - + +@implementation RemoteAttestationKeys + ++ (nullable RemoteAttestationKeys *)keysForKeyPair:(ECKeyPair *)keyPair + serverEphemeralPublic:(NSData *)serverEphemeralPublic + serverStaticPublic:(NSData *)serverStaticPublic + error:(NSError **)error +{ + if (!keyPair) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"Missing keyPair"); + return nil; + } + if (serverEphemeralPublic.length < 1) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"Invalid serverEphemeralPublic"); + return nil; + } + if (serverStaticPublic.length < 1) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"Invalid serverStaticPublic"); + return nil; + } + RemoteAttestationKeys *keys = [RemoteAttestationKeys new]; + keys.keyPair = keyPair; + keys.serverEphemeralPublic = serverEphemeralPublic; + keys.serverStaticPublic = serverStaticPublic; + if (![keys deriveKeys]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"failed to derive keys"); + return nil; + } + return keys; +} + +// Returns YES on success. +- (BOOL)deriveKeys +{ + NSData *ephemeralToEphemeral; + NSData *ephemeralToStatic; + @try { + ephemeralToEphemeral = + [Curve25519 generateSharedSecretFromPublicKey:self.serverEphemeralPublic andKeyPair:self.keyPair]; + ephemeralToStatic = + [Curve25519 generateSharedSecretFromPublicKey:self.serverStaticPublic andKeyPair:self.keyPair]; + } @catch (NSException *exception) { + OWSFailDebug(@"could not generate shared secrets: %@", exception); + return NO; + } + + NSData *masterSecret = [ephemeralToEphemeral dataByAppendingData:ephemeralToStatic]; + NSData *publicKeys = [NSData join:@[ + self.keyPair.publicKey, + self.serverEphemeralPublic, + self.serverStaticPublic, + ]]; + + NSData *_Nullable derivedMaterial; + @try { + derivedMaterial = + [HKDFKit deriveKey:masterSecret info:nil salt:publicKeys outputSize:(int)kAES256_KeyByteLength * 2]; + } @catch (NSException *exception) { + OWSFailDebug(@"could not derive service key: %@", exception); + return NO; + } + + if (!derivedMaterial) { + OWSFailDebug(@"missing derived service key."); + return NO; + } + if (derivedMaterial.length != kAES256_KeyByteLength * 2) { + OWSFailDebug(@"derived service key has unexpected length."); + return NO; + } + + NSData *_Nullable clientKeyData = + [derivedMaterial subdataWithRange:NSMakeRange(kAES256_KeyByteLength * 0, kAES256_KeyByteLength)]; + OWSAES256Key *_Nullable clientKey = [OWSAES256Key keyWithData:clientKeyData]; + if (!clientKey) { + OWSFailDebug(@"clientKey has unexpected length."); + return NO; + } + + NSData *_Nullable serverKeyData = + [derivedMaterial subdataWithRange:NSMakeRange(kAES256_KeyByteLength * 1, kAES256_KeyByteLength)]; + OWSAES256Key *_Nullable serverKey = [OWSAES256Key keyWithData:serverKeyData]; + if (!serverKey) { + OWSFailDebug(@"serverKey has unexpected length."); + return NO; + } + + self.clientKey = clientKey; + self.serverKey = serverKey; + + return YES; +} + +@end + +#pragma mark - + +@interface RemoteAttestation () + +@property (nonatomic) RemoteAttestationKeys *keys; +@property (nonatomic) NSArray *cookies; +@property (nonatomic) NSData *requestId; +@property (nonatomic) NSString *enclaveId; +@property (nonatomic) RemoteAttestationAuth *auth; + +@end + +#pragma mark - + +@implementation RemoteAttestation + +@end + +#pragma mark - + +@interface SignatureBodyEntity : NSObject + +@property (nonatomic) NSData *isvEnclaveQuoteBody; +@property (nonatomic) NSString *isvEnclaveQuoteStatus; +@property (nonatomic) NSString *timestamp; +@property (nonatomic) NSNumber *version; + +@end + +#pragma mark - + +@implementation SignatureBodyEntity + +@end + +#pragma mark - + +@interface NSDictionary (CDS) + +@end + +#pragma mark - + +@implementation NSDictionary (CDS) + +- (nullable NSString *)stringForKey:(NSString *)key +{ + NSString *_Nullable valueString = self[key]; + if (![valueString isKindOfClass:[NSString class]]) { + OWSFailDebug(@"couldn't parse string for key: %@", key); + return nil; + } + return valueString; +} + +- (nullable NSNumber *)numberForKey:(NSString *)key +{ + NSNumber *_Nullable value = self[key]; + if (![value isKindOfClass:[NSNumber class]]) { + OWSFailDebug(@"couldn't parse number for key: %@", key); + return nil; + } + return value; +} + +- (nullable NSData *)base64DataForKey:(NSString *)key +{ + NSString *_Nullable valueString = self[key]; + if (![valueString isKindOfClass:[NSString class]]) { + OWSFailDebug(@"couldn't parse base 64 value for key: %@", key); + return nil; + } + NSData *_Nullable valueData = [[NSData alloc] initWithBase64EncodedString:valueString options:0]; + if (!valueData) { + OWSFailDebug(@"couldn't decode base 64 value for key: %@", key); + return nil; + } + return valueData; +} + +- (nullable NSData *)base64DataForKey:(NSString *)key expectedLength:(NSUInteger)expectedLength +{ + NSData *_Nullable valueData = [self base64DataForKey:key]; + if (valueData && valueData.length != expectedLength) { + OWSLogDebug(@"decoded base 64 value for key: %@, has unexpected length: %lu != %lu", + key, + (unsigned long)valueData.length, + (unsigned long)expectedLength); + OWSFailDebug(@"decoded base 64 value for key has unexpected length: %lu != %lu", + (unsigned long)valueData.length, + (unsigned long)expectedLength); + return nil; + } + return valueData; +} + +@end + +#pragma mark - + +@implementation ContactDiscoveryService + ++ (instancetype)shared +{ + OWSAssertDebug(SSKEnvironment.shared.contactDiscoveryService); + + return SSKEnvironment.shared.contactDiscoveryService; +} + +- (instancetype)initDefault +{ + self = [super init]; + if (!self) { + return self; + } + + OWSSingletonAssert(); + + return self; +} + +- (void)testService +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self + performRemoteAttestationWithSuccess:^(RemoteAttestation *remoteAttestation) { + OWSLogDebug(@"succeeded"); + } + failure:^(NSError *error) { + OWSLogDebug(@"failed with error: %@", error); + }]; + }); +} + +- (void)performRemoteAttestationWithSuccess:(void (^)(RemoteAttestation *remoteAttestation))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + [self + getRemoteAttestationAuthWithSuccess:^(RemoteAttestationAuth *auth) { + [self performRemoteAttestationWithAuth:auth success:successHandler failure:failureHandler]; + } + failure:failureHandler]; +} + +- (void)getRemoteAttestationAuthWithSuccess:(void (^)(RemoteAttestationAuth *))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + TSRequest *request = [OWSRequestFactory remoteAttestationAuthRequest]; + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseDict) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + RemoteAttestationAuth *_Nullable auth = [self parseAuthParams:responseDict]; + if (!auth) { + OWSLogError(@"remote attestation auth could not be parsed: %@", responseDict); + NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + failureHandler(error); + return; + } + + successHandler(auth); + }); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; + OWSLogVerbose(@"remote attestation auth failure: %lu", (unsigned long)response.statusCode); + failureHandler(error); + }]; +} + +- (nullable RemoteAttestationAuth *)parseAuthParams:(id)response +{ + if (![response isKindOfClass:[NSDictionary class]]) { + return nil; + } + + NSDictionary *responseDict = response; + NSString *_Nullable password = [responseDict stringForKey:@"password"]; + if (password.length < 1) { + OWSFailDebug(@"missing or empty password."); + return nil; + } + + NSString *_Nullable username = [responseDict stringForKey:@"username"]; + if (username.length < 1) { + OWSFailDebug(@"missing or empty username."); + return nil; + } + + RemoteAttestationAuth *result = [RemoteAttestationAuth new]; + result.username = username; + result.password = password; + return result; +} + +- (void)performRemoteAttestationWithAuth:(RemoteAttestationAuth *)auth + success:(void (^)(RemoteAttestation *remoteAttestation))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + return; // Loki: Do nothing + + ECKeyPair *keyPair = [Curve25519 generateKeyPair]; + + NSString *enclaveId = @"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9"; + + TSRequest *request = [OWSRequestFactory remoteAttestationRequest:keyPair + enclaveId:enclaveId + authUsername:auth.username + authPassword:auth.password]; + + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseJson) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *_Nullable error; + RemoteAttestation *_Nullable attestation = [self parseAttestationResponseJson:responseJson + response:task.response + keyPair:keyPair + enclaveId:enclaveId + auth:auth + error:&error]; + + if (!attestation) { + if (!error) { + OWSFailDebug(@"error was unexpectedly nil"); + error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError, + @"failure when parsing attestation - no reason given"); + } else { + OWSFailDebug(@"error with attestation: %@", error); + } + error.isRetryable = NO; + failureHandler(error); + return; + } + + successHandler(attestation); + }); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + failureHandler(error); + }]; +} + +- (nullable RemoteAttestation *)parseAttestationResponseJson:(id)responseJson + response:(NSURLResponse *)response + keyPair:(ECKeyPair *)keyPair + enclaveId:(NSString *)enclaveId + auth:(RemoteAttestationAuth *)auth + error:(NSError **)error +{ + OWSAssertDebug(responseJson); + OWSAssertDebug(response); + OWSAssertDebug(keyPair); + OWSAssertDebug(enclaveId.length > 0); + + if (![response isKindOfClass:[NSHTTPURLResponse class]]) { + *error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError, @"unexpected response type."); + return nil; + } + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + NSArray *cookies = + [NSHTTPCookie cookiesWithResponseHeaderFields:httpResponse.allHeaderFields forURL:httpResponse.URL]; + if (cookies.count < 1) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse cookie."); + return nil; + } + + if (![responseJson isKindOfClass:[NSDictionary class]]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"invalid json response"); + return nil; + } + NSDictionary *responseDict = responseJson; + NSData *_Nullable serverEphemeralPublic = + [responseDict base64DataForKey:@"serverEphemeralPublic" expectedLength:32]; + if (!serverEphemeralPublic) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse serverEphemeralPublic."); + return nil; + } + NSData *_Nullable serverStaticPublic = [responseDict base64DataForKey:@"serverStaticPublic" expectedLength:32]; + if (!serverStaticPublic) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse serverStaticPublic."); + return nil; + } + NSData *_Nullable encryptedRequestId = [responseDict base64DataForKey:@"ciphertext"]; + if (!encryptedRequestId) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestId."); + return nil; + } + NSData *_Nullable encryptedRequestIv = [responseDict base64DataForKey:@"iv" expectedLength:12]; + if (!encryptedRequestIv) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestIv."); + return nil; + } + NSData *_Nullable encryptedRequestTag = [responseDict base64DataForKey:@"tag" expectedLength:16]; + if (!encryptedRequestTag) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encryptedRequestTag."); + return nil; + } + NSData *_Nullable quoteData = [responseDict base64DataForKey:@"quote"]; + if (!quoteData) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse quote data."); + return nil; + } + NSString *_Nullable signatureBody = [responseDict stringForKey:@"signatureBody"]; + if (![signatureBody isKindOfClass:[NSString class]]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse signatureBody."); + return nil; + } + NSData *_Nullable signature = [responseDict base64DataForKey:@"signature"]; + if (!signature) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse signature."); + return nil; + } + NSString *_Nullable encodedCertificates = [responseDict stringForKey:@"certificates"]; + if (![encodedCertificates isKindOfClass:[NSString class]]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse encodedCertificates."); + return nil; + } + NSString *_Nullable certificates = [encodedCertificates stringByRemovingPercentEncoding]; + if (!certificates) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't parse certificates."); + return nil; + } + + RemoteAttestationKeys *_Nullable keys = [RemoteAttestationKeys keysForKeyPair:keyPair + serverEphemeralPublic:serverEphemeralPublic + serverStaticPublic:serverStaticPublic + error:error]; + if (!keys || *error != nil) { + if (*error == nil) { + OWSFailDebug(@"missing error specifics"); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"Couldn't derive keys. No reason given"); + } + return nil; + } + + CDSQuote *_Nullable quote = [CDSQuote parseQuoteFromData:quoteData]; + if (!quote) { + OWSFailDebug(@"couldn't parse quote."); + return nil; + } + NSData *_Nullable requestId = [self decryptRequestId:encryptedRequestId + encryptedRequestIv:encryptedRequestIv + encryptedRequestTag:encryptedRequestTag + keys:keys]; + if (!requestId) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"couldn't decrypt request id."); + return nil; + } + + if (![self verifyServerQuote:quote keys:keys enclaveId:enclaveId]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"couldn't verify quote."); + return nil; + } + + if (![self verifyIasSignatureWithCertificates:certificates + signatureBody:signatureBody + signature:signature + quoteData:quoteData + error:error]) { + + if (*error == nil) { + OWSFailDebug(@"missing error specifics"); + *error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAssertionError, + @"verifyIasSignatureWithCertificates failed. No reason given"); + } + return nil; + } + + RemoteAttestation *result = [RemoteAttestation new]; + result.cookies = cookies; + result.keys = keys; + result.requestId = requestId; + result.enclaveId = enclaveId; + result.auth = auth; + + OWSLogVerbose(@"remote attestation complete."); + + return result; +} + +- (BOOL)verifyIasSignatureWithCertificates:(NSString *)certificates + signatureBody:(NSString *)signatureBody + signature:(NSData *)signature + quoteData:(NSData *)quoteData + error:(NSError **)error +{ + OWSAssertDebug(certificates.length > 0); + OWSAssertDebug(signatureBody.length > 0); + OWSAssertDebug(signature.length > 0); + OWSAssertDebug(quoteData); + + NSError *signingError; + CDSSigningCertificate *_Nullable certificate = + [CDSSigningCertificate parseCertificateFromPem:certificates error:&signingError]; + if (signingError) { + *error = signingError; + return NO; + } + + if (!certificate) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"could not parse signing certificate."); + return NO; + } + if (![certificate verifySignatureOfBody:signatureBody signature:signature]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"could not verify signature."); + return NO; + } + + SignatureBodyEntity *_Nullable signatureBodyEntity = [self parseSignatureBodyEntity:signatureBody]; + if (!signatureBodyEntity) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"could not parse signature body."); + return NO; + } + + // Compare the first N bytes of the quote data with the signed quote body. + const NSUInteger kQuoteBodyComparisonLength = 432; + if (signatureBodyEntity.isvEnclaveQuoteBody.length < kQuoteBodyComparisonLength) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"isvEnclaveQuoteBody has unexpected length."); + return NO; + } + // NOTE: This version is separate from and does _NOT_ match the CDS quote version. + const NSUInteger kSignatureBodyVersion = 3; + if (![signatureBodyEntity.version isEqual:@(kSignatureBodyVersion)]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"signatureBodyEntity has unexpected version."); + return NO; + } + if (quoteData.length < kQuoteBodyComparisonLength) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"quoteData has unexpected length."); + return NO; + } + NSData *isvEnclaveQuoteBodyForComparison = + [signatureBodyEntity.isvEnclaveQuoteBody subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)]; + NSData *quoteDataForComparison = [quoteData subdataWithRange:NSMakeRange(0, kQuoteBodyComparisonLength)]; + if (![isvEnclaveQuoteBodyForComparison ows_constantTimeIsEqualToData:quoteDataForComparison]) { + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"isvEnclaveQuoteBody and quoteData do not match."); + return NO; + } + + if (![@"OK" isEqualToString:signatureBodyEntity.isvEnclaveQuoteStatus]) { + NSString *reason = + [NSString stringWithFormat:@"invalid isvEnclaveQuoteStatus: %@", signatureBodyEntity.isvEnclaveQuoteStatus]; + *error = ContactDiscoveryServiceErrorMakeWithReason(ContactDiscoveryServiceErrorAttestationFailed, reason); + return NO; + } + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; + [dateFormatter setTimeZone:timeZone]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSSSS"]; + + // Specify parsing locale + // from: https://developer.apple.com/library/archive/qa/qa1480/_index.html + // Q: I'm using NSDateFormatter to parse an Internet-style date, but this fails for some users in some regions. + // I've set a specific date format string; shouldn't that force NSDateFormatter to work independently of the user's + // region settings? A: No. While setting a date format string will appear to work for most users, it's not the right + // solution to this problem. There are many places where format strings behave in unexpected ways. [...] + NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + [dateFormatter setLocale:enUSPOSIXLocale]; + NSDate *timestampDate = [dateFormatter dateFromString:signatureBodyEntity.timestamp]; + if (!timestampDate) { + OWSFailDebug(@"Could not parse signature body timestamp: %@", signatureBodyEntity.timestamp); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAssertionError, @"could not parse signature body timestamp."); + return NO; + } + + // Only accept signatures from the last 24 hours. + NSDateComponents *dayComponent = [[NSDateComponents alloc] init]; + dayComponent.day = 1; + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSDate *timestampDatePlus1Day = [calendar dateByAddingComponents:dayComponent toDate:timestampDate options:0]; + + NSDate *now = [NSDate new]; + BOOL isExpired = [now isAfterDate:timestampDatePlus1Day]; + + if (isExpired) { + OWSFailDebug(@"Signature is expired: %@", signatureBodyEntity.timestamp); + *error = ContactDiscoveryServiceErrorMakeWithReason( + ContactDiscoveryServiceErrorAttestationFailed, @"Signature is expired."); + return NO; + } + + return YES; +} + +- (nullable SignatureBodyEntity *)parseSignatureBodyEntity:(NSString *)signatureBody +{ + OWSAssertDebug(signatureBody.length > 0); + + NSError *error = nil; + NSDictionary *_Nullable jsonDict = + [NSJSONSerialization JSONObjectWithData:[signatureBody dataUsingEncoding:NSUTF8StringEncoding] + options:0 + error:&error]; + if (error || ![jsonDict isKindOfClass:[NSDictionary class]]) { + OWSFailDebug(@"could not parse signature body JSON: %@.", error); + return nil; + } + NSString *_Nullable timestamp = [jsonDict stringForKey:@"timestamp"]; + if (timestamp.length < 1) { + OWSFailDebug(@"could not parse signature timestamp."); + return nil; + } + NSData *_Nullable isvEnclaveQuoteBody = [jsonDict base64DataForKey:@"isvEnclaveQuoteBody"]; + if (isvEnclaveQuoteBody.length < 1) { + OWSFailDebug(@"could not parse signature isvEnclaveQuoteBody."); + return nil; + } + NSString *_Nullable isvEnclaveQuoteStatus = [jsonDict stringForKey:@"isvEnclaveQuoteStatus"]; + if (isvEnclaveQuoteStatus.length < 1) { + OWSFailDebug(@"could not parse signature isvEnclaveQuoteStatus."); + return nil; + } + NSNumber *_Nullable version = [jsonDict numberForKey:@"version"]; + if (!version) { + OWSFailDebug(@"could not parse signature version."); + return nil; + } + + SignatureBodyEntity *result = [SignatureBodyEntity new]; + result.isvEnclaveQuoteBody = isvEnclaveQuoteBody; + result.isvEnclaveQuoteStatus = isvEnclaveQuoteStatus; + result.timestamp = timestamp; + result.version = version; + return result; +} + +- (BOOL)verifyServerQuote:(CDSQuote *)quote keys:(RemoteAttestationKeys *)keys enclaveId:(NSString *)enclaveId +{ + OWSAssertDebug(quote); + OWSAssertDebug(keys); + OWSAssertDebug(enclaveId.length > 0); + + if (quote.reportData.length < keys.serverStaticPublic.length) { + OWSFailDebug(@"reportData has unexpected length: %lu != %lu.", + (unsigned long)quote.reportData.length, + (unsigned long)keys.serverStaticPublic.length); + return NO; + } + + NSData *_Nullable theirServerPublicStatic = + [quote.reportData subdataWithRange:NSMakeRange(0, keys.serverStaticPublic.length)]; + if (theirServerPublicStatic.length != keys.serverStaticPublic.length) { + OWSFailDebug(@"could not extract server public static."); + return NO; + } + if (![keys.serverStaticPublic ows_constantTimeIsEqualToData:theirServerPublicStatic]) { + OWSFailDebug(@"server public statics do not match."); + return NO; + } + // It's easier to compare as hex data than parsing hexadecimal. + NSData *_Nullable ourEnclaveIdHexData = [enclaveId dataUsingEncoding:NSUTF8StringEncoding]; + NSData *_Nullable theirEnclaveIdHexData = + [quote.mrenclave.hexadecimalString dataUsingEncoding:NSUTF8StringEncoding]; + if (!ourEnclaveIdHexData || !theirEnclaveIdHexData + || ![ourEnclaveIdHexData ows_constantTimeIsEqualToData:theirEnclaveIdHexData]) { + OWSFailDebug(@"enclave ids do not match."); + return NO; + } + if (quote.isDebugQuote) { + OWSFailDebug(@"quote has invalid isDebugQuote value."); + return NO; + } + return YES; +} + +- (nullable NSData *)decryptRequestId:(NSData *)encryptedRequestId + encryptedRequestIv:(NSData *)encryptedRequestIv + encryptedRequestTag:(NSData *)encryptedRequestTag + keys:(RemoteAttestationKeys *)keys +{ + OWSAssertDebug(encryptedRequestId.length > 0); + OWSAssertDebug(encryptedRequestIv.length > 0); + OWSAssertDebug(encryptedRequestTag.length > 0); + OWSAssertDebug(keys); + + OWSAES256Key *_Nullable key = keys.serverKey; + if (!key) { + OWSFailDebug(@"invalid server key."); + return nil; + } + NSData *_Nullable decryptedData = [Cryptography decryptAESGCMWithInitializationVector:encryptedRequestIv + ciphertext:encryptedRequestId + additionalAuthenticatedData:nil + authTag:encryptedRequestTag + key:key]; + if (!decryptedData) { + OWSFailDebug(@"couldn't decrypt request id."); + return nil; + } + return decryptedData; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ContactParser.swift b/SignalUtilitiesKit/ContactParser.swift new file mode 100644 index 000000000..035bb580f --- /dev/null +++ b/SignalUtilitiesKit/ContactParser.swift @@ -0,0 +1,28 @@ + +public final class ContactParser { + private let data: Data + + public init(data: Data) { + self.data = data + } + + public func parse() -> [(publicKey: String, isBlocked: Bool)] { + var index = 0 + var result: [(String, Bool)] = [] + while index < data.endIndex { + var uncheckedSize: UInt32? = try? data[index..<(index+4)].withUnsafeBytes { $0.pointee } + if let size = uncheckedSize, size >= data.count, let intermediate = try? data[index..<(index+4)].reversed() { + uncheckedSize = Data(intermediate).withUnsafeBytes { $0.pointee } + } + guard let size = uncheckedSize, size < data.count else { break } + let sizeAsInt = Int(size) + index += 4 + guard index + sizeAsInt <= data.count else { break } + let protoAsData = data[index..<(index+sizeAsInt)] + guard let proto = try? SSKProtoContactDetails.parseData(protoAsData) else { break } + index += sizeAsInt + result.append((publicKey: proto.number, isBlocked: proto.blocked)) + } + return result + } +} diff --git a/SignalUtilitiesKit/ContactsManagerProtocol.h b/SignalUtilitiesKit/ContactsManagerProtocol.h new file mode 100644 index 000000000..7dff8e6c7 --- /dev/null +++ b/SignalUtilitiesKit/ContactsManagerProtocol.h @@ -0,0 +1,37 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class CNContact; +@class Contact; +@class PhoneNumber; +@class SignalAccount; +@class UIImage; +@class YapDatabaseReadTransaction; + +@protocol ContactsManagerProtocol + +- (NSString *)displayNameForPhoneIdentifier:(nullable NSString *)recipientId; +- (NSString *)displayNameForPhoneIdentifier:(NSString *_Nullable)recipientId + transaction:(YapDatabaseReadTransaction *)transaction; +- (NSArray *)signalAccounts; + +- (BOOL)isSystemContact:(NSString *)recipientId; +- (BOOL)isSystemContactWithSignalAccount:(NSString *)recipientId; + +- (NSComparisonResult)compareSignalAccount:(SignalAccount *)left + withSignalAccount:(SignalAccount *)right NS_SWIFT_NAME(compare(signalAccount:with:)); + +#pragma mark - CNContacts + +- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId; +- (nullable NSData *)avatarDataForCNContactId:(nullable NSString *)contactId; +- (nullable UIImage *)avatarImageForCNContactId:(nullable NSString *)contactId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ContactsUpdater.h b/SignalUtilitiesKit/ContactsUpdater.h new file mode 100644 index 000000000..732b2c5c4 --- /dev/null +++ b/SignalUtilitiesKit/ContactsUpdater.h @@ -0,0 +1,25 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "SignalRecipient.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ContactsUpdater : NSObject + ++ (instancetype)sharedUpdater; + +// This asynchronously tries to verify whether or not a group of possible +// contact ids correspond to service accounts. +// +// The failure callback is only invoked if the lookup fails. Otherwise, +// the success callback is invoked with the (possibly empty) set of contacts +// that were found. +- (void)lookupIdentifiers:(NSArray *)identifiers + success:(void (^)(NSArray *recipients))success + failure:(void (^)(NSError *error))failure; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ContactsUpdater.m b/SignalUtilitiesKit/ContactsUpdater.m new file mode 100644 index 000000000..1fa10886a --- /dev/null +++ b/SignalUtilitiesKit/ContactsUpdater.m @@ -0,0 +1,120 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "ContactsUpdater.h" +#import "OWSError.h" +#import "OWSPrimaryStorage.h" +#import "OWSRequestFactory.h" +#import "PhoneNumber.h" +#import "SSKEnvironment.h" +#import "TSNetworkManager.h" +#import +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ContactsUpdater () + +@property (nonatomic, readonly) NSOperationQueue *contactIntersectionQueue; + +@end + +#pragma mark - + +@implementation ContactsUpdater + ++ (instancetype)sharedUpdater { + OWSAssertDebug(SSKEnvironment.shared.contactsUpdater); + + return SSKEnvironment.shared.contactsUpdater; +} + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + _contactIntersectionQueue = [NSOperationQueue new]; + _contactIntersectionQueue.maxConcurrentOperationCount = 1; + _contactIntersectionQueue.name = self.logTag; + + OWSSingletonAssert(); + + return self; +} + +- (void)lookupIdentifiers:(NSArray *)identifiers + success:(void (^)(NSArray *recipients))success + failure:(void (^)(NSError *error))failure +{ + if (identifiers.count < 1) { + OWSFailDebug(@"Cannot lookup zero identifiers"); + DispatchMainThreadSafe(^{ + failure( + OWSErrorWithCodeDescription(OWSErrorCodeInvalidMethodParameters, @"Cannot lookup zero identifiers")); + }); + return; + } + + [self contactIntersectionWithSet:[NSSet setWithArray:identifiers] + success:^(NSSet *recipients) { + if (recipients.count == 0) { + OWSLogInfo(@"no contacts are Signal users"); + } + DispatchMainThreadSafe(^{ + success(recipients.allObjects); + }); + } + failure:^(NSError *error) { + DispatchMainThreadSafe(^{ + failure(error); + }); + }]; +} + +- (void)contactIntersectionWithSet:(NSSet *)recipientIdsToLookup + success:(void (^)(NSSet *recipients))success + failure:(void (^)(NSError *error))failure +{ + OWSLegacyContactDiscoveryOperation *operation = + [[OWSLegacyContactDiscoveryOperation alloc] initWithRecipientIdsToLookup:recipientIdsToLookup.allObjects]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSArray *operationAndDependencies = [operation.dependencies arrayByAddingObject:operation]; + [self.contactIntersectionQueue addOperations:operationAndDependencies waitUntilFinished:YES]; + + if (operation.failingError != nil) { + failure(operation.failingError); + return; + } + + NSSet *registeredRecipientIds = operation.registeredRecipientIds; + + NSMutableSet *recipients = [NSMutableSet new]; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSString *recipientId in recipientIdsToLookup) { + if ([registeredRecipientIds containsObject:recipientId]) { + SignalRecipient *recipient = + [SignalRecipient markRecipientAsRegisteredAndGet:recipientId transaction:transaction]; + [recipients addObject:recipient]; + } else { + [SignalRecipient markRecipientAsUnregistered:recipientId transaction:transaction]; + } + } + }]; + + dispatch_async(dispatch_get_main_queue(), ^{ + success([recipients copy]); + }); + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ContentProxy.swift b/SignalUtilitiesKit/ContentProxy.swift new file mode 100644 index 000000000..2efb7765f --- /dev/null +++ b/SignalUtilitiesKit/ContentProxy.swift @@ -0,0 +1,127 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + + +@objc +public class ContentProxy: NSObject { + + @available(*, unavailable, message:"do not instantiate this class.") + private override init() { + } + + @objc + public class func sessionConfiguration() -> URLSessionConfiguration { + let configuration = URLSessionConfiguration.ephemeral + let proxyHost = "contentproxy.signal.org" + let proxyPort = 443 + configuration.connectionProxyDictionary = [ + "HTTPEnable": 1, + "HTTPProxy": proxyHost, + "HTTPPort": proxyPort, + "HTTPSEnable": 1, + "HTTPSProxy": proxyHost, + "HTTPSPort": proxyPort + ] + return configuration + } + + @objc + public class func sessionManager(baseUrl baseUrlString: String?) -> AFHTTPSessionManager? { + guard let baseUrlString = baseUrlString else { + return AFHTTPSessionManager(baseURL: nil, sessionConfiguration: sessionConfiguration()) + } + guard let baseUrl = URL(string: baseUrlString) else { + owsFailDebug("Invalid base URL.") + return nil + } + let sessionManager = AFHTTPSessionManager(baseURL: baseUrl, + sessionConfiguration: sessionConfiguration()) + return sessionManager + } + + @objc + public class func jsonSessionManager(baseUrl: String) -> AFHTTPSessionManager? { + guard let sessionManager = self.sessionManager(baseUrl: baseUrl) else { + owsFailDebug("Could not create session manager") + return nil + } + sessionManager.requestSerializer = AFJSONRequestSerializer() + sessionManager.responseSerializer = AFJSONResponseSerializer() + return sessionManager + } + + static let userAgent = "Signal iOS (+https://signal.org/download)" + + public class func configureProxiedRequest(request: inout URLRequest) -> Bool { + request.addValue(userAgent, forHTTPHeaderField: "User-Agent") + + padRequestSize(request: &request) + + guard let url = request.url, + let scheme = url.scheme, + scheme.lowercased() == "https" else { + return false + } + return true + } + + // This mutates the session manager state, so its the caller's obligation to avoid conflicts by: + // + // * Using a new session manager for each request. + // * Pooling session managers. + // * Using a single session manager on a single queue. + @objc + public class func configureSessionManager(sessionManager: AFHTTPSessionManager, + forUrl urlString: String) -> Bool { + + guard let url = URL(string: urlString, relativeTo: sessionManager.baseURL) else { + owsFailDebug("Invalid URL query: \(urlString).") + return false + } + + var request = URLRequest(url: url) + + guard configureProxiedRequest(request: &request) else { + owsFailDebug("Invalid URL query: \(urlString).") + return false + } + + // Remove all headers from the request. + for headerField in sessionManager.requestSerializer.httpRequestHeaders.keys { + sessionManager.requestSerializer.setValue(nil, forHTTPHeaderField: headerField) + } + // Honor the request's headers. + if let allHTTPHeaderFields = request.allHTTPHeaderFields { + for (headerField, headerValue) in allHTTPHeaderFields { + sessionManager.requestSerializer.setValue(headerValue, forHTTPHeaderField: headerField) + } + } + return true + } + + public class func padRequestSize(request: inout URLRequest) { + // Generate 1-64 chars of padding. + let paddingLength: Int = 1 + Int(arc4random_uniform(64)) + let padding = self.padding(withLength: paddingLength) + assert(padding.count == paddingLength) + request.addValue(padding, forHTTPHeaderField: "X-SignalPadding") + } + + private class func padding(withLength length: Int) -> String { + // Pick a random ASCII char in the range 48-122 + var result = "" + // Min and max values, inclusive. + let minValue: UInt32 = 48 + let maxValue: UInt32 = 122 + for _ in 1...length { + let value = minValue + arc4random_uniform(maxValue - minValue + 1) + assert(value >= minValue) + assert(value <= maxValue) + result += String(UnicodeScalar(UInt8(value))) + } + return result + } +} diff --git a/SignalUtilitiesKit/CreatePreKeysOperation.swift b/SignalUtilitiesKit/CreatePreKeysOperation.swift new file mode 100644 index 000000000..5a8c75bcf --- /dev/null +++ b/SignalUtilitiesKit/CreatePreKeysOperation.swift @@ -0,0 +1,57 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +@objc(SSKCreatePreKeysOperation) +public class CreatePreKeysOperation: OWSOperation { + + private var accountServiceClient: AccountServiceClient { + return AccountServiceClient.shared + } + + private var primaryStorage: OWSPrimaryStorage { + return OWSPrimaryStorage.shared() + } + + private var identityKeyManager: OWSIdentityManager { + return OWSIdentityManager.shared() + } + + public override func run() { + Logger.debug("") + + if identityKeyManager.identityKeyPair() == nil { + identityKeyManager.generateNewIdentityKeyPair() + } + + SessionManagementProtocol.createPreKeys() + reportSuccess() + + /* Loki: Original code + * ================ + let identityKey: Data = self.identityKeyManager.identityKeyPair()!.publicKey + let signedPreKeyRecord: SignedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord() + let preKeyRecords: [PreKeyRecord] = self.primaryStorage.generatePreKeyRecords() + + self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + self.primaryStorage.storePreKeyRecords(preKeyRecords) + + firstly { + self.accountServiceClient.setPreKeys(identityKey: identityKey, signedPreKeyRecord: signedPreKeyRecord, preKeyRecords: preKeyRecords) + }.done { + signedPreKeyRecord.markAsAcceptedByService() + self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id) + + Logger.debug("done") + self.reportSuccess() + }.catch { error in + self.reportError(error) + }.retainUntilComplete() + * ================ + */ + } +} diff --git a/SignalUtilitiesKit/Data+SecureRandom.swift b/SignalUtilitiesKit/Data+SecureRandom.swift new file mode 100644 index 000000000..e520e4553 --- /dev/null +++ b/SignalUtilitiesKit/Data+SecureRandom.swift @@ -0,0 +1,12 @@ + +public extension Data { + + /// Returns `size` bytes of random data generated using the default secure random number generator. See + /// [SecRandomCopyBytes](https://developer.apple.com/documentation/security/1399291-secrandomcopybytes) for more information. + public static func getSecureRandomData(ofSize size: UInt) -> Data? { + var data = Data(count: Int(size)) + let result = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, Int(size), $0.baseAddress!) } + guard result == errSecSuccess else { return nil } + return data + } +} diff --git a/SignalUtilitiesKit/Data+Streaming.swift b/SignalUtilitiesKit/Data+Streaming.swift new file mode 100644 index 000000000..745dc3792 --- /dev/null +++ b/SignalUtilitiesKit/Data+Streaming.swift @@ -0,0 +1,22 @@ + +extension Data { + + init(from inputStream: InputStream) throws { + self.init() + inputStream.open() + defer { inputStream.close() } + let bufferSize = 1024 + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + defer { buffer.deallocate() } + while inputStream.hasBytesAvailable { + let count = inputStream.read(buffer, maxLength: bufferSize) + if count < 0 { + throw inputStream.streamError! + } else if count == 0 { + break + } else { + append(buffer, count: count) + } + } + } +} diff --git a/SignalUtilitiesKit/DataSource.h b/SignalUtilitiesKit/DataSource.h new file mode 100755 index 000000000..5401c4c35 --- /dev/null +++ b/SignalUtilitiesKit/DataSource.h @@ -0,0 +1,67 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// A base class that abstracts away a source of NSData +// and allows us to: +// +// * Lazy-load if possible. +// * Avoid duplicate reads & writes. +@interface DataSource : NSObject + +@property (nonatomic, nullable) NSString *sourceFilename; + +// Should not be called unless necessary as it can involve an expensive read. +- (NSData *)data; + +// The URL for the data. Should always be a File URL. +// +// Should not be called unless necessary as it can involve an expensive write. +// +// Will only return nil in the error case. +- (nullable NSURL *)dataUrl; + +// Will return zero in the error case. +- (NSUInteger)dataLength; + +// Returns YES on success. +- (BOOL)writeToPath:(NSString *)dstFilePath; + +- (BOOL)isValidImage; + +- (BOOL)isValidVideo; + +@end + +#pragma mark - + +@interface DataSourceValue : DataSource + ++ (nullable DataSource *)dataSourceWithData:(NSData *)data fileExtension:(NSString *)fileExtension; + ++ (nullable DataSource *)dataSourceWithData:(NSData *)data utiType:(NSString *)utiType; + ++ (nullable DataSource *)dataSourceWithOversizeText:(NSString *_Nullable)text; + ++ (DataSource *)dataSourceWithSyncMessageData:(NSData *)data; + ++ (DataSource *)emptyDataSource; + +@end + +#pragma mark - + +@interface DataSourcePath : DataSource + ++ (nullable DataSource *)dataSourceWithURL:(NSURL *)fileUrl shouldDeleteOnDeallocation:(BOOL)shouldDeleteOnDeallocation; + ++ (nullable DataSource *)dataSourceWithFilePath:(NSString *)filePath + shouldDeleteOnDeallocation:(BOOL)shouldDeleteOnDeallocation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/DataSource.m b/SignalUtilitiesKit/DataSource.m new file mode 100755 index 000000000..3772908c0 --- /dev/null +++ b/SignalUtilitiesKit/DataSource.m @@ -0,0 +1,401 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "DataSource.h" +#import "MIMETypeUtil.h" +#import "NSData+Image.h" +#import "OWSFileSystem.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DataSource () + +@property (nonatomic) BOOL shouldDeleteOnDeallocation; + +// The file path for the data, if it already exists on disk. +// +// This method is safe to call as it will not do any expensive reads or writes. +// +// May return nil if the data does not (yet) reside on disk. +// +// Use dataUrl instead if you need to access the data; it will +// ensure the data is on disk and return a URL, barring an error. +- (nullable NSString *)dataPathIfOnDisk; + +@end + +#pragma mark - + +@implementation DataSource + +- (NSData *)data +{ + OWSAbstractMethod(); + return nil; +} + +- (nullable NSURL *)dataUrl +{ + OWSAbstractMethod(); + return nil; +} + +- (nullable NSString *)dataPathIfOnDisk +{ + OWSAbstractMethod(); + return nil; +} + +- (NSUInteger)dataLength +{ + OWSAbstractMethod(); + return 0; +} + +- (BOOL)writeToPath:(NSString *)dstFilePath +{ + OWSAbstractMethod(); + return NO; +} + +- (BOOL)isValidImage +{ + NSString *_Nullable dataPath = [self dataPathIfOnDisk]; + if (dataPath) { + // if ows_isValidImage is given a file path, it will + // avoid loading most of the data into memory, which + // is considerably more performant, so try to do that. + return [NSData ows_isValidImageAtPath:dataPath mimeType:self.mimeType]; + } + NSData *data = [self data]; + return [data ows_isValidImage]; +} + +- (BOOL)isValidVideo +{ + return [OWSMediaUtils isValidVideoWithPath:self.dataUrl.path]; +} + +- (void)setSourceFilename:(nullable NSString *)sourceFilename +{ + _sourceFilename = sourceFilename.filterFilename; +} + +// Returns the MIME type, if known. +- (nullable NSString *)mimeType +{ + OWSAbstractMethod(); + + return nil; +} + +@end + +#pragma mark - + +@interface DataSourceValue () + +@property (nonatomic) NSData *dataValue; + +@property (nonatomic) NSString *fileExtension; + +// This property is lazy-populated. +@property (nonatomic, nullable) NSString *cachedFilePath; + +@end + +#pragma mark - + +@implementation DataSourceValue + +- (void)dealloc +{ + if (self.shouldDeleteOnDeallocation) { + NSString *_Nullable filePath = self.cachedFilePath; + if (filePath) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error; + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + if (!success || error) { + OWSCFailDebug(@"DataSourceValue could not delete file: %@, %@", filePath, error); + } + }); + } + } +} + ++ (nullable DataSource *)dataSourceWithData:(NSData *)data + fileExtension:(NSString *)fileExtension +{ + OWSAssertDebug(data); + + if (!data) { + return nil; + } + + DataSourceValue *instance = [DataSourceValue new]; + instance.dataValue = data; + instance.fileExtension = fileExtension; + instance.shouldDeleteOnDeallocation = YES; + return instance; +} + ++ (nullable DataSource *)dataSourceWithData:(NSData *)data + utiType:(NSString *)utiType +{ + NSString *fileExtension = [MIMETypeUtil fileExtensionForUTIType:utiType]; + return [self dataSourceWithData:data fileExtension:fileExtension]; +} + ++ (nullable DataSource *)dataSourceWithOversizeText:(NSString *_Nullable)text +{ + if (!text) { + return nil; + } + + NSData *data = [text.filterStringForDisplay dataUsingEncoding:NSUTF8StringEncoding]; + return [self dataSourceWithData:data fileExtension:kOversizeTextAttachmentFileExtension]; +} + ++ (DataSource *)dataSourceWithSyncMessageData:(NSData *)data +{ + return [self dataSourceWithData:data fileExtension:kSyncMessageFileExtension]; +} + ++ (DataSource *)emptyDataSource +{ + return [self dataSourceWithData:[NSData new] fileExtension:@"bin"]; +} + +- (NSData *)data +{ + OWSAssertDebug(self.dataValue); + + return self.dataValue; +} + +- (nullable NSURL *)dataUrl +{ + NSString *_Nullable path = [self dataPath]; + return (path ? [NSURL fileURLWithPath:path] : nil); +} + +- (nullable NSString *)dataPath +{ + OWSAssertDebug(self.dataValue); + + @synchronized(self) + { + if (!self.cachedFilePath) { + NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:self.fileExtension]; + if ([self writeToPath:filePath]) { + self.cachedFilePath = filePath; + } else { + OWSLogDebug(@"Could not write data to disk: %@", self.fileExtension); + OWSFailDebug(@"Could not write data to disk."); + } + } + + return self.cachedFilePath; + } +} + +- (nullable NSString *)dataPathIfOnDisk +{ + return self.cachedFilePath; +} + +- (NSUInteger)dataLength +{ + OWSAssertDebug(self.dataValue); + + return self.dataValue.length; +} + +- (BOOL)writeToPath:(NSString *)dstFilePath +{ + OWSAssertDebug(self.dataValue); + + NSData *dataCopy = self.dataValue; + + BOOL success = [dataCopy writeToFile:dstFilePath atomically:YES]; + if (!success) { + OWSLogDebug(@"Could not write data to disk: %@", dstFilePath); + OWSFailDebug(@"Could not write data to disk."); + return NO; + } else { + return YES; + } +} + +- (nullable NSString *)mimeType +{ + return (self.fileExtension ? [MIMETypeUtil mimeTypeForFileExtension:self.fileExtension] : nil); +} + +@end + +#pragma mark - + +@interface DataSourcePath () + +@property (nonatomic) NSString *filePath; + +// These properties are lazy-populated. +@property (nonatomic) NSData *cachedData; +@property (nonatomic) NSNumber *cachedDataLength; + +@end + +#pragma mark - + +@implementation DataSourcePath + +- (void)dealloc +{ + if (self.shouldDeleteOnDeallocation) { + NSString *filePath = self.filePath; + if (filePath) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error; + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + if (!success || error) { + OWSCFailDebug(@"DataSourcePath could not delete file: %@, %@", filePath, error); + } + }); + } + } +} + ++ (nullable DataSource *)dataSourceWithURL:(NSURL *)fileUrl shouldDeleteOnDeallocation:(BOOL)shouldDeleteOnDeallocation +{ + OWSAssertDebug(fileUrl); + + if (!fileUrl || ![fileUrl isFileURL]) { + return nil; + } + DataSourcePath *instance = [DataSourcePath new]; + instance.filePath = fileUrl.path; + instance.shouldDeleteOnDeallocation = shouldDeleteOnDeallocation; + return instance; +} + ++ (nullable DataSource *)dataSourceWithFilePath:(NSString *)filePath + shouldDeleteOnDeallocation:(BOOL)shouldDeleteOnDeallocation +{ + OWSAssertDebug(filePath); + + if (!filePath) { + return nil; + } + + DataSourcePath *instance = [DataSourcePath new]; + instance.filePath = filePath; + instance.shouldDeleteOnDeallocation = shouldDeleteOnDeallocation; + return instance; +} + +- (void)setFilePath:(NSString *)filePath +{ + OWSAssertDebug(filePath.length > 0); + +#ifdef DEBUG + BOOL isDirectory; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]; + OWSAssertDebug(exists); + OWSAssertDebug(!isDirectory); +#endif + + _filePath = filePath; +} + +- (NSData *)data +{ + OWSAssertDebug(self.filePath); + + @synchronized(self) + { + if (!self.cachedData) { + self.cachedData = [NSData dataWithContentsOfFile:self.filePath]; + } + if (!self.cachedData) { + OWSLogDebug(@"Could not read data from disk: %@", self.filePath); + OWSFailDebug(@"Could not read data from disk."); + self.cachedData = [NSData new]; + } + return self.cachedData; + } +} + +- (nullable NSURL *)dataUrl +{ + OWSAssertDebug(self.filePath); + + return [NSURL fileURLWithPath:self.filePath]; +} + +- (nullable NSString *)dataPath +{ + OWSAssertDebug(self.filePath); + + return self.filePath; +} + +- (nullable NSString *)dataPathIfOnDisk +{ + OWSAssertDebug(self.filePath); + + return self.filePath; +} + +- (NSUInteger)dataLength +{ + OWSAssertDebug(self.filePath); + + @synchronized(self) + { + if (!self.cachedDataLength) { + NSError *error; + NSDictionary *_Nullable attributes = + [[NSFileManager defaultManager] attributesOfItemAtPath:self.filePath error:&error]; + if (!attributes || error) { + OWSLogDebug(@"Could not read data length from disk: %@, %@", self.filePath, error); + OWSFailDebug(@"Could not read data length from disk with error: %@", error); + self.cachedDataLength = @(0); + } else { + uint64_t fileSize = [attributes fileSize]; + self.cachedDataLength = @(fileSize); + } + } + return [self.cachedDataLength unsignedIntegerValue]; + } +} + +- (BOOL)writeToPath:(NSString *)dstFilePath +{ + OWSAssertDebug(self.filePath); + + NSError *error; + BOOL success = [[NSFileManager defaultManager] copyItemAtPath:self.filePath toPath:dstFilePath error:&error]; + if (!success || error) { + OWSLogDebug(@"Could not write data from path: %@, to path: %@, %@", self.filePath, dstFilePath, error); + OWSFailDebug(@"Could not write data with error: %@", error); + return NO; + } else { + return YES; + } +} + +- (nullable NSString *)mimeType +{ + NSString *_Nullable fileExtension = self.filePath.pathExtension; + return (fileExtension ? [MIMETypeUtil mimeTypeForFileExtension:fileExtension] : nil); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Debugging.swift b/SignalUtilitiesKit/Debugging.swift new file mode 100644 index 000000000..87a709f44 --- /dev/null +++ b/SignalUtilitiesKit/Debugging.swift @@ -0,0 +1,12 @@ + +// For some reason NSLog doesn't seem to work from SignalServiceKit. This is a workaround to still allow debugging from Obj-C. + +@objc(LKLogger) +public final class ObjC_Logger : NSObject { + + private override init() { } + + @objc public static func print(_ message: String) { + Swift.print(message) + } +} diff --git a/SignalUtilitiesKit/DecryptionUtilities.swift b/SignalUtilitiesKit/DecryptionUtilities.swift new file mode 100644 index 000000000..974451235 --- /dev/null +++ b/SignalUtilitiesKit/DecryptionUtilities.swift @@ -0,0 +1,18 @@ +import CryptoSwift + +enum DecryptionUtilities { + + /// - Note: Sync. Don't call from the main thread. + internal static func decrypt(_ ivAndCiphertext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data { + if Thread.isMainThread { + #if DEBUG + preconditionFailure("It's illegal to call decrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.") + #endif + } + let iv = ivAndCiphertext[0.. Bool { + guard let other = other as? Device else { return false } + return publicKey == other.publicKey && signature == other.signature + } + + @objc override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:) + var result = publicKey.hashValue + if let signature = signature { result = result ^ signature.hashValue } + return result + } + + @objc override public var description: String { return publicKey } + } + + // MARK: Lifecycle + @objc public init(between master: Device, and slave: Device) { + self.master = master + self.slave = slave + } + + // MARK: Coding + @objc public init?(coder: NSCoder) { + master = coder.decodeObject(forKey: "master") as! Device + slave = coder.decodeObject(forKey: "slave") as! Device + super.init() + } + + @objc public func encode(with coder: NSCoder) { + coder.encode(master, forKey: "master") + coder.encode(slave, forKey: "slave") + } + + // MARK: JSON + public func toJSON() -> JSON { + var result = [ "primaryDevicePubKey" : master.publicKey, "secondaryDevicePubKey" : slave.publicKey ] + if let masterSignature = master.signature { result["grantSignature"] = masterSignature.base64EncodedString() } + if let slaveSignature = slave.signature { result["requestSignature"] = slaveSignature.base64EncodedString() } + return result + } + + // MARK: Equality + @objc override public func isEqual(_ other: Any?) -> Bool { + guard let other = other as? DeviceLink else { return false } + return master == other.master && slave == other.slave + } + + // MARK: Hashing + @objc override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:) + return master.hash ^ slave.hash + } + + // MARK: Description + @objc override public var description: String { return "\(master) - \(slave)" } +} diff --git a/SignalUtilitiesKit/DeviceLinkIndex.swift b/SignalUtilitiesKit/DeviceLinkIndex.swift new file mode 100644 index 000000000..15c3fd0ec --- /dev/null +++ b/SignalUtilitiesKit/DeviceLinkIndex.swift @@ -0,0 +1,43 @@ + +@objc(LKDeviceLinkIndex) +public final class DeviceLinkIndex : NSObject { + + private static let name = "loki_device_link_index" + + @objc public static let masterPublicKey = "master_hex_encoded_public_key" + @objc public static let slavePublicKey = "slave_hex_encoded_public_key" + @objc public static let isAuthorized = "is_authorized" + + @objc public static let indexDatabaseExtension: YapDatabaseSecondaryIndex = { + let setup = YapDatabaseSecondaryIndexSetup() + setup.addColumn(masterPublicKey, with: .text) + setup.addColumn(slavePublicKey, with: .text) + setup.addColumn(isAuthorized, with: .integer) + let handler = YapDatabaseSecondaryIndexHandler.withObjectBlock { _, map, _, _, object in + guard let deviceLink = object as? DeviceLink else { return } + map[masterPublicKey] = deviceLink.master.publicKey + map[slavePublicKey] = deviceLink.slave.publicKey + map[isAuthorized] = deviceLink.isAuthorized + } + return YapDatabaseSecondaryIndex(setup: setup, handler: handler) + }() + + @objc public static let databaseExtensionName: String = name + + @objc public static func asyncRegisterDatabaseExtensions(_ storage: OWSStorage) { + storage.asyncRegister(indexDatabaseExtension, withName: name) + } + + @objc public static func getDeviceLinks(for query: YapDatabaseQuery, in transaction: YapDatabaseReadTransaction) -> [DeviceLink] { + guard let ext = transaction.ext(DeviceLinkIndex.name) as? YapDatabaseSecondaryIndexTransaction else { + print("[Loki] Couldn't get device link index database extension.") + return [] + } + var result: [DeviceLink] = [] + ext.enumerateKeysAndObjects(matching: query) { _, _, object, _ in + guard let deviceLink = object as? DeviceLink else { return } + result.append(deviceLink) + } + return result + } +} diff --git a/SignalUtilitiesKit/DeviceLinkingSession.swift b/SignalUtilitiesKit/DeviceLinkingSession.swift new file mode 100644 index 000000000..b3b0bd1e1 --- /dev/null +++ b/SignalUtilitiesKit/DeviceLinkingSession.swift @@ -0,0 +1,69 @@ +import Curve25519Kit +import PromiseKit + +@objc (LKDeviceLinkingSession) +public final class DeviceLinkingSession : NSObject { + private let delegate: DeviceLinkingSessionDelegate + @objc public var isListeningForLinkingRequests = false + @objc public var isProcessingLinkingRequest = false + @objc public var isListeningForLinkingAuthorization = false + + // MARK: Lifecycle + @objc public static var current: DeviceLinkingSession? + + private init(delegate: DeviceLinkingSessionDelegate) { + self.delegate = delegate + } + + // MARK: Public API + public static func startListeningForLinkingRequests(with delegate: DeviceLinkingSessionDelegate) -> DeviceLinkingSession { + let session = DeviceLinkingSession(delegate: delegate) + session.isListeningForLinkingRequests = true + DeviceLinkingSession.current = session + return session + } + + public static func startListeningForLinkingAuthorization(with delegate: DeviceLinkingSessionDelegate) -> DeviceLinkingSession { + let session = DeviceLinkingSession(delegate: delegate) + session.isListeningForLinkingAuthorization = true + DeviceLinkingSession.current = session + return session + } + + @objc public func processLinkingRequest(from slavePublicKey: String, to masterPublicKey: String, with slaveSignature: Data) { + guard isListeningForLinkingRequests, !isProcessingLinkingRequest, masterPublicKey == getUserHexEncodedPublicKey() else { return } + let master = DeviceLink.Device(publicKey: masterPublicKey) + let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature) + let deviceLink = DeviceLink(between: master, and: slave) + guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else { return } + isProcessingLinkingRequest = true + DispatchQueue.main.async { + self.delegate.requestUserAuthorization(for: deviceLink) + } + } + + @objc public func processLinkingAuthorization(from masterPublicKey: String, for slavePublicKey: String, masterSignature: Data, slaveSignature: Data) { + guard isListeningForLinkingAuthorization, slavePublicKey == getUserHexEncodedPublicKey() else { return } + let master = DeviceLink.Device(publicKey: masterPublicKey, signature: masterSignature) + let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature) + let deviceLink = DeviceLink(between: master, and: slave) + guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) && DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else { return } + DispatchQueue.main.async { + self.delegate.handleDeviceLinkAuthorized(deviceLink) + } + } + + public func stopListeningForLinkingRequests() { + DeviceLinkingSession.current = nil + isListeningForLinkingRequests = false + } + + public func stopListeningForLinkingAuthorization() { + DeviceLinkingSession.current = nil + isListeningForLinkingAuthorization = false + } + + public func markLinkingRequestAsProcessed() { + isProcessingLinkingRequest = false + } +} diff --git a/SignalUtilitiesKit/DeviceLinkingSessionDelegate.swift b/SignalUtilitiesKit/DeviceLinkingSessionDelegate.swift new file mode 100644 index 000000000..ab806251c --- /dev/null +++ b/SignalUtilitiesKit/DeviceLinkingSessionDelegate.swift @@ -0,0 +1,6 @@ + +public protocol DeviceLinkingSessionDelegate { + + func requestUserAuthorization(for deviceLink: DeviceLink) + func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) +} diff --git a/SignalUtilitiesKit/DeviceLinkingUtilities.swift b/SignalUtilitiesKit/DeviceLinkingUtilities.swift new file mode 100644 index 000000000..8893a1106 --- /dev/null +++ b/SignalUtilitiesKit/DeviceLinkingUtilities.swift @@ -0,0 +1,57 @@ + +@objc(LKDeviceLinkingUtilities) +public final class DeviceLinkingUtilities : NSObject { + private static var lastUnexpectedDeviceLinkRequestDate: Date? = nil + + private override init() { } + + @objc public static var shouldShowUnexpectedDeviceLinkRequestReceivedAlert: Bool { + let now = Date() + if let lastUnexpectedDeviceLinkRequestDate = lastUnexpectedDeviceLinkRequestDate { + if now.timeIntervalSince(lastUnexpectedDeviceLinkRequestDate) < 30 { return false } + } + lastUnexpectedDeviceLinkRequestDate = now + return true + } + + // When requesting a device link, the slave device signs the master device's public key. When authorizing + // a device link, the master device signs the slave device's public key. + + public static func getLinkingRequestMessage(for masterPublicKey: String) -> DeviceLinkMessage { + let slaveKeyPair = OWSIdentityManager.shared().identityKeyPair()! + let slavePublicKey = slaveKeyPair.hexEncodedPublicKey + var kind = UInt8(LKDeviceLinkMessageKind.request.rawValue) + let data = Data(hex: masterPublicKey) + Data(bytes: &kind, count: MemoryLayout.size(ofValue: kind)) + let slaveSignature = try! Ed25519.sign(data, with: slaveKeyPair) + let thread = TSContactThread.getOrCreateThread(contactId: masterPublicKey) + return DeviceLinkMessage(in: thread, masterPublicKey: masterPublicKey, slavePublicKey: slavePublicKey, masterSignature: nil, slaveSignature: slaveSignature) + } + + public static func getLinkingAuthorizationMessage(for deviceLink: DeviceLink) -> DeviceLinkMessage { + let masterKeyPair = OWSIdentityManager.shared().identityKeyPair()! + let masterPublicKey = masterKeyPair.hexEncodedPublicKey + let slavePublicKey = deviceLink.slave.publicKey + var kind = UInt8(LKDeviceLinkMessageKind.authorization.rawValue) + let data = Data(hex: slavePublicKey) + Data(bytes: &kind, count: MemoryLayout.size(ofValue: kind)) + let masterSignature = try! Ed25519.sign(data, with: masterKeyPair) + let slaveSignature = deviceLink.slave.signature! + let thread = TSContactThread.getOrCreateThread(contactId: slavePublicKey) + return DeviceLinkMessage(in: thread, masterPublicKey: masterPublicKey, slavePublicKey: slavePublicKey, masterSignature: masterSignature, slaveSignature: slaveSignature) + } + + public static func hasValidSlaveSignature(_ deviceLink: DeviceLink) -> Bool { + guard let slaveSignature = deviceLink.slave.signature else { return false } + let slavePublicKey = Data(hex: deviceLink.slave.publicKey.removing05PrefixIfNeeded()) + var kind = UInt8(LKDeviceLinkMessageKind.request.rawValue) + let data = Data(hex: deviceLink.master.publicKey) + Data(bytes: &kind, count: MemoryLayout.size(ofValue: kind)) + return (try? Ed25519.verifySignature(slaveSignature, publicKey: slavePublicKey, data: data)) ?? false + } + + public static func hasValidMasterSignature(_ deviceLink: DeviceLink) -> Bool { + guard let masterSignature = deviceLink.master.signature else { return false } + let masterPublicKey = Data(hex: deviceLink.master.publicKey.removing05PrefixIfNeeded()) + var kind = UInt8(LKDeviceLinkMessageKind.authorization.rawValue) + let data = Data(hex: deviceLink.slave.publicKey) + Data(bytes: &kind, count: MemoryLayout.size(ofValue: kind)) + return (try? Ed25519.verifySignature(masterSignature, publicKey: masterPublicKey, data: data)) ?? false + } +} diff --git a/SignalUtilitiesKit/DeviceNames.swift b/SignalUtilitiesKit/DeviceNames.swift new file mode 100644 index 000000000..00505ea14 --- /dev/null +++ b/SignalUtilitiesKit/DeviceNames.swift @@ -0,0 +1,217 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import Curve25519Kit + +@objc +public enum DeviceNameError: Int, Error { + case assertionFailure + case invalidInput +} + +@objc +public class DeviceNames: NSObject { + // Never instantiate this class. + private override init() {} + + private static let syntheticIVLength: UInt = 16 + + @objc + public class func encryptDeviceName(plaintext: String, + identityKeyPair: ECKeyPair) throws -> Data { + + guard let plaintextData = plaintext.data(using: .utf8) else { + owsFailDebug("Could not convert text to UTF-8.") + throw DeviceNameError.invalidInput + } + + let ephemeralKeyPair = Curve25519.generateKeyPair()! + + // master_secret = ECDH(ephemeral_private, identity_public). + let masterSecret: Data + do { + masterSecret = try Curve25519.generateSharedSecret(fromPublicKey: identityKeyPair.publicKey(), andKeyPair: ephemeralKeyPair) + } catch { + Logger.error("Could not generate shared secret: \(error)") + throw error + } + + // synthetic_iv = HmacSHA256(key=HmacSHA256(key=master_secret, input=“auth”), input=plaintext)[0:16] + let syntheticIV = try computeSyntheticIV(masterSecret: masterSecret, + plaintextData: plaintextData) + + // cipher_key = HmacSHA256(key=HmacSHA256(key=master_secret, “cipher”), input=synthetic_iv) + let cipherKey = try computeCipherKey(masterSecret: masterSecret, syntheticIV: syntheticIV) + + // cipher_text = AES-CTR(key=cipher_key, input=plaintext, counter=0) + // + // An all-zeros IV corresponds to an AES CTR counter of zero. + let ciphertextIV = Data(count: Int(kAES256CTR_IVLength)) + guard let ciphertextKey = OWSAES256Key(data: cipherKey) else { + owsFailDebug("Invalid cipher key.") + throw DeviceNameError.assertionFailure + } + guard let ciphertext: AES256CTREncryptionResult = Cryptography.encryptAESCTR(plaintextData: plaintextData, initializationVector: ciphertextIV, key: ciphertextKey) else { + owsFailDebug("Could not encrypt cipher text.") + throw DeviceNameError.assertionFailure + } + + guard let keyData = (ephemeralKeyPair.publicKey() as NSData).prependKeyType() else { + owsFailDebug("Could not prepend key type.") + throw DeviceNameError.assertionFailure + } + let protoBuilder = SignalIOSProtoDeviceName.builder(ephemeralPublic: keyData as Data, + syntheticIv: syntheticIV, + ciphertext: ciphertext.ciphertext) + let protoData = try protoBuilder.buildSerializedData() + + // NOTE: This uses Data's foundation method rather than the NSData's SSK method. + let protoDataBase64 = protoData.base64EncodedData() + + return protoDataBase64 + } + + private class func computeSyntheticIV(masterSecret: Data, + plaintextData: Data) throws -> Data { + // synthetic_iv = HmacSHA256(key=HmacSHA256(key=master_secret, input=“auth”), input=plaintext)[0:16] + guard let syntheticIVInput = "auth".data(using: .utf8) else { + owsFailDebug("Could not convert text to UTF-8.") + throw DeviceNameError.assertionFailure + } + guard let syntheticIVKey = Cryptography.computeSHA256HMAC(syntheticIVInput, withHMACKey: masterSecret) else { + owsFailDebug("Could not compute synthetic IV key.") + throw DeviceNameError.assertionFailure + } + guard let syntheticIV = Cryptography.truncatedSHA256HMAC(plaintextData, withHMACKey: syntheticIVKey, truncation: syntheticIVLength) else { + owsFailDebug("Could not compute synthetic IV.") + throw DeviceNameError.assertionFailure + } + return syntheticIV + } + + private class func computeCipherKey(masterSecret: Data, + syntheticIV: Data) throws -> Data { + // cipher_key = HmacSHA256(key=HmacSHA256(key=master_secret, “cipher”), input=synthetic_iv) + guard let cipherKeyInput = "cipher".data(using: .utf8) else { + owsFailDebug("Could not convert text to UTF-8.") + throw DeviceNameError.assertionFailure + } + guard let cipherKeyKey = Cryptography.computeSHA256HMAC(cipherKeyInput, withHMACKey: masterSecret) else { + owsFailDebug("Could not compute cipher key key.") + throw DeviceNameError.assertionFailure + } + guard let cipherKey = Cryptography.computeSHA256HMAC(syntheticIV, withHMACKey: cipherKeyKey) else { + owsFailDebug("Could not compute cipher key.") + throw DeviceNameError.assertionFailure + } + return cipherKey + } + + @objc + public class func decryptDeviceName(base64String: String, + identityKeyPair: ECKeyPair) throws -> String { + + guard let protoData = Data(base64Encoded: base64String) else { + // Not necessarily an error; might be a legacy device name. + throw DeviceNameError.invalidInput + } + + return try decryptDeviceName(protoData: protoData, + identityKeyPair: identityKeyPair) + } + + @objc + public class func decryptDeviceName(base64Data: Data, + identityKeyPair: ECKeyPair) throws -> String { + + guard let protoData = Data(base64Encoded: base64Data) else { + // Not necessarily an error; might be a legacy device name. + throw DeviceNameError.invalidInput + } + + return try decryptDeviceName(protoData: protoData, + identityKeyPair: identityKeyPair) + } + + @objc + public class func decryptDeviceName(protoData: Data, + identityKeyPair: ECKeyPair) throws -> String { + + let proto: SignalIOSProtoDeviceName + do { + proto = try SignalIOSProtoDeviceName.parseData(protoData) + } catch { + // Not necessarily an error; might be a legacy device name. + Logger.error("failed to parse proto") + throw DeviceNameError.invalidInput + } + + let ephemeralPublicData = proto.ephemeralPublic + let receivedSyntheticIV = proto.syntheticIv + let ciphertext = proto.ciphertext + + let ephemeralPublic: Data + do { + ephemeralPublic = try (ephemeralPublicData as NSData).removeKeyType() as Data + } catch { + owsFailDebug("failed to remove key type") + throw DeviceNameError.invalidInput + } + + guard ephemeralPublic.count > 0 else { + owsFailDebug("Invalid ephemeral public.") + throw DeviceNameError.assertionFailure + } + guard receivedSyntheticIV.count == syntheticIVLength else { + owsFailDebug("Invalid synthetic IV.") + throw DeviceNameError.assertionFailure + } + guard ciphertext.count > 0 else { + owsFailDebug("Invalid cipher text.") + throw DeviceNameError.assertionFailure + } + + // master_secret = ECDH(identity_private, ephemeral_public) + let masterSecret: Data + do { + masterSecret = try Curve25519.generateSharedSecret(fromPublicKey: ephemeralPublic, andKeyPair: identityKeyPair) + } catch { + Logger.error("Could not generate shared secret: \(error)") + throw error + } + + // cipher_key = HmacSHA256(key=HmacSHA256(key=master_secret, input=“cipher”), input=synthetic_iv) + let cipherKey = try computeCipherKey(masterSecret: masterSecret, syntheticIV: receivedSyntheticIV) + + // plaintext = AES-CTR(key=cipher_key, input=ciphertext, counter=0) + // + // An all-zeros IV corresponds to an AES CTR counter of zero. + let ciphertextIV = Data(count: Int(kAES256CTR_IVLength)) + guard let ciphertextKey = OWSAES256Key(data: cipherKey) else { + owsFailDebug("Invalid cipher key.") + throw DeviceNameError.assertionFailure + } + guard let plaintextData = Cryptography.decryptAESCTR(cipherText: ciphertext, initializationVector: ciphertextIV, key: ciphertextKey) else { + owsFailDebug("Could not decrypt cipher text.") + throw DeviceNameError.assertionFailure + } + + // Verify the synthetic IV was correct. + // constant_time_compare(HmacSHA256(key=HmacSHA256(key=master_secret, input=”auth”), input=plaintext)[0:16], synthetic_iv) == true + let computedSyntheticIV = try computeSyntheticIV(masterSecret: masterSecret, + plaintextData: plaintextData) + guard receivedSyntheticIV.ows_constantTimeIsEqual(to: computedSyntheticIV) else { + owsFailDebug("Synthetic IV did not match.") + throw DeviceNameError.assertionFailure + } + + guard let plaintext = String(bytes: plaintextData, encoding: .utf8) else { + owsFailDebug("Invalid plaintext.") + throw DeviceNameError.invalidInput + } + + return plaintext + } +} diff --git a/SignalUtilitiesKit/Dictionary+Description.swift b/SignalUtilitiesKit/Dictionary+Description.swift new file mode 100644 index 000000000..c7aedbfe6 --- /dev/null +++ b/SignalUtilitiesKit/Dictionary+Description.swift @@ -0,0 +1,13 @@ + +public extension Dictionary { + + public var prettifiedDescription: String { + return "[ " + map { key, value in + let keyDescription = String(describing: key) + let valueDescription = String(describing: value) + let maxLength = 20 + let truncatedValueDescription = valueDescription.count > maxLength ? valueDescription.prefix(maxLength) + "..." : valueDescription + return keyDescription + " : " + truncatedValueDescription + }.joined(separator: ", ") + " ]" + } +} diff --git a/SignalUtilitiesKit/DisplayNameUtilities.swift b/SignalUtilitiesKit/DisplayNameUtilities.swift new file mode 100644 index 000000000..f4866b5be --- /dev/null +++ b/SignalUtilitiesKit/DisplayNameUtilities.swift @@ -0,0 +1,68 @@ + +@objc(LKUserDisplayNameUtilities) +public final class UserDisplayNameUtilities : NSObject { + + override private init() { } + + private static var userPublicKey: String { + return getUserHexEncodedPublicKey() + } + + private static var userDisplayName: String? { + return SSKEnvironment.shared.profileManager.localProfileName() + } + + // MARK: Sessions + @objc(getPrivateChatDisplayNameAvoidWriteTransaction:) + public static func getPrivateChatDisplayNameAvoidingWriteTransaction(for publicKey: String) -> String? { + if publicKey == userPublicKey { + return userDisplayName + } else { + return SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: publicKey, avoidingWriteTransaction: true) + } + } + + @objc public static func getPrivateChatDisplayName(for publicKey: String) -> String? { + if publicKey == userPublicKey { + return userDisplayName + } else { + return SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: publicKey) + } + } + + // MARK: Open Groups + @objc public static func getPublicChatDisplayName(for publicKey: String, in channel: UInt64, on server: String) -> String? { + var result: String? + OWSPrimaryStorage.shared().dbReadConnection.read { transaction in + result = getPublicChatDisplayName(for: publicKey, in: channel, on: server, using: transaction) + } + return result + } + + @objc public static func getPublicChatDisplayName(for publicKey: String, in channel: UInt64, on server: String, using transaction: YapDatabaseReadTransaction) -> String? { + if publicKey == userPublicKey { + return userDisplayName + } else { + let collection = "\(server).\(channel)" + return transaction.object(forKey: publicKey, inCollection: collection) as! String? + } + } +} + +@objc(LKGroupDisplayNameUtilities) +public final class GroupDisplayNameUtilities : NSObject { + + override private init() { } + + // MARK: Closed Groups + @objc public static func getDefaultDisplayName(for group: TSGroupThread) -> String { + let members = group.groupModel.groupMemberIds + let displayNames = members.map { hexEncodedPublicKey -> String in + guard let displayName = UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) else { return hexEncodedPublicKey } + let regex = try! NSRegularExpression(pattern: ".* \\(\\.\\.\\.[0-9a-fA-F]*\\)") + guard regex.hasMatch(input: displayName) else { return displayName } + return String(displayName[displayName.startIndex..<(displayName.index(displayName.endIndex, offsetBy: -14))]) + }.sorted() + return displayNames.joined(separator: ", ") + } +} diff --git a/SignalUtilitiesKit/DisplayNameUtilities2.swift b/SignalUtilitiesKit/DisplayNameUtilities2.swift new file mode 100644 index 000000000..728fc44a8 --- /dev/null +++ b/SignalUtilitiesKit/DisplayNameUtilities2.swift @@ -0,0 +1,28 @@ + +@objc(LKDisplayNameUtilities2) +public final class DisplayNameUtilities2 : NSObject { + + private override init() { } + + @objc(getDisplayNameForPublicKey:threadID:transaction:) + public static func getDisplayName(for publicKey: String, inThreadWithID threadID: String, using transaction: YapDatabaseReadWriteTransaction) -> String { + // Case 1: The public key belongs to the user themselves + if publicKey == getUserHexEncodedPublicKey() { return SSKEnvironment.shared.profileManager.localProfileName() ?? publicKey } + // Case 2: The given thread is an open group + var openGroup: OpenGroup? = nil + Storage.read { transaction in + openGroup = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction) + } + if let openGroup = openGroup { + var displayName: String? = nil + Storage.read { transaction in + displayName = transaction.object(forKey: publicKey, inCollection: openGroup.id) as! String? + } + if let displayName = displayName { return displayName } + } + // Case 3: The given thread is a closed group or a one-to-one conversation + // FIXME: The line below opens a write transaction under certain circumstances. We should move away from this and towards passing + // a write transaction into this function. + return SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: publicKey) ?? publicKey + } +} diff --git a/SignalUtilitiesKit/ECKeyPair+Hexadecimal.swift b/SignalUtilitiesKit/ECKeyPair+Hexadecimal.swift new file mode 100644 index 000000000..ae9c07b85 --- /dev/null +++ b/SignalUtilitiesKit/ECKeyPair+Hexadecimal.swift @@ -0,0 +1,22 @@ + +public extension ECKeyPair { + + @objc public var hexEncodedPrivateKey: String { + return privateKey().map { String(format: "%02hhx", $0) }.joined() + } + + @objc public var hexEncodedPublicKey: String { + // Prefixing with "05" is necessary for what seems to be a sort of Signal public key versioning system + return "05" + publicKey().map { String(format: "%02hhx", $0) }.joined() + } + + @objc public static func isValidHexEncodedPublicKey(candidate: String) -> Bool { + // Check that it's a valid hexadecimal encoding + let allowedCharacters = CharacterSet(charactersIn: "0123456789ABCDEF") + guard candidate.uppercased().unicodeScalars.allSatisfy({ allowedCharacters.contains($0) }) else { return false } + // Check that it has length 66 and a leading "05" + guard candidate.count == 66 && candidate.hasPrefix("05") else { return false } + // It appears to be a valid public key + return true + } +} diff --git a/SignalUtilitiesKit/EncryptionUtilities.swift b/SignalUtilitiesKit/EncryptionUtilities.swift new file mode 100644 index 000000000..3820c8d55 --- /dev/null +++ b/SignalUtilitiesKit/EncryptionUtilities.swift @@ -0,0 +1,38 @@ +import CryptoSwift + +internal typealias EncryptionResult = (ciphertext: Data, symmetricKey: Data, ephemeralPublicKey: Data) + +enum EncryptionUtilities { + internal static let gcmTagSize: UInt = 16 + internal static let ivSize: UInt = 12 + + /// - Note: Sync. Don't call from the main thread. + internal static func encrypt(_ plaintext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data { + if Thread.isMainThread { + #if DEBUG + preconditionFailure("It's illegal to call encrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.") + #endif + } + let iv = Data.getSecureRandomData(ofSize: ivSize)! + let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined) + let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding) + let ciphertext = try aes.encrypt(plaintext.bytes) + return iv + Data(bytes: ciphertext) + } + + /// - Note: Sync. Don't call from the main thread. + internal static func encrypt(_ plaintext: Data, using hexEncodedX25519PublicKey: String) throws -> EncryptionResult { + if Thread.isMainThread { + #if DEBUG + preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") + #endif + } + let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey) + let ephemeralKeyPair = Curve25519.generateKeyPair()! + let ephemeralSharedSecret = Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, andKeyPair: ephemeralKeyPair)! + let salt = "LOKI" + let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes) + let ciphertext = try encrypt(plaintext, usingAESGCMWithSymmetricKey: Data(bytes: symmetricKey)) + return (ciphertext, Data(bytes: symmetricKey), ephemeralKeyPair.publicKey()) + } +} diff --git a/SignalUtilitiesKit/FeatureFlags.swift b/SignalUtilitiesKit/FeatureFlags.swift new file mode 100644 index 000000000..0d78affd1 --- /dev/null +++ b/SignalUtilitiesKit/FeatureFlags.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +/// By centralizing feature flags here and documenting their rollout plan, it's easier to review +/// which feature flags are in play. +@objc(SSKFeatureFlags) +public class FeatureFlags: NSObject { + + @objc + public static var conversationSearch: Bool { + return false + } + + /// iOS has long supported sending oversized text as a sidecar attachment. The other clients + /// simply displayed it as a text attachment. As part of the new cross-client long-text feature, + /// we want to be able to display long text with attachments as well. Existing iOS clients + /// won't properly display this, so we'll need to wait a while for rollout. + /// The stakes aren't __too__ high, because legacy clients won't lose data - they just won't + /// see the media attached to a long text message until they update their client. + @objc + public static var sendingMediaWithOversizeText: Bool { + return false + } + + @objc + public static var useCustomPhotoCapture: Bool { + return true + } +} diff --git a/SignalUtilitiesKit/FileServerAPI+Deprecated.swift b/SignalUtilitiesKit/FileServerAPI+Deprecated.swift new file mode 100644 index 000000000..1fd608a6f --- /dev/null +++ b/SignalUtilitiesKit/FileServerAPI+Deprecated.swift @@ -0,0 +1,148 @@ +import PromiseKit + +public extension FileServerAPI { + + /// Gets the device links associated with the given hex encoded public key from the + /// server and stores and returns the valid ones. + /// + /// - Note: Deprecated. + public static func getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> Promise> { + return getDeviceLinks(associatedWith: [ hexEncodedPublicKey ]) + } + + /// Gets the device links associated with the given hex encoded public keys from the + /// server and stores and returns the valid ones. + /// + /// - Note: Deprecated. + public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set) -> Promise> { + return Promise.value([]) + /* + let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]" + print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).") + return getAuthToken(for: server).then2 { token -> Promise> in + let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1" + let url = URL(string: "\(server)/users?\(queryParameters)")! + let request = TSRequest(url: url) + return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { rawResponse -> Set in + guard let data = rawResponse["data"] as? [JSON] else { + print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).") + throw DotNetAPIError.parsingFailed + } + return Set(data.flatMap { data -> [DeviceLink] in + guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] } + guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }), + let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON], + let hexEncodedPublicKey = data["username"] as? String else { + print("[Loki] Couldn't parse device links from: \(rawResponse).") + return [] + } + return rawDeviceLinks.compactMap { rawDeviceLink in + guard let masterPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slavePublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String, + let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else { + print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).") + return nil + } + let masterSignature: Data? + if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String { + masterSignature = Data(base64Encoded: base64EncodedMasterSignature) + } else { + masterSignature = nil + } + let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature) + let master = DeviceLink.Device(publicKey: masterPublicKey, signature: masterSignature) + let slave = DeviceLink.Device(publicKey: slavePublicKey, signature: slaveSignature) + let deviceLink = DeviceLink(between: master, and: slave) + if let masterSignature = masterSignature { + guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else { + print("[Loki] Received a device link with an invalid master signature.") + return nil + } + } + guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else { + print("[Loki] Received a device link with an invalid slave signature.") + return nil + } + return deviceLink + } + }) + }.map2 { deviceLinks in + storage.setDeviceLinks(deviceLinks) + return deviceLinks + } + }.handlingInvalidAuthTokenIfNeeded(for: server) + */ + } + + /// - Note: Deprecated. + public static func setDeviceLinks(_ deviceLinks: Set) -> Promise { + return Promise.value(()) + /* + print("[Loki] Updating device links.") + return getAuthToken(for: server).then2 { token -> Promise in + let isMaster = deviceLinks.contains { $0.master.publicKey == getUserHexEncodedPublicKey() } + let deviceLinksAsJSON = deviceLinks.map { $0.toJSON() } + let value = !deviceLinksAsJSON.isEmpty ? [ "isPrimary" : isMaster ? 1 : 0, "authorisations" : deviceLinksAsJSON ] : nil + let annotation: JSON = [ "type" : deviceLinkType, "value" : value ] + let parameters: JSON = [ "annotations" : [ annotation ] ] + let url = URL(string: "\(server)/users/me")! + let request = TSRequest(url: url, method: "PATCH", parameters: parameters) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey).map2 { _ in } + }.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in + print("[Loki] Couldn't update device links due to error: \(error).") + throw error + } + } + */ + } + + /// Adds the given device link to the user's device mapping on the server. + /// + /// - Note: Deprecated. + public static func addDeviceLink(_ deviceLink: DeviceLink) -> Promise { + return Promise.value(()) + /* + var deviceLinks: Set = [] + storage.dbReadConnection.read { transaction in + deviceLinks = storage.getDeviceLinks(for: getUserHexEncodedPublicKey(), in: transaction) + } + deviceLinks.insert(deviceLink) + return setDeviceLinks(deviceLinks).map2 { _ in + storage.addDeviceLink(deviceLink) + } + */ + } + + /// Removes the given device link from the user's device mapping on the server. + /// + /// - Note: Deprecated. + public static func removeDeviceLink(_ deviceLink: DeviceLink) -> Promise { + return Promise.value(()) + /* + var deviceLinks: Set = [] + storage.dbReadConnection.read { transaction in + deviceLinks = storage.getDeviceLinks(for: getUserHexEncodedPublicKey(), in: transaction) + } + deviceLinks.remove(deviceLink) + return setDeviceLinks(deviceLinks).map2 { _ in + storage.removeDeviceLink(deviceLink) + } + */ + } +} + +@objc public extension FileServerAPI { + + /// - Note: Deprecated. + @objc(getDeviceLinksAssociatedWithHexEncodedPublicKey:) + public static func objc_getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> AnyPromise { + return AnyPromise.from(getDeviceLinks(associatedWith: hexEncodedPublicKey)) + } + + /// - Note: Deprecated. + @objc(getDeviceLinksAssociatedWithHexEncodedPublicKeys:) + public static func objc_getDeviceLinks(associatedWith hexEncodedPublicKeys: Set) -> AnyPromise { + return AnyPromise.from(getDeviceLinks(associatedWith: hexEncodedPublicKeys)) + } +} diff --git a/SignalUtilitiesKit/Fingerprint.pb.swift b/SignalUtilitiesKit/Fingerprint.pb.swift new file mode 100644 index 000000000..7ebca2ef2 --- /dev/null +++ b/SignalUtilitiesKit/Fingerprint.pb.swift @@ -0,0 +1,164 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Fingerprint.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +/// iOS - since we use a modern proto-compiler, we must specify +/// the legacy proto format. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct FingerprintProtos_LogicalFingerprint { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var identityData: Data { + get {return _identityData ?? SwiftProtobuf.Internal.emptyData} + set {_identityData = newValue} + } + /// Returns true if `identityData` has been explicitly set. + var hasIdentityData: Bool {return self._identityData != nil} + /// Clears the value of `identityData`. Subsequent reads from it will return its default value. + mutating func clearIdentityData() {self._identityData = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _identityData: Data? = nil +} + +struct FingerprintProtos_LogicalFingerprints { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var version: UInt32 { + get {return _version ?? 0} + set {_version = newValue} + } + /// Returns true if `version` has been explicitly set. + var hasVersion: Bool {return self._version != nil} + /// Clears the value of `version`. Subsequent reads from it will return its default value. + mutating func clearVersion() {self._version = nil} + + /// @required + var localFingerprint: FingerprintProtos_LogicalFingerprint { + get {return _localFingerprint ?? FingerprintProtos_LogicalFingerprint()} + set {_localFingerprint = newValue} + } + /// Returns true if `localFingerprint` has been explicitly set. + var hasLocalFingerprint: Bool {return self._localFingerprint != nil} + /// Clears the value of `localFingerprint`. Subsequent reads from it will return its default value. + mutating func clearLocalFingerprint() {self._localFingerprint = nil} + + /// @required + var remoteFingerprint: FingerprintProtos_LogicalFingerprint { + get {return _remoteFingerprint ?? FingerprintProtos_LogicalFingerprint()} + set {_remoteFingerprint = newValue} + } + /// Returns true if `remoteFingerprint` has been explicitly set. + var hasRemoteFingerprint: Bool {return self._remoteFingerprint != nil} + /// Clears the value of `remoteFingerprint`. Subsequent reads from it will return its default value. + mutating func clearRemoteFingerprint() {self._remoteFingerprint = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _version: UInt32? = nil + fileprivate var _localFingerprint: FingerprintProtos_LogicalFingerprint? = nil + fileprivate var _remoteFingerprint: FingerprintProtos_LogicalFingerprint? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "FingerprintProtos" + +extension FingerprintProtos_LogicalFingerprint: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LogicalFingerprint" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "identityData"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._identityData) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._identityData { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: FingerprintProtos_LogicalFingerprint, rhs: FingerprintProtos_LogicalFingerprint) -> Bool { + if lhs._identityData != rhs._identityData {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension FingerprintProtos_LogicalFingerprints: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LogicalFingerprints" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "version"), + 2: .same(proto: "localFingerprint"), + 3: .same(proto: "remoteFingerprint"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt32Field(value: &self._version) + case 2: try decoder.decodeSingularMessageField(value: &self._localFingerprint) + case 3: try decoder.decodeSingularMessageField(value: &self._remoteFingerprint) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._version { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } + if let v = self._localFingerprint { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } + if let v = self._remoteFingerprint { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: FingerprintProtos_LogicalFingerprints, rhs: FingerprintProtos_LogicalFingerprints) -> Bool { + if lhs._version != rhs._version {return false} + if lhs._localFingerprint != rhs._localFingerprint {return false} + if lhs._remoteFingerprint != rhs._remoteFingerprint {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/SignalUtilitiesKit/FingerprintProto.swift b/SignalUtilitiesKit/FingerprintProto.swift new file mode 100644 index 000000000..b841ce062 --- /dev/null +++ b/SignalUtilitiesKit/FingerprintProto.swift @@ -0,0 +1,235 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +// WARNING: This code is generated. Only edit within the markers. + +public enum FingerprintProtoError: Error { + case invalidProtobuf(description: String) +} + +// MARK: - FingerprintProtoLogicalFingerprint + +@objc public class FingerprintProtoLogicalFingerprint: NSObject { + + // MARK: - FingerprintProtoLogicalFingerprintBuilder + + @objc public class func builder(identityData: Data) -> FingerprintProtoLogicalFingerprintBuilder { + return FingerprintProtoLogicalFingerprintBuilder(identityData: identityData) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> FingerprintProtoLogicalFingerprintBuilder { + let builder = FingerprintProtoLogicalFingerprintBuilder(identityData: identityData) + return builder + } + + @objc public class FingerprintProtoLogicalFingerprintBuilder: NSObject { + + private var proto = FingerprintProtos_LogicalFingerprint() + + @objc fileprivate override init() {} + + @objc fileprivate init(identityData: Data) { + super.init() + + setIdentityData(identityData) + } + + @objc public func setIdentityData(_ valueParam: Data) { + proto.identityData = valueParam + } + + @objc public func build() throws -> FingerprintProtoLogicalFingerprint { + return try FingerprintProtoLogicalFingerprint.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try FingerprintProtoLogicalFingerprint.parseProto(proto).serializedData() + } + } + + fileprivate let proto: FingerprintProtos_LogicalFingerprint + + @objc public let identityData: Data + + private init(proto: FingerprintProtos_LogicalFingerprint, + identityData: Data) { + self.proto = proto + self.identityData = identityData + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> FingerprintProtoLogicalFingerprint { + let proto = try FingerprintProtos_LogicalFingerprint(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: FingerprintProtos_LogicalFingerprint) throws -> FingerprintProtoLogicalFingerprint { + guard proto.hasIdentityData else { + throw FingerprintProtoError.invalidProtobuf(description: "\(logTag) missing required field: identityData") + } + let identityData = proto.identityData + + // MARK: - Begin Validation Logic for FingerprintProtoLogicalFingerprint - + + // MARK: - End Validation Logic for FingerprintProtoLogicalFingerprint - + + let result = FingerprintProtoLogicalFingerprint(proto: proto, + identityData: identityData) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension FingerprintProtoLogicalFingerprint { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension FingerprintProtoLogicalFingerprint.FingerprintProtoLogicalFingerprintBuilder { + @objc public func buildIgnoringErrors() -> FingerprintProtoLogicalFingerprint? { + return try! self.build() + } +} + +#endif + +// MARK: - FingerprintProtoLogicalFingerprints + +@objc public class FingerprintProtoLogicalFingerprints: NSObject { + + // MARK: - FingerprintProtoLogicalFingerprintsBuilder + + @objc public class func builder(version: UInt32, localFingerprint: FingerprintProtoLogicalFingerprint, remoteFingerprint: FingerprintProtoLogicalFingerprint) -> FingerprintProtoLogicalFingerprintsBuilder { + return FingerprintProtoLogicalFingerprintsBuilder(version: version, localFingerprint: localFingerprint, remoteFingerprint: remoteFingerprint) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> FingerprintProtoLogicalFingerprintsBuilder { + let builder = FingerprintProtoLogicalFingerprintsBuilder(version: version, localFingerprint: localFingerprint, remoteFingerprint: remoteFingerprint) + return builder + } + + @objc public class FingerprintProtoLogicalFingerprintsBuilder: NSObject { + + private var proto = FingerprintProtos_LogicalFingerprints() + + @objc fileprivate override init() {} + + @objc fileprivate init(version: UInt32, localFingerprint: FingerprintProtoLogicalFingerprint, remoteFingerprint: FingerprintProtoLogicalFingerprint) { + super.init() + + setVersion(version) + setLocalFingerprint(localFingerprint) + setRemoteFingerprint(remoteFingerprint) + } + + @objc public func setVersion(_ valueParam: UInt32) { + proto.version = valueParam + } + + @objc public func setLocalFingerprint(_ valueParam: FingerprintProtoLogicalFingerprint) { + proto.localFingerprint = valueParam.proto + } + + @objc public func setRemoteFingerprint(_ valueParam: FingerprintProtoLogicalFingerprint) { + proto.remoteFingerprint = valueParam.proto + } + + @objc public func build() throws -> FingerprintProtoLogicalFingerprints { + return try FingerprintProtoLogicalFingerprints.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try FingerprintProtoLogicalFingerprints.parseProto(proto).serializedData() + } + } + + fileprivate let proto: FingerprintProtos_LogicalFingerprints + + @objc public let version: UInt32 + + @objc public let localFingerprint: FingerprintProtoLogicalFingerprint + + @objc public let remoteFingerprint: FingerprintProtoLogicalFingerprint + + private init(proto: FingerprintProtos_LogicalFingerprints, + version: UInt32, + localFingerprint: FingerprintProtoLogicalFingerprint, + remoteFingerprint: FingerprintProtoLogicalFingerprint) { + self.proto = proto + self.version = version + self.localFingerprint = localFingerprint + self.remoteFingerprint = remoteFingerprint + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> FingerprintProtoLogicalFingerprints { + let proto = try FingerprintProtos_LogicalFingerprints(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: FingerprintProtos_LogicalFingerprints) throws -> FingerprintProtoLogicalFingerprints { + guard proto.hasVersion else { + throw FingerprintProtoError.invalidProtobuf(description: "\(logTag) missing required field: version") + } + let version = proto.version + + guard proto.hasLocalFingerprint else { + throw FingerprintProtoError.invalidProtobuf(description: "\(logTag) missing required field: localFingerprint") + } + let localFingerprint = try FingerprintProtoLogicalFingerprint.parseProto(proto.localFingerprint) + + guard proto.hasRemoteFingerprint else { + throw FingerprintProtoError.invalidProtobuf(description: "\(logTag) missing required field: remoteFingerprint") + } + let remoteFingerprint = try FingerprintProtoLogicalFingerprint.parseProto(proto.remoteFingerprint) + + // MARK: - Begin Validation Logic for FingerprintProtoLogicalFingerprints - + + // MARK: - End Validation Logic for FingerprintProtoLogicalFingerprints - + + let result = FingerprintProtoLogicalFingerprints(proto: proto, + version: version, + localFingerprint: localFingerprint, + remoteFingerprint: remoteFingerprint) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension FingerprintProtoLogicalFingerprints { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension FingerprintProtoLogicalFingerprints.FingerprintProtoLogicalFingerprintsBuilder { + @objc public func buildIgnoringErrors() -> FingerprintProtoLogicalFingerprints? { + return try! self.build() + } +} + +#endif diff --git a/SignalUtilitiesKit/FullTextSearchFinder.swift b/SignalUtilitiesKit/FullTextSearchFinder.swift new file mode 100644 index 000000000..5ead04baf --- /dev/null +++ b/SignalUtilitiesKit/FullTextSearchFinder.swift @@ -0,0 +1,274 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import libPhoneNumber_iOS + +// Create a searchable index for objects of type T +public class SearchIndexer { + + private let indexBlock: (T, YapDatabaseReadTransaction) -> String + + public init(indexBlock: @escaping (T, YapDatabaseReadTransaction) -> String) { + self.indexBlock = indexBlock + } + + public func index(_ item: T, transaction: YapDatabaseReadTransaction) -> String { + return normalize(indexingText: indexBlock(item, transaction)) + } + + private func normalize(indexingText: String) -> String { + return FullTextSearchFinder.normalize(text: indexingText) + } +} + +@objc +public class FullTextSearchFinder: NSObject { + + // MARK: - Dependencies + + private static var tsAccountManager: TSAccountManager { + return TSAccountManager.sharedInstance() + } + + // MARK: - Querying + + // We want to match by prefix for "search as you type" functionality. + // SQLite does not support suffix or contains matches. + public class func query(searchText: String) -> String { + // 1. Normalize the search text. + // + // TODO: We could arguably convert to lowercase since the search + // is case-insensitive. + let normalizedSearchText = FullTextSearchFinder.normalize(text: searchText) + + // 2. Split the non-numeric text into query terms (or tokens). + let nonNumericText = String(String.UnicodeScalarView(normalizedSearchText.unicodeScalars.lazy.map { + if CharacterSet.decimalDigits.contains($0) { + return " " + } else { + return $0 + } + })) + var queryTerms = nonNumericText.split(separator: " ") + + // 3. Add an additional numeric-only query term. + let digitsOnlyScalars = normalizedSearchText.unicodeScalars.lazy.filter { + CharacterSet.decimalDigits.contains($0) + } + let digitsOnly: Substring = Substring(String(String.UnicodeScalarView(digitsOnlyScalars))) + queryTerms.append(digitsOnly) + + // 4. De-duplicate and sort query terms. + // Duplicate terms are redundant. + // Sorting terms makes the output of this method deterministic and easier to test, + // and the order won't affect the search results. + queryTerms = Array(Set(queryTerms)).sorted() + + // 5. Filter the query terms. + let filteredQueryTerms = queryTerms.filter { + // Ignore empty terms. + $0.count > 0 + }.map { + // Allow partial match of each term. + // + // Note that we use double-quotes to enclose each search term. + // Quoted search terms can include a few more characters than + // "bareword" (non-quoted) search terms. This shouldn't matter, + // since we're filtering all of the affected characters, but + // quoting protects us from any bugs in that logic. + "\"\($0)\"*" + } + + // 6. Join terms into query string. + let query = filteredQueryTerms.joined(separator: " ") + return query + } + + public func enumerateObjects(searchText: String, transaction: YapDatabaseReadTransaction, block: @escaping (Any, String) -> Void) { + guard let ext: YapDatabaseFullTextSearchTransaction = ext(transaction: transaction) else { + owsFailDebug("ext was unexpectedly nil") + return + } + + let query = FullTextSearchFinder.query(searchText: searchText) + + Logger.verbose("query: \(query)") + + let maxSearchResults = 500 + var searchResultCount = 0 + let snippetOptions = YapDatabaseFullTextSearchSnippetOptions() + snippetOptions.startMatchText = "" + snippetOptions.endMatchText = "" + ext.enumerateKeysAndObjects(matching: query, with: snippetOptions) { (snippet: String, _: String, _: String, object: Any, stop: UnsafeMutablePointer) in + guard searchResultCount < maxSearchResults else { + stop.pointee = true + return + } + searchResultCount += 1 + + block(object, snippet) + } + } + + // MARK: - Normalization + + fileprivate static var charactersToRemove: CharacterSet = { + // * We want to strip punctuation - and our definition of "punctuation" + // is broader than `CharacterSet.punctuationCharacters`. + // * FTS should be robust to (i.e. ignore) illegal and control characters, + // but it's safer if we filter them ourselves as well. + var charactersToFilter = CharacterSet.punctuationCharacters + charactersToFilter.formUnion(CharacterSet.illegalCharacters) + charactersToFilter.formUnion(CharacterSet.controlCharacters) + + // We want to strip all ASCII characters except: + // * Letters a-z, A-Z + // * Numerals 0-9 + // * Whitespace + var asciiToFilter = CharacterSet(charactersIn: UnicodeScalar(0x0)!.. String { + // 1. Filter out invalid characters. + let filtered = text.removeCharacters(characterSet: charactersToRemove) + + // 2. Simplify whitespace. + let simplified = filtered.replaceCharacters(characterSet: .whitespacesAndNewlines, + replacement: " ") + + // 3. Strip leading & trailing whitespace last, since we may replace + // filtered characters with whitespace. + return simplified.trimmingCharacters(in: .whitespacesAndNewlines) + } + + // MARK: - Index Building + + private class var contactsManager: ContactsManagerProtocol { + return SSKEnvironment.shared.contactsManager + } + + private static let groupThreadIndexer: SearchIndexer = SearchIndexer { (groupThread: TSGroupThread, transaction: YapDatabaseReadTransaction) in + let groupName = groupThread.groupModel.groupName ?? "" + + let memberStrings = groupThread.groupModel.groupMemberIds.map { recipientId in + recipientIndexer.index(recipientId, transaction: transaction) + }.joined(separator: " ") + + return "\(groupName) \(memberStrings)" + } + + private static let contactThreadIndexer: SearchIndexer = SearchIndexer { (contactThread: TSContactThread, transaction: YapDatabaseReadTransaction) in + let recipientId = contactThread.contactIdentifier() + var result = recipientIndexer.index(recipientId, transaction: transaction) + + if IsNoteToSelfEnabled(), + let localNumber = tsAccountManager.storedOrCachedLocalNumber(transaction), + localNumber == recipientId { + + let noteToSelfLabel = NSLocalizedString("NOTE_TO_SELF", comment: "Label for 1:1 conversation with yourself.") + result += " \(noteToSelfLabel)" + } + + return result + } + + private static let recipientIndexer: SearchIndexer = SearchIndexer { (recipientId: String, transaction: YapDatabaseReadTransaction) in + let displayName = contactsManager.displayName(forPhoneIdentifier: recipientId, transaction: transaction) + + let nationalNumber: String = { (recipientId: String) -> String in + + guard let phoneNumber = PhoneNumber(fromE164: recipientId) else { + owsFailDebug("unexpected unparseable recipientId: \(recipientId)") + return "" + } + + guard let digitScalars = phoneNumber.nationalNumber?.unicodeScalars.filter({ CharacterSet.decimalDigits.contains($0) }) else { + owsFailDebug("unexpected unparseable recipientId: \(recipientId)") + return "" + } + + return String(String.UnicodeScalarView(digitScalars)) + }(recipientId) + + return "\(recipientId) \(nationalNumber) \(displayName)" + } + + private static let messageIndexer: SearchIndexer = SearchIndexer { (message: TSMessage, transaction: YapDatabaseReadTransaction) in + if let bodyText = message.bodyText(with: transaction) { + return bodyText + } + return "" + } + + private class func indexContent(object: Any, transaction: YapDatabaseReadTransaction) -> String? { + if let groupThread = object as? TSGroupThread { + return self.groupThreadIndexer.index(groupThread, transaction: transaction) + } else if let contactThread = object as? TSContactThread { + guard contactThread.shouldThreadBeVisible && !contactThread.isSlaveThread else { + // If we've never sent/received a message in a TSContactThread, + // then we want it to appear in the "Other Contacts" section rather + // than in the "Conversations" section. + return nil + } + return self.contactThreadIndexer.index(contactThread, transaction: transaction) + } else if let message = object as? TSMessage { + return self.messageIndexer.index(message, transaction: transaction) + } else if let signalAccount = object as? SignalAccount { + return self.recipientIndexer.index(signalAccount.recipientId, transaction: transaction) + } else { + return nil + } + } + + // MARK: - Extension Registration + + private static let dbExtensionName: String = "FullTextSearchFinderExtension" + + private func ext(transaction: YapDatabaseReadTransaction) -> YapDatabaseFullTextSearchTransaction? { + return transaction.ext(FullTextSearchFinder.dbExtensionName) as? YapDatabaseFullTextSearchTransaction + } + + @objc + public class func asyncRegisterDatabaseExtension(storage: OWSStorage) { + storage.asyncRegister(dbExtensionConfig, withName: dbExtensionName) + } + + // Only for testing. + public class func ensureDatabaseExtensionRegistered(storage: OWSStorage) { + guard storage.registeredExtension(dbExtensionName) == nil else { + return + } + + storage.register(dbExtensionConfig, withName: dbExtensionName) + } + + private class var dbExtensionConfig: YapDatabaseFullTextSearch { + AssertIsOnMainThread() + + let contentColumnName = "content" + + let handler = YapDatabaseFullTextSearchHandler.withObjectBlock { (transaction: YapDatabaseReadTransaction, dict: NSMutableDictionary, _: String, _: String, object: Any) in + dict[contentColumnName] = indexContent(object: object, transaction: transaction) + } + + // update search index on contact name changes? + + return YapDatabaseFullTextSearch(columnNames: ["content"], + options: nil, + handler: handler, + ftsVersion: YapDatabaseFullTextSearchFTS5Version, + versionTag: "1") + } +} diff --git a/SignalUtilitiesKit/FunctionalUtil.h b/SignalUtilitiesKit/FunctionalUtil.h new file mode 100644 index 000000000..e86ed911a --- /dev/null +++ b/SignalUtilitiesKit/FunctionalUtil.h @@ -0,0 +1,27 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray (FunctionalUtil) + +/// Returns true when any of the items in this array match the given predicate. +- (bool)any:(int (^)(id item))predicate; + +/// Returns true when all of the items in this array match the given predicate. +- (bool)all:(int (^)(id item))predicate; + +/// Returns an array of all the results of passing items from this array through the given projection function. +- (NSArray *)map:(id (^)(id item))projection; + +/// Returns an array of all the results of passing items from this array through the given projection function. +- (NSArray *)filter:(int (^)(id item))predicate; + +- (NSDictionary *)groupBy:(id (^)(id value))keySelector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/FunctionalUtil.m b/SignalUtilitiesKit/FunctionalUtil.m new file mode 100644 index 000000000..5ff42f203 --- /dev/null +++ b/SignalUtilitiesKit/FunctionalUtil.m @@ -0,0 +1,98 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "FunctionalUtil.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FUBadArgument : NSException + ++ (FUBadArgument *) new:(NSString *)reason; ++ (void)raise:(NSString *)message; + +@end + +@implementation FUBadArgument + ++ (FUBadArgument *) new:(NSString *)reason { + return [[FUBadArgument alloc] initWithName:@"Invalid Argument" reason:reason userInfo:nil]; +} ++ (void)raise:(NSString *)message { + [FUBadArgument raise:@"Invalid Argument" format:@"%@", message]; +} + +@end + +#define tskit_require(expr) \ + if (!(expr)) { \ + NSString *reason = \ + [NSString stringWithFormat:@"require %@ (in %s at line %d)", (@ #expr), __FILE__, __LINE__]; \ + OWSLogError(@"%@", reason); \ + [FUBadArgument raise:reason]; \ + }; + + +@implementation NSArray (FunctionalUtil) +- (bool)any:(int (^)(id item))predicate { + tskit_require(predicate != nil); + for (id e in self) { + if (predicate(e)) { + return true; + } + } + return false; +} +- (bool)all:(int (^)(id item))predicate { + tskit_require(predicate != nil); + for (id e in self) { + if (!predicate(e)) { + return false; + } + } + return true; +} +- (NSArray *)map:(id (^)(id item))projection { + tskit_require(projection != nil); + + NSMutableArray *r = [NSMutableArray arrayWithCapacity:self.count]; + for (id e in self) { + [r addObject:projection(e)]; + } + return r; +} +- (NSArray *)filter:(int (^)(id item))predicate { + tskit_require(predicate != nil); + + NSMutableArray *r = [NSMutableArray array]; + for (id e in self) { + if (predicate(e)) { + [r addObject:e]; + } + } + return r; +} + +- (NSDictionary *)groupBy:(id (^)(id value))keySelector { + tskit_require(keySelector != nil); + + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + for (id item in self) { + id key = keySelector(item); + + NSMutableArray *group = result[key]; + if (group == nil) { + group = [NSMutableArray array]; + result[key] = group; + } + [group addObject:item]; + } + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/GeneralUtilities.swift b/SignalUtilitiesKit/GeneralUtilities.swift new file mode 100644 index 000000000..62fee4eb1 --- /dev/null +++ b/SignalUtilitiesKit/GeneralUtilities.swift @@ -0,0 +1,7 @@ + +public func getUserHexEncodedPublicKey() -> String { + if let keyPair = OWSIdentityManager.shared().identityKeyPair() { // Can be nil under some circumstances + return keyPair.hexEncodedPublicKey + } + return "" +} diff --git a/SignalUtilitiesKit/GroupUtilities.swift b/SignalUtilitiesKit/GroupUtilities.swift new file mode 100644 index 000000000..69b5f9ff1 --- /dev/null +++ b/SignalUtilitiesKit/GroupUtilities.swift @@ -0,0 +1,25 @@ + +public enum GroupUtilities { + + public static func getClosedGroupMembers(_ closedGroup: TSGroupThread) -> [String] { + var result: [String]! + OWSPrimaryStorage.shared().dbReadConnection.read { transaction in + result = getClosedGroupMembers(closedGroup, with: transaction) + } + return result + } + + public static func getClosedGroupMembers(_ closedGroup: TSGroupThread, with transaction: YapDatabaseReadTransaction) -> [String] { + return closedGroup.groupModel.groupMemberIds.filter { member in + OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: member, in: transaction) == nil // Don't show slave devices + } + } + + public static func getClosedGroupMemberCount(_ closedGroup: TSGroupThread) -> Int { + return getClosedGroupMembers(closedGroup).count + } + + public static func getClosedGroupMemberCount(_ closedGroup: TSGroupThread, with transaction: YapDatabaseReadTransaction) -> Int { + return getClosedGroupMembers(closedGroup, with: transaction).count + } +} diff --git a/SignalUtilitiesKit/JobQueue.swift b/SignalUtilitiesKit/JobQueue.swift new file mode 100644 index 000000000..366c9992c --- /dev/null +++ b/SignalUtilitiesKit/JobQueue.swift @@ -0,0 +1,411 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +/// JobQueue - A durable work queue +/// +/// When work needs to be done, add it to the JobQueue. +/// The JobQueue will persist a JobRecord to be sure that work can be restarted if the app is killed. +/// +/// The actual work, is carried out in a DurableOperation which the JobQueue spins off, based on the contents +/// of a JobRecord. +/// +/// For a concrete example, take message sending. +/// Add an outgoing message to the MessageSenderJobQueue, which first records a SSKMessageSenderJobRecord. +/// The MessageSenderJobQueue then uses that SSKMessageSenderJobRecord to create a MessageSenderOperation which +/// takes care of the actual business of communicating with the service. +/// +/// DurableOperations are retryable - via their `remainingRetries` logic. However, if the operation encounters +/// an error where `error.isRetryable == false`, the operation will fail, regardless of available retries. + +public extension Error { + var isRetryable: Bool { + return (self as NSError).isRetryable + } +} + +extension SSKJobRecordStatus: CustomStringConvertible { + public var description: String { + switch self { + case .ready: + return "ready" + case .unknown: + return "unknown" + case .running: + return "running" + case .permanentlyFailed: + return "permanentlyFailed" + case .obsolete: + return "obsolete" + } + } +} + +public enum JobError: Error { + case assertionFailure(description: String) + case obsolete(description: String) +} + +public protocol DurableOperation: class { + associatedtype JobRecordType: SSKJobRecord + associatedtype DurableOperationDelegateType: DurableOperationDelegate + + var jobRecord: JobRecordType { get } + var durableOperationDelegate: DurableOperationDelegateType? { get set } + var operation: OWSOperation { get } + var remainingRetries: UInt { get set } +} + +public protocol DurableOperationDelegate: class { + associatedtype DurableOperationType: DurableOperation + + func durableOperationDidSucceed(_ operation: DurableOperationType, transaction: YapDatabaseReadWriteTransaction) + func durableOperation(_ operation: DurableOperationType, didReportError: Error, transaction: YapDatabaseReadWriteTransaction) + func durableOperation(_ operation: DurableOperationType, didFailWithError error: Error, transaction: YapDatabaseReadWriteTransaction) +} + +public protocol JobQueue: DurableOperationDelegate { + typealias DurableOperationDelegateType = Self + typealias JobRecordType = DurableOperationType.JobRecordType + + // MARK: Dependencies + + var dbConnection: YapDatabaseConnection { get } + var finder: JobRecordFinder { get } + + // MARK: Default Implementations + + func add(jobRecord: JobRecordType, transaction: YapDatabaseReadWriteTransaction) + func restartOldJobs() + func workStep() + func defaultSetup() + + // MARK: Required + + var runningOperations: [DurableOperationType] { get set } + var jobRecordLabel: String { get } + + var isSetup: Bool { get set } + func setup() + func didMarkAsReady(oldJobRecord: JobRecordType, transaction: YapDatabaseReadWriteTransaction) + + func operationQueue(jobRecord: JobRecordType) -> OperationQueue + func buildOperation(jobRecord: JobRecordType, transaction: YapDatabaseReadTransaction) throws -> DurableOperationType + + /// When `requiresInternet` is true, we immediately run any jobs which are waiting for retry upon detecting Reachability. + /// + /// Because `Reachability` isn't 100% reliable, the jobs will be attempted regardless of what we think our current Reachability is. + /// However, because these jobs will likely fail many times in succession, their `retryInterval` could be quite long by the time we + /// are back online. + var requiresInternet: Bool { get } + static var maxRetries: UInt { get } +} + +public extension JobQueue { + + // MARK: Dependencies + + var dbConnection: YapDatabaseConnection { + return SSKEnvironment.shared.primaryStorage.dbReadWriteConnection + } + + var finder: JobRecordFinder { + return JobRecordFinder() + } + + var reachabilityManager: SSKReachabilityManager { + return SSKEnvironment.shared.reachabilityManager + } + + // MARK: + + func add(jobRecord: JobRecordType, transaction: YapDatabaseReadWriteTransaction) { + assert(jobRecord.status == .ready) + + jobRecord.save(with: transaction) + + transaction.addCompletionQueue(DispatchQueue.global()) { + self.startWorkWhenAppIsReady() + } + } + + func startWorkWhenAppIsReady() { + guard !CurrentAppContext().isRunningTests else { + DispatchQueue.global().async { + self.workStep() + } + return + } + + AppReadiness.runNowOrWhenAppDidBecomeReady { + DispatchQueue.global().async { + self.workStep() + } + } + } + + func workStep() { + Logger.debug("") + + guard isSetup else { + if !CurrentAppContext().isRunningTests { + owsFailDebug("not setup") + } + + return + } + + Storage.writeSync { transaction in + guard let nextJob: JobRecordType = self.finder.getNextReady(label: self.jobRecordLabel, transaction: transaction) as? JobRecordType else { + Logger.verbose("nothing left to enqueue") + return + } + + do { + try nextJob.saveAsStarted(transaction: transaction) + + let operationQueue = self.operationQueue(jobRecord: nextJob) + let durableOperation = try self.buildOperation(jobRecord: nextJob, transaction: transaction) + + durableOperation.durableOperationDelegate = self as? Self.DurableOperationType.DurableOperationDelegateType + assert(durableOperation.durableOperationDelegate != nil) + + let remainingRetries = self.remainingRetries(durableOperation: durableOperation) + durableOperation.remainingRetries = remainingRetries + + self.runningOperations.append(durableOperation) + + Logger.debug("adding operation: \(durableOperation) with remainingRetries: \(remainingRetries)") + operationQueue.addOperation(durableOperation.operation) + } catch JobError.assertionFailure(let description) { + owsFailDebug("assertion failure: \(description)") + nextJob.saveAsPermanentlyFailed(transaction: transaction) + } catch JobError.obsolete(let description) { + // TODO is this even worthwhile to have obsolete state? Should we just delete the task outright? + Logger.verbose("marking obsolete task as such. description:\(description)") + nextJob.saveAsObsolete(transaction: transaction) + } catch { + owsFailDebug("unexpected error") + } + + DispatchQueue.global().async { + self.workStep() + } + } + } + + public func restartOldJobs() { + Storage.writeSync { transaction in + let runningRecords = self.finder.allRecords(label: self.jobRecordLabel, status: .running, transaction: transaction) + Logger.info("marking old `running` JobRecords as ready: \(runningRecords.count)") + for record in runningRecords { + guard let jobRecord = record as? JobRecordType else { + owsFailDebug("unexpectred jobRecord: \(record)") + continue + } + do { + try jobRecord.saveRunningAsReady(transaction: transaction) + self.didMarkAsReady(oldJobRecord: jobRecord, transaction: transaction) + } catch { + owsFailDebug("failed to mark old running records as ready error: \(error)") + jobRecord.saveAsPermanentlyFailed(transaction: transaction) + } + } + } + } + + /// Unless you need special handling, your setup method can be as simple as + /// + /// func setup() { + /// defaultSetup() + /// } + /// + /// So you might ask, why not just rename this method to `setup`? Because + /// `setup` is called from objc, and default implementations from a protocol + /// cannot be marked as @objc. + func defaultSetup() { + guard !isSetup else { + owsFailDebug("already ready already") + return + } + self.restartOldJobs() + + if self.requiresInternet { + NotificationCenter.default.addObserver(forName: .reachabilityChanged, + object: self.reachabilityManager.observationContext, + queue: nil) { _ in + + if self.reachabilityManager.isReachable { + Logger.verbose("isReachable: true") + self.becameReachable() + } else { + Logger.verbose("isReachable: false") + } + } + } + + self.isSetup = true + + self.startWorkWhenAppIsReady() + } + + func remainingRetries(durableOperation: DurableOperationType) -> UInt { + let maxRetries = type(of: self).maxRetries + let failureCount = durableOperation.jobRecord.failureCount + + guard maxRetries > failureCount else { + return 0 + } + + return maxRetries - failureCount + } + + func becameReachable() { + guard requiresInternet else { + owsFailDebug("should only be called if `requiresInternet` is true") + return + } + + _ = self.runAnyQueuedRetry() + } + + func runAnyQueuedRetry() -> DurableOperationType? { + guard let runningDurableOperation = self.runningOperations.first else { + return nil + } + runningDurableOperation.operation.runAnyQueuedRetry() + + return runningDurableOperation + } + + // MARK: DurableOperationDelegate + + func durableOperationDidSucceed(_ operation: DurableOperationType, transaction: YapDatabaseReadWriteTransaction) { + self.runningOperations = self.runningOperations.filter { $0 !== operation } + operation.jobRecord.remove(with: transaction) + } + + func durableOperation(_ operation: DurableOperationType, didReportError: Error, transaction: YapDatabaseReadWriteTransaction) { + do { + try operation.jobRecord.addFailure(transaction: transaction) + } catch { + owsFailDebug("error while addingFailure: \(error)") + operation.jobRecord.saveAsPermanentlyFailed(transaction: transaction) + } + } + + func durableOperation(_ operation: DurableOperationType, didFailWithError error: Error, transaction: YapDatabaseReadWriteTransaction) { + self.runningOperations = self.runningOperations.filter { $0 !== operation } + operation.jobRecord.saveAsPermanentlyFailed(transaction: transaction) + } +} + +@objc(SSKJobRecordFinder) +public class JobRecordFinder: NSObject, Finder { + + typealias ExtensionType = YapDatabaseSecondaryIndex + typealias TransactionType = YapDatabaseSecondaryIndexTransaction + + enum JobRecordField: String { + case status, label, sortId + } + + func getNextReady(label: String, transaction: YapDatabaseReadTransaction) -> SSKJobRecord? { + var result: SSKJobRecord? + self.enumerateJobRecords(label: label, status: .ready, transaction: transaction) { jobRecord, stopPointer in + result = jobRecord + stopPointer.pointee = true + } + return result + } + + func allRecords(label: String, status: SSKJobRecordStatus, transaction: YapDatabaseReadTransaction) -> [SSKJobRecord] { + var result: [SSKJobRecord] = [] + self.enumerateJobRecords(label: label, status: status, transaction: transaction) { jobRecord, _ in + result.append(jobRecord) + } + return result + } + + func enumerateJobRecords(label: String, status: SSKJobRecordStatus, transaction: YapDatabaseReadTransaction, block: @escaping (SSKJobRecord, UnsafeMutablePointer) -> Void) { + let queryFormat = String(format: "WHERE %@ = ? AND %@ = ? ORDER BY %@", JobRecordField.status.rawValue, JobRecordField.label.rawValue, JobRecordField.sortId.rawValue) + let query = YapDatabaseQuery(string: queryFormat, parameters: [status.rawValue, label]) + + self.ext(transaction: transaction).enumerateKeysAndObjects(matching: query) { _, _, object, stopPointer in + guard let jobRecord = object as? SSKJobRecord else { + owsFailDebug("expecting jobRecord but found: \(object)") + return + } + block(jobRecord, stopPointer) + } + } + + static var dbExtensionName: String { + return "SecondaryIndexJobRecord" + } + + @objc + public class func asyncRegisterDatabaseExtensionObjC(storage: OWSStorage) { + asyncRegisterDatabaseExtension(storage: storage) + } + + static var dbExtensionConfig: YapDatabaseSecondaryIndex { + let setup = YapDatabaseSecondaryIndexSetup() + setup.addColumn(JobRecordField.sortId.rawValue, with: .integer) + setup.addColumn(JobRecordField.status.rawValue, with: .integer) + setup.addColumn(JobRecordField.label.rawValue, with: .text) + + let block: YapDatabaseSecondaryIndexWithObjectBlock = { transaction, dict, collection, key, object in + guard let jobRecord = object as? SSKJobRecord else { + return + } + + dict[JobRecordField.sortId.rawValue] = jobRecord.sortId + dict[JobRecordField.status.rawValue] = jobRecord.status.rawValue + dict[JobRecordField.label.rawValue] = jobRecord.label + } + + let handler = YapDatabaseSecondaryIndexHandler.withObjectBlock(block) + + let options = YapDatabaseSecondaryIndexOptions() + let whitelist = YapWhitelistBlacklist(whitelist: Set([SSKJobRecord.collection()])) + options.allowedCollections = whitelist + + return YapDatabaseSecondaryIndex.init(setup: setup, handler: handler, versionTag: "2", options: options) + } +} + +protocol Finder { + associatedtype ExtensionType: YapDatabaseExtension + associatedtype TransactionType: YapDatabaseExtensionTransaction + + static var dbExtensionName: String { get } + static var dbExtensionConfig: ExtensionType { get } + + func ext(transaction: YapDatabaseReadTransaction) -> TransactionType + + static func asyncRegisterDatabaseExtension(storage: OWSStorage) + static func testingOnly_ensureDatabaseExtensionRegistered(storage: OWSStorage) +} + +extension Finder { + + func ext(transaction: YapDatabaseReadTransaction) -> TransactionType { + return transaction.ext(type(of: self).dbExtensionName) as! TransactionType + } + + static func asyncRegisterDatabaseExtension(storage: OWSStorage) { + storage.asyncRegister(dbExtensionConfig, withName: dbExtensionName) + } + + // Only for testing. + static func testingOnly_ensureDatabaseExtensionRegistered(storage: OWSStorage) { + guard storage.registeredExtension(dbExtensionName) == nil else { + return + } + + storage.register(dbExtensionConfig, withName: dbExtensionName) + } +} diff --git a/SignalUtilitiesKit/LKDeviceLinkMessage.h b/SignalUtilitiesKit/LKDeviceLinkMessage.h new file mode 100644 index 000000000..783078a42 --- /dev/null +++ b/SignalUtilitiesKit/LKDeviceLinkMessage.h @@ -0,0 +1,19 @@ +#import "TSOutgoingMessage.h" + +typedef NS_ENUM(NSUInteger, LKDeviceLinkMessageKind) { + LKDeviceLinkMessageKindRequest = 1, + LKDeviceLinkMessageKindAuthorization = 2, +}; + +NS_SWIFT_NAME(DeviceLinkMessage) +@interface LKDeviceLinkMessage : TSOutgoingMessage + +@property (nonatomic, readonly) NSString *masterPublicKey; +@property (nonatomic, readonly) NSString *slavePublicKey; +@property (nonatomic, readonly) NSData *masterSignature; // nil for device linking requests +@property (nonatomic, readonly) NSData *slaveSignature; +@property (nonatomic, readonly) LKDeviceLinkMessageKind kind; + +- (instancetype)initInThread:(TSThread *)thread masterPublicKey:(NSString *)masterHexEncodedPublicKey slavePublicKey:(NSString *)slaveHexEncodedPublicKey masterSignature:(NSData * _Nullable)masterSignature slaveSignature:(NSData *)slaveSignature; + +@end diff --git a/SignalUtilitiesKit/LKDeviceLinkMessage.m b/SignalUtilitiesKit/LKDeviceLinkMessage.m new file mode 100644 index 000000000..1b1cc7219 --- /dev/null +++ b/SignalUtilitiesKit/LKDeviceLinkMessage.m @@ -0,0 +1,89 @@ +#import "LKDeviceLinkMessage.h" +#import "OWSIdentityManager.h" +#import "OWSPrimaryStorage+Loki.h" +#import "ProfileManagerProtocol.h" +#import "ProtoUtils.h" +#import "SSKEnvironment.h" +#import "SignalRecipient.h" +#import +#import +#import + +@implementation LKDeviceLinkMessage + +#pragma mark Convenience +- (LKDeviceLinkMessageKind)kind { + if (self.masterSignature != nil) { + return LKDeviceLinkMessageKindAuthorization; + } else { + return LKDeviceLinkMessageKindRequest; + } +} + +#pragma mark Initialization +- (instancetype)initInThread:(TSThread *)thread masterPublicKey:(NSString *)masterHexEncodedPublicKey slavePublicKey:(NSString *)slaveHexEncodedPublicKey masterSignature:(NSData * _Nullable)masterSignature slaveSignature:(NSData *)slaveSignature { + self = [self initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"" attachmentIds:[NSMutableArray new] + expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil]; + if (self) { + _masterPublicKey = masterHexEncodedPublicKey; + _slavePublicKey = slaveHexEncodedPublicKey; + _masterSignature = masterSignature; + _slaveSignature = slaveSignature; + } + return self; +} + +#pragma mark Building +- (nullable id)prepareCustomContentBuilder:(SignalRecipient *)recipient { + SSKProtoContentBuilder *contentBuilder = [super prepareCustomContentBuilder:recipient]; + NSError *error; + if (self.kind == LKDeviceLinkMessageKindRequest) { + // The slave device attaches a pre key bundle with the request it sends so that a + // session can be established with the master device. + PreKeyBundle *preKeyBundle = [OWSPrimaryStorage.sharedManager generatePreKeyBundleForContact:recipient.recipientId]; + SSKProtoPrekeyBundleMessageBuilder *preKeyBundleMessageBuilder = [SSKProtoPrekeyBundleMessage builderFromPreKeyBundle:preKeyBundle]; + SSKProtoPrekeyBundleMessage *preKeyBundleMessage = [preKeyBundleMessageBuilder buildAndReturnError:&error]; + if (error || preKeyBundleMessage == nil) { + OWSFailDebug(@"Failed to build pre key bundle message for: %@ due to error: %@.", recipient.recipientId, error); + return nil; + } else { + [contentBuilder setPrekeyBundleMessage:preKeyBundleMessage]; + } + } else { + // The master device attaches its display name and profile picture URL to the device link + // authorization message so that the slave device is in sync with these things as soon + // as possible. + id profileManager = SSKEnvironment.shared.profileManager; + NSString *displayName = profileManager.localProfileName; + NSString *profilePictureURL = profileManager.profilePictureURL; + SSKProtoDataMessageLokiProfileBuilder *profileBuilder = [SSKProtoDataMessageLokiProfile builder]; + [profileBuilder setDisplayName:displayName]; + [profileBuilder setProfilePicture:profilePictureURL ?: @""]; + SSKProtoDataMessageBuilder *messageBuilder = [SSKProtoDataMessage builder]; + [messageBuilder setProfile:[profileBuilder buildAndReturnError:nil]]; + [ProtoUtils addLocalProfileKeyToDataMessageBuilder:messageBuilder]; + [contentBuilder setDataMessage:[messageBuilder buildIgnoringErrors]]; + } + // Build the device link message + SSKProtoLokiDeviceLinkMessageBuilder *deviceLinkMessageBuilder = [SSKProtoLokiDeviceLinkMessage builder]; + [deviceLinkMessageBuilder setMasterPublicKey:self.masterPublicKey]; + [deviceLinkMessageBuilder setSlavePublicKey:self.slavePublicKey]; + if (self.masterSignature != nil) { [deviceLinkMessageBuilder setMasterSignature:self.masterSignature]; } + [deviceLinkMessageBuilder setSlaveSignature:self.slaveSignature]; + SSKProtoLokiDeviceLinkMessage *deviceLinkMessage = [deviceLinkMessageBuilder buildAndReturnError:&error]; + if (error || deviceLinkMessage == nil) { + OWSFailDebug(@"Failed to build device link message for: %@ due to error: %@.", recipient.recipientId, error); + return nil; + } else { + [contentBuilder setLokiDeviceLinkMessage:deviceLinkMessage]; + } + // Return + return contentBuilder; +} + +#pragma mark Settings +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeLinkDevice]; } +- (BOOL)shouldSyncTranscript { return NO; } +- (BOOL)shouldBeSaved { return NO; } + +@end diff --git a/SignalUtilitiesKit/LKGroupUtilities.h b/SignalUtilitiesKit/LKGroupUtilities.h new file mode 100644 index 000000000..b613a37aa --- /dev/null +++ b/SignalUtilitiesKit/LKGroupUtilities.h @@ -0,0 +1,26 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface LKGroupUtilities : NSObject + ++(NSString *)getEncodedOpenGroupID:(NSString *)groupID; ++(NSData *)getEncodedOpenGroupIDAsData:(NSString *)groupID; + ++(NSString *)getEncodedRSSFeedID:(NSString *)groupID; ++(NSData *)getEncodedRSSFeedIDAsData:(NSString *)groupID; + ++(NSString *)getEncodedClosedGroupID:(NSString *)groupID; ++(NSData *)getEncodedClosedGroupIDAsData:(NSString *)groupID; + ++(NSString *)getEncodedMMSGroupID:(NSString *)groupID; ++(NSData *)getEncodedMMSGroupIDAsData:(NSString *)groupID; + ++(NSString *)getEncodedGroupID:(NSData *)groupID; + ++(NSString *)getDecodedGroupID:(NSData *)groupID; ++(NSData *)getDecodedGroupIDAsData:(NSData *)groupID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/LKGroupUtilities.m b/SignalUtilitiesKit/LKGroupUtilities.m new file mode 100644 index 000000000..f0cd741ad --- /dev/null +++ b/SignalUtilitiesKit/LKGroupUtilities.m @@ -0,0 +1,77 @@ +#import "LKGroupUtilities.h" +#import + +@implementation LKGroupUtilities + +#define ClosedGroupPrefix @"__textsecure_group__!" // a.k.a. private group chat +#define MMSGroupPrefix @"__signal_mms_group__!" +#define OpenGroupPrefix @"__loki_public_chat_group__!" // a.k.a. public group chat +#define RSSFeedPrefix @"__loki_rss_feed_group__!" + ++(NSString *)getEncodedOpenGroupID:(NSString *)groupID +{ + return [OpenGroupPrefix stringByAppendingString:groupID]; +} + ++(NSData *)getEncodedOpenGroupIDAsData:(NSString *)groupID +{ + return [[OpenGroupPrefix stringByAppendingString:groupID] dataUsingEncoding:NSUTF8StringEncoding]; +} + ++(NSString *)getEncodedRSSFeedID:(NSString *)groupID +{ + return [RSSFeedPrefix stringByAppendingString:groupID]; +} + ++(NSData *)getEncodedRSSFeedIDAsData:(NSString *)groupID +{ + return [[RSSFeedPrefix stringByAppendingString:groupID] dataUsingEncoding:NSUTF8StringEncoding]; +} + ++(NSString *)getEncodedClosedGroupID:(NSString *)groupID +{ + return [ClosedGroupPrefix stringByAppendingString:groupID]; +} + ++(NSData *)getEncodedClosedGroupIDAsData:(NSString *)groupID +{ + return [[ClosedGroupPrefix stringByAppendingString:groupID] dataUsingEncoding:NSUTF8StringEncoding]; +} + ++(NSString *)getEncodedMMSGroupID:(NSString *)groupID +{ + return [MMSGroupPrefix stringByAppendingString:groupID]; +} + ++(NSData *)getEncodedMMSGroupIDAsData:(NSString *)groupID +{ + return [[MMSGroupPrefix stringByAppendingString:groupID] dataUsingEncoding:NSUTF8StringEncoding]; +} + ++(NSString *)getEncodedGroupID: (NSData *)groupID +{ + return [[NSString alloc] initWithData:groupID encoding:NSUTF8StringEncoding]; +} + ++(NSString *)getDecodedGroupID:(NSData *)groupID +{ + OWSAssertDebug(groupID.length > 0); + NSString *encodedGroupID = [[NSString alloc] initWithData:groupID encoding:NSUTF8StringEncoding]; + if ([encodedGroupID componentsSeparatedByString:@"!"].count > 1) { + return [encodedGroupID componentsSeparatedByString:@"!"][1]; + } + return [encodedGroupID componentsSeparatedByString:@"!"][0]; +} + ++(NSData *)getDecodedGroupIDAsData:(NSData *)groupID +{ + OWSAssertDebug(groupID.length > 0); + NSString *encodedGroupID = [[NSString alloc]initWithData:groupID encoding:NSUTF8StringEncoding]; + NSString *decodedGroupID = [encodedGroupID componentsSeparatedByString:@"!"][0]; + if ([encodedGroupID componentsSeparatedByString:@"!"].count > 1) { + decodedGroupID = [encodedGroupID componentsSeparatedByString:@"!"][1]; + } + return [decodedGroupID dataUsingEncoding:NSUTF8StringEncoding]; +} + +@end diff --git a/SignalUtilitiesKit/LKSyncOpenGroupsMessage.h b/SignalUtilitiesKit/LKSyncOpenGroupsMessage.h new file mode 100644 index 000000000..674c74180 --- /dev/null +++ b/SignalUtilitiesKit/LKSyncOpenGroupsMessage.h @@ -0,0 +1,14 @@ +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(SyncOpenGroupsMessage) +@interface LKSyncOpenGroupsMessage : OWSOutgoingSyncMessage + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/LKSyncOpenGroupsMessage.m b/SignalUtilitiesKit/LKSyncOpenGroupsMessage.m new file mode 100644 index 000000000..8881ec3e1 --- /dev/null +++ b/SignalUtilitiesKit/LKSyncOpenGroupsMessage.m @@ -0,0 +1,43 @@ +#import "LKSyncOpenGroupsMessage.h" +#import "OWSPrimaryStorage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation LKSyncOpenGroupsMessage + +- (instancetype)init +{ + return [super init]; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + NSError *error; + NSMutableArray *openGroupSyncMessages = @[].mutableCopy; + __block NSDictionary *openGroups; + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + openGroups = [LKDatabaseUtilities getAllPublicChats:transaction]; + }]; + for (SNOpenGroup *openGroup in openGroups.allValues) { + SSKProtoSyncMessageOpenGroupDetailsBuilder *openGroupSyncMessageBuilder = [SSKProtoSyncMessageOpenGroupDetails builderWithUrl:openGroup.server channelID:openGroup.channel]; + SSKProtoSyncMessageOpenGroupDetails *_Nullable openGroupSyncMessage = [openGroupSyncMessageBuilder buildAndReturnError:&error]; + if (error || !openGroupSyncMessage) { + OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error); + return nil; + } + [openGroupSyncMessages addObject:openGroupSyncMessage]; + } + SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder]; + [syncMessageBuilder setOpenGroups:openGroupSyncMessages]; + return syncMessageBuilder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/LKUnlinkDeviceMessage.h b/SignalUtilitiesKit/LKUnlinkDeviceMessage.h new file mode 100644 index 000000000..b9b9f9fbb --- /dev/null +++ b/SignalUtilitiesKit/LKUnlinkDeviceMessage.h @@ -0,0 +1,12 @@ +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(UnlinkDeviceMessage) +@interface LKUnlinkDeviceMessage : TSOutgoingMessage + +- (instancetype)initWithThread:(TSThread *)thread; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/LKUnlinkDeviceMessage.m b/SignalUtilitiesKit/LKUnlinkDeviceMessage.m new file mode 100644 index 000000000..ed941cf07 --- /dev/null +++ b/SignalUtilitiesKit/LKUnlinkDeviceMessage.m @@ -0,0 +1,27 @@ +#import "LKUnlinkDeviceMessage.h" +#import +#import + +@implementation LKUnlinkDeviceMessage + +#pragma mark Initialization +- (instancetype)initWithThread:(TSThread *)thread { + return [self initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"" attachmentIds:[NSMutableArray new] + expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil]; +} + +#pragma mark Building +- (nullable id)dataMessageBuilder +{ + SSKProtoDataMessageBuilder *builder = super.dataMessageBuilder; + if (builder == nil) { return nil; } + [builder setFlags:SSKProtoDataMessageFlagsUnlinkDevice]; + return builder; +} + +#pragma mark Settings +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeUnlinkDevice]; } +- (BOOL)shouldSyncTranscript { return NO; } +- (BOOL)shouldBeSaved { return NO; } + +@end diff --git a/SignalUtilitiesKit/LKUserDefaults.swift b/SignalUtilitiesKit/LKUserDefaults.swift new file mode 100644 index 000000000..0ce6bea81 --- /dev/null +++ b/SignalUtilitiesKit/LKUserDefaults.swift @@ -0,0 +1,73 @@ +import Foundation + +public enum LKUserDefaults { + + public enum Bool : Swift.String { + case hasLaunchedOnce + case hasSeenGIFMetadataWarning + case hasViewedSeed + case isUsingFullAPNs + /// Whether the device was unlinked as a slave device (used to notify the user on the landing screen). + case wasUnlinked + } + + public enum Date : Swift.String { + case lastProfilePictureUpload + } + + public enum Double : Swift.String { + /// - Note: Deprecated + case lastDeviceTokenUpload = "lastDeviceTokenUploadTime" + } + + public enum Int: Swift.String { + case appMode + } + + public enum String { + case slaveDeviceName(Swift.String) + case deviceToken + /// `nil` if this is a master device or if the user hasn't linked a device. + case masterHexEncodedPublicKey + + public var key: Swift.String { + switch self { + case .slaveDeviceName(let hexEncodedPublicKey): return "\(hexEncodedPublicKey)_display_name" + case .deviceToken: return "deviceToken" + case .masterHexEncodedPublicKey: return "masterDeviceHexEncodedPublicKey" + } + } + } +} + +public extension UserDefaults { + + public subscript(bool: LKUserDefaults.Bool) -> Bool { + get { return self.bool(forKey: bool.rawValue) } + set { set(newValue, forKey: bool.rawValue) } + } + + public subscript(date: LKUserDefaults.Date) -> Date? { + get { return self.object(forKey: date.rawValue) as? Date } + set { set(newValue, forKey: date.rawValue) } + } + + public subscript(double: LKUserDefaults.Double) -> Double { + get { return self.double(forKey: double.rawValue) } + set { set(newValue, forKey: double.rawValue) } + } + + public subscript(int: LKUserDefaults.Int) -> Int { + get { return self.integer(forKey: int.rawValue) } + set { set(newValue, forKey: int.rawValue) } + } + + public subscript(string: LKUserDefaults.String) -> String? { + get { return self.string(forKey: string.key) } + set { set(newValue, forKey: string.key) } + } + + public var isMasterDevice: Bool { + return (self[.masterHexEncodedPublicKey] == nil) + } +} diff --git a/SignalUtilitiesKit/LRUCache.swift b/SignalUtilitiesKit/LRUCache.swift new file mode 100644 index 000000000..436b4e365 --- /dev/null +++ b/SignalUtilitiesKit/LRUCache.swift @@ -0,0 +1,105 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +@objc +public class AnyLRUCache: NSObject { + + private let backingCache: LRUCache + + @objc + public init(maxSize: Int) { + backingCache = LRUCache(maxSize: maxSize) + } + + @objc + public func get(key: NSObject) -> NSObject? { + return self.backingCache.get(key: key) + } + + @objc + public func set(key: NSObject, value: NSObject) { + self.backingCache.set(key: key, value: value) + } + + @objc + public func clear() { + self.backingCache.clear() + } +} + +// A simple LRU cache bounded by the number of entries. +public class LRUCache { + + private var cacheMap: [KeyType: ValueType] = [:] + private var cacheOrder: [KeyType] = [] + private let maxSize: Int + + @objc + public init(maxSize: Int) { + self.maxSize = maxSize + + NotificationCenter.default.addObserver(self, + selector: #selector(didReceiveMemoryWarning), + name: UIApplication.didReceiveMemoryWarningNotification, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(didEnterBackground), + name: NSNotification.Name.OWSApplicationDidEnterBackground, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc func didEnterBackground() { + AssertIsOnMainThread() + + clear() + } + + @objc func didReceiveMemoryWarning() { + AssertIsOnMainThread() + + clear() + } + + private func updateCacheOrder(key: KeyType) { + cacheOrder = cacheOrder.filter { $0 != key } + cacheOrder.append(key) + } + + public func get(key: KeyType) -> ValueType? { + guard let value = cacheMap[key] else { + // Miss + return nil + } + + // Hit + updateCacheOrder(key: key) + + return value + } + + public func set(key: KeyType, value: ValueType) { + cacheMap[key] = value + + updateCacheOrder(key: key) + + while cacheOrder.count > maxSize { + guard let staleKey = cacheOrder.first else { + owsFailDebug("Cache ordering unexpectedly empty") + return + } + cacheOrder.removeFirst() + cacheMap.removeValue(forKey: staleKey) + } + } + + @objc + public func clear() { + cacheMap.removeAll() + cacheOrder.removeAll() + } +} diff --git a/SignalUtilitiesKit/LokiDatabaseUtilities.swift b/SignalUtilitiesKit/LokiDatabaseUtilities.swift new file mode 100644 index 000000000..8655eb10c --- /dev/null +++ b/SignalUtilitiesKit/LokiDatabaseUtilities.swift @@ -0,0 +1,98 @@ + +@objc(LKDatabaseUtilities) +public final class LokiDatabaseUtilities : NSObject { + + private override init() { } + + // MARK: - Quotes + @objc(getServerIDForQuoteWithID:quoteeHexEncodedPublicKey:threadID:transaction:) + public static func getServerID(quoteID: UInt64, quoteeHexEncodedPublicKey: String, threadID: String, transaction: YapDatabaseReadTransaction) -> UInt64 { + guard let message = TSInteraction.interactions(withTimestamp: quoteID, filter: { interaction in + let senderHexEncodedPublicKey: String + if let message = interaction as? TSIncomingMessage { + senderHexEncodedPublicKey = message.authorId + } else if let message = interaction as? TSOutgoingMessage { + senderHexEncodedPublicKey = getUserHexEncodedPublicKey() + } else { + return false + } + return (senderHexEncodedPublicKey == quoteeHexEncodedPublicKey) && (interaction.uniqueThreadId == threadID) + }, with: transaction).first as! TSMessage? else { return 0 } + return message.openGroupServerMessageID + } + + + + // MARK: - Device Links + @objc(getLinkedDeviceHexEncodedPublicKeysFor:in:) + public static func getLinkedDeviceHexEncodedPublicKeys(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set { + return [ hexEncodedPublicKey ] + /* + let storage = OWSPrimaryStorage.shared() + let masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey + var result = Set(storage.getDeviceLinks(for: masterHexEncodedPublicKey, in: transaction).flatMap { deviceLink in + return [ deviceLink.master.publicKey, deviceLink.slave.publicKey ] + }) + result.insert(hexEncodedPublicKey) + return result + */ + } + + @objc(getLinkedDeviceThreadsFor:in:) + public static func getLinkedDeviceThreads(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set { + return Set([ TSContactThread.getWithContactId(hexEncodedPublicKey, transaction: transaction) ].compactMap { $0 }) +// return Set(getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction).compactMap { TSContactThread.getWithContactId($0, transaction: transaction) }) + } + + @objc(isUserLinkedDevice:in:) + public static func isUserLinkedDevice(_ hexEncodedPublicKey: String, transaction: YapDatabaseReadTransaction) -> Bool { + return hexEncodedPublicKey == getUserHexEncodedPublicKey() + /* + let userHexEncodedPublicKey = getUserHexEncodedPublicKey() + let userLinkedDeviceHexEncodedPublicKeys = getLinkedDeviceHexEncodedPublicKeys(for: userHexEncodedPublicKey, in: transaction) + return userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey) + */ + } + + @objc(getMasterHexEncodedPublicKeyFor:in:) + public static func objc_getMasterHexEncodedPublicKey(for slaveHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> String? { + return nil +// return OWSPrimaryStorage.shared().getMasterHexEncodedPublicKey(for: slaveHexEncodedPublicKey, in: transaction) + } + + @objc(getDeviceLinksFor:in:) + public static func objc_getDeviceLinks(for masterHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set { + return [] +// return OWSPrimaryStorage.shared().getDeviceLinks(for: masterHexEncodedPublicKey, in: transaction) + } + + + + // MARK: - Open Groups + private static let publicChatCollection = "LokiPublicChatCollection" + + @objc(getAllPublicChats:) + public static func getAllPublicChats(in transaction: YapDatabaseReadTransaction) -> [String:OpenGroup] { + var result = [String:OpenGroup]() + transaction.enumerateKeysAndObjects(inCollection: publicChatCollection) { threadID, object, _ in + guard let publicChat = object as? OpenGroup else { return } + result[threadID] = publicChat + } + return result + } + + @objc(getPublicChatForThreadID:transaction:) + public static func getPublicChat(for threadID: String, in transaction: YapDatabaseReadTransaction) -> OpenGroup? { + return transaction.object(forKey: threadID, inCollection: publicChatCollection) as? OpenGroup + } + + @objc(setPublicChat:threadID:transaction:) + public static func setPublicChat(_ publicChat: OpenGroup, for threadID: String, in transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(publicChat, forKey: threadID, inCollection: publicChatCollection) + } + + @objc(removePublicChatForThreadID:transaction:) + public static func removePublicChat(for threadID: String, in transaction: YapDatabaseReadWriteTransaction) { + transaction.removeObject(forKey: threadID, inCollection: publicChatCollection) + } +} diff --git a/SignalUtilitiesKit/LokiMessage.swift b/SignalUtilitiesKit/LokiMessage.swift new file mode 100644 index 000000000..c2048e763 --- /dev/null +++ b/SignalUtilitiesKit/LokiMessage.swift @@ -0,0 +1,75 @@ +import PromiseKit + +public struct LokiMessage { + /// The hex encoded public key of the recipient. + let recipientPublicKey: String + /// The content of the message. + let data: LosslessStringConvertible + /// The time to live for the message in milliseconds. + let ttl: UInt64 + /// Whether this message is a ping. + /// + /// - Note: The concept of pinging only applies to P2P messaging. + let isPing: Bool + /// When the proof of work was calculated, if applicable (P2P messages don't require proof of work). + /// + /// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. + private(set) var timestamp: UInt64? = nil + /// The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work). + private(set) var nonce: String? = nil + + private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool) { + self.recipientPublicKey = destination + self.data = data + self.ttl = ttl + self.isPing = isPing + } + + /// Construct a `LokiMessage` from a `SignalMessage`. + /// + /// - Note: `timestamp` is the original message timestamp (i.e. `TSOutgoingMessage.timestamp`). + public static func from(signalMessage: SignalMessage) -> LokiMessage? { + // To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object + do { + let wrappedMessage = try MessageWrapper.wrap(message: signalMessage) + let data = wrappedMessage.base64EncodedString() + let destination = signalMessage.recipientPublicKey + var ttl = TTLUtilities.fallbackMessageTTL + if let messageTTL = signalMessage.ttl, messageTTL > 0 { ttl = UInt64(messageTTL) } + let isPing = signalMessage.isPing + return LokiMessage(destination: destination, data: data, ttl: ttl, isPing: isPing) + } catch let error { + print("[Loki] Failed to convert Signal message to Loki message: \(signalMessage).") + return nil + } + } + + /// Calculate the proof of work for this message. + /// + /// - Returns: The promise of a new message with its `timestamp` and `nonce` set. + public func calculatePoW() -> Promise { + return Promise { seal in + DispatchQueue.global(qos: .userInitiated).async { + let now = NSDate.ows_millisecondTimeStamp() + let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented + if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.recipientPublicKey, timestamp: now, ttl: self.ttl) { + var result = self + result.timestamp = now + result.nonce = nonce + seal.fulfill(result) + } else { + seal.reject(SessionMessagingKit.MessageSender.Error.proofOfWorkCalculationFailed) + } + } + } + } + + public func toJSON() -> JSON { + var result = [ "pubKey" : recipientPublicKey, "data" : data.description, "ttl" : String(ttl) ] + if let timestamp = timestamp, let nonce = nonce { + result["timestamp"] = String(timestamp) + result["nonce"] = nonce + } + return result + } +} diff --git a/SignalUtilitiesKit/LokiPushNotificationManager.swift b/SignalUtilitiesKit/LokiPushNotificationManager.swift new file mode 100644 index 000000000..d29e1b837 --- /dev/null +++ b/SignalUtilitiesKit/LokiPushNotificationManager.swift @@ -0,0 +1,153 @@ +import PromiseKit + +@objc(LKPushNotificationManager) +public final class LokiPushNotificationManager : NSObject { + + // MARK: Settings + #if DEBUG + private static let server = "https://live.apns.getsession.org" + #else + private static let server = "https://live.apns.getsession.org" + #endif + internal static let pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" + private static let maxRetryCount: UInt = 4 + private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 + + public enum ClosedGroupOperation: String { + case subscribe = "subscribe_closed_group" + case unsubscribe = "unsubscribe_closed_group" + } + + // MARK: Initialization + private override init() { } + + // MARK: Registration + /// Unregisters the user from push notifications. Only the user's device token is needed for this. + static func unregister(with token: Data, isForcedUpdate: Bool) -> Promise { + let hexEncodedToken = token.toHexString() + let parameters = [ "token" : hexEncodedToken ] + let url = URL(string: "\(server)/unregister")! + let request = TSRequest(url: url, method: "POST", parameters: parameters) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't unregister from push notifications.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").") + } + } + } + promise.catch2 { error in + print("[Loki] Couldn't unregister from push notifications.") + } + // Unsubscribe from all closed groups + Storage.getUserClosedGroupPublicKeys().forEach { closedGroup in + performOperation(.unsubscribe, for: closedGroup, publicKey: getUserHexEncodedPublicKey()) + } + return promise + } + + /// Unregisters the user from push notifications. Only the user's device token is needed for this. + @objc(unregisterWithToken:isForcedUpdate:) + public static func objc_unregister(with token: Data, isForcedUpdate: Bool) -> AnyPromise { + return AnyPromise.from(unregister(with: token, isForcedUpdate: isForcedUpdate)) + } + + /// Registers the user for push notifications. Requires the user's device + /// token and their Session ID. + static func register(with token: Data, publicKey: String, isForcedUpdate: Bool) -> Promise { + let hexEncodedToken = token.toHexString() + let userDefaults = UserDefaults.standard + let oldToken = userDefaults[.deviceToken] + let lastUploadTime = userDefaults[.lastDeviceTokenUpload] + let now = Date().timeIntervalSince1970 + guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else { + print("[Loki] Device token hasn't changed or expired; no need to re-upload.") + return Promise { $0.fulfill(()) } + } + let parameters = [ "token" : hexEncodedToken, "pubKey" : publicKey] + let url = URL(string: "\(server)/register")! + let request = TSRequest(url: url, method: "POST", parameters: parameters) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't register device token.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't register device token due to error: \(json["message"] as? String ?? "nil").") + } + userDefaults[.deviceToken] = hexEncodedToken + userDefaults[.lastDeviceTokenUpload] = now + userDefaults[.isUsingFullAPNs] = true + } + } + promise.catch2 { error in + print("[Loki] Couldn't register device token.") + } + // Subscribe to all closed groups + Storage.getUserClosedGroupPublicKeys().forEach { closedGroup in + performOperation(.subscribe, for: closedGroup, publicKey: publicKey) + } + return promise + } + + /// Registers the user for push notifications. Requires the user's device + /// token and their Session ID. + @objc(registerWithToken:hexEncodedPublicKey:isForcedUpdate:) + public static func objc_register(with token: Data, publicKey: String, isForcedUpdate: Bool) -> AnyPromise { + return AnyPromise.from(register(with: token, publicKey: publicKey, isForcedUpdate: isForcedUpdate)) + } + + static func performOperation(_ operation: ClosedGroupOperation, for closedGroupPublicKey: String, publicKey: String) -> Promise { + let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs] + guard isUsingFullAPNs else { return Promise { $0.fulfill(()) } } + let parameters = [ "closedGroupPublicKey" : closedGroupPublicKey, "pubKey" : publicKey] + let url = URL(string: "\(server)/\(operation.rawValue)")! + let request = TSRequest(url: url, method: "POST", parameters: parameters) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").") + } + } + } + promise.catch2 { error in + print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") + } + return promise + } + + static func notify(for signalMessage: SignalMessage) -> Promise { + let message = LokiMessage.from(signalMessage: signalMessage)! + let parameters = [ "data" : message.data.description, "send_to" : message.recipientPublicKey] + let url = URL(string: "\(server)/notify")! + let request = TSRequest(url: url, method: "POST", parameters: parameters) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't notify PN server.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't notify PN server due to error: \(json["message"] as? String ?? "nil").") + } + } + } + promise.catch2 { error in + print("[Loki] Couldn't notify PN server.") + } + return promise + } + + @objc(notifyForMessage:) + public static func objc_notify(for signalMessage: SignalMessage) -> AnyPromise { + return AnyPromise.from(notify(for: signalMessage)) + } +} diff --git a/SignalUtilitiesKit/LokiSessionResetImplementation.swift b/SignalUtilitiesKit/LokiSessionResetImplementation.swift new file mode 100644 index 000000000..64569eb62 --- /dev/null +++ b/SignalUtilitiesKit/LokiSessionResetImplementation.swift @@ -0,0 +1,48 @@ +import SessionProtocolKit + +@objc(SNSessionRestorationImplementation) +public final class SessionRestorationImplementation : NSObject, SessionRestorationProtocol { + + private var storage: OWSPrimaryStorage { + return OWSPrimaryStorage.shared() + } + + enum Error : LocalizedError { + case missingPreKey + case invalidPreKeyID + } + + public func validatePreKeyWhisperMessage(for publicKey: String, preKeyWhisperMessage: PreKeyWhisperMessage, using transaction: Any) throws { + guard let transaction = transaction as? YapDatabaseReadTransaction else { return } + guard let storedPreKey = storage.getPreKeyRecord(forContact: publicKey, transaction: transaction) else { + print("[Loki] Missing pre key bundle.") + throw Error.missingPreKey + } + guard storedPreKey.id == preKeyWhisperMessage.prekeyID else { + print("[Loki] Received a PreKeyWhisperMessage from an unknown source.") + throw Error.invalidPreKeyID + } + } + + public func getSessionRestorationStatus(for publicKey: String) -> SessionRestorationStatus { + var thread: TSContactThread? + Storage.read { transaction in + thread = TSContactThread.getWithContactId(publicKey, transaction: transaction) + } + return thread?.sessionResetStatus ?? .none + } + + public func handleNewSessionAdopted(for publicKey: String, using transaction: Any) { + guard let transaction = transaction as? YapDatabaseReadWriteTransaction else { return } + guard !publicKey.isEmpty else { return } + guard let thread = TSContactThread.getWithContactId(publicKey, transaction: transaction) else { + return print("[Loki] A new session was adopted but the thread couldn't be found for: \(publicKey).") + } + // Notify the user + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetDone) + infoMessage.save(with: transaction) + // Update the session reset status + thread.sessionResetStatus = .none + thread.save(with: transaction) + } +} diff --git a/SignalUtilitiesKit/MIMETypeUtil.h b/SignalUtilitiesKit/MIMETypeUtil.h new file mode 100644 index 000000000..899eb4be8 --- /dev/null +++ b/SignalUtilitiesKit/MIMETypeUtil.h @@ -0,0 +1,68 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const OWSMimeTypeApplicationOctetStream; +extern NSString *const OWSMimeTypeApplicationZip; +extern NSString *const OWSMimeTypeImagePng; +extern NSString *const OWSMimeTypeImageJpeg; +extern NSString *const OWSMimeTypeImageGif; +extern NSString *const OWSMimeTypeImageTiff1; +extern NSString *const OWSMimeTypeImageTiff2; +extern NSString *const OWSMimeTypeImageBmp1; +extern NSString *const OWSMimeTypeImageBmp2; +extern NSString *const OWSMimeTypeOversizeTextMessage; +extern NSString *const OWSMimeTypeUnknownForTests; + +extern NSString *const kOversizeTextAttachmentUTI; +extern NSString *const kOversizeTextAttachmentFileExtension; +extern NSString *const kUnknownTestAttachmentUTI; +extern NSString *const kSyncMessageFileExtension; + +@interface MIMETypeUtil : NSObject + ++ (BOOL)isSupportedVideoMIMEType:(NSString *)contentType; ++ (BOOL)isSupportedAudioMIMEType:(NSString *)contentType; ++ (BOOL)isSupportedImageMIMEType:(NSString *)contentType; ++ (BOOL)isSupportedAnimatedMIMEType:(NSString *)contentType; + ++ (BOOL)isSupportedVideoFile:(NSString *)filePath; ++ (BOOL)isSupportedAudioFile:(NSString *)filePath; ++ (BOOL)isSupportedImageFile:(NSString *)filePath; ++ (BOOL)isSupportedAnimatedFile:(NSString *)filePath; + ++ (nullable NSString *)getSupportedExtensionFromVideoMIMEType:(NSString *)supportedMIMEType; ++ (nullable NSString *)getSupportedExtensionFromAudioMIMEType:(NSString *)supportedMIMEType; ++ (nullable NSString *)getSupportedExtensionFromImageMIMEType:(NSString *)supportedMIMEType; ++ (nullable NSString *)getSupportedExtensionFromAnimatedMIMEType:(NSString *)supportedMIMEType; + ++ (BOOL)isAnimated:(NSString *)contentType; ++ (BOOL)isImage:(NSString *)contentType; ++ (BOOL)isVideo:(NSString *)contentType; ++ (BOOL)isAudio:(NSString *)contentType; ++ (BOOL)isVisualMedia:(NSString *)contentType; + +// filename is optional and should not be trusted. ++ (nullable NSString *)filePathForAttachment:(NSString *)uniqueId + ofMIMEType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + inFolder:(NSString *)folder; + ++ (NSSet *)supportedVideoUTITypes; ++ (NSSet *)supportedAudioUTITypes; ++ (NSSet *)supportedImageUTITypes; ++ (NSSet *)supportedAnimatedImageUTITypes; + ++ (nullable NSString *)utiTypeForMIMEType:(NSString *)mimeType; ++ (nullable NSString *)utiTypeForFileExtension:(NSString *)fileExtension; ++ (nullable NSString *)fileExtensionForUTIType:(NSString *)utiType; ++ (nullable NSString *)fileExtensionForMIMEType:(NSString *)mimeType; ++ (nullable NSString *)mimeTypeForFileExtension:(NSString *)fileExtension; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/MIMETypeUtil.m b/SignalUtilitiesKit/MIMETypeUtil.m new file mode 100644 index 000000000..ecbf5f5fe --- /dev/null +++ b/SignalUtilitiesKit/MIMETypeUtil.m @@ -0,0 +1,2626 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "MIMETypeUtil.h" +#import "OWSFileSystem.h" +#import + +#if TARGET_OS_IPHONE +#import + +#else +#import + +#endif + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSMimeTypeApplicationOctetStream = @"application/octet-stream"; +NSString *const OWSMimeTypeImagePng = @"image/png"; +NSString *const OWSMimeTypeImageJpeg = @"image/jpeg"; +NSString *const OWSMimeTypeImageGif = @"image/gif"; +NSString *const OWSMimeTypeImageTiff1 = @"image/tiff"; +NSString *const OWSMimeTypeImageTiff2 = @"image/x-tiff"; +NSString *const OWSMimeTypeImageBmp1 = @"image/bmp"; +NSString *const OWSMimeTypeImageBmp2 = @"image/x-windows-bmp"; +NSString *const OWSMimeTypeOversizeTextMessage = @"text/x-signal-plain"; +NSString *const OWSMimeTypeUnknownForTests = @"unknown/mimetype"; +NSString *const OWSMimeTypeApplicationZip = @"application/zip"; + +NSString *const kOversizeTextAttachmentUTI = @"org.whispersystems.oversize-text-attachment"; +NSString *const kOversizeTextAttachmentFileExtension = @"txt"; +NSString *const kUnknownTestAttachmentUTI = @"org.whispersystems.unknown"; +NSString *const kSyncMessageFileExtension = @"bin"; + +@implementation MIMETypeUtil + ++ (NSDictionary *)supportedVideoMIMETypesToExtensionTypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"video/3gpp" : @"3gp", + @"video/3gpp2" : @"3g2", + @"video/mp4" : @"mp4", + @"video/quicktime" : @"mov", + @"video/x-m4v" : @"m4v", + @"video/mpeg" : @"mpg", + }; + }); + return result; +} + ++ (NSDictionary *)supportedAudioMIMETypesToExtensionTypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"audio/aac" : @"m4a", + @"audio/x-m4p" : @"m4p", + @"audio/x-m4b" : @"m4b", + @"audio/x-m4a" : @"m4a", + @"audio/wav" : @"wav", + @"audio/x-wav" : @"wav", + @"audio/x-mpeg" : @"mp3", + @"audio/mpeg" : @"mp3", + @"audio/mp4" : @"mp4", + @"audio/mp3" : @"mp3", + @"audio/mpeg3" : @"mp3", + @"audio/x-mp3" : @"mp3", + @"audio/x-mpeg3" : @"mp3", + @"audio/aiff" : @"aiff", + @"audio/x-aiff" : @"aiff", + @"audio/3gpp2" : @"3g2", + @"audio/3gpp" : @"3gp", + }; + }); + return result; +} + ++ (NSDictionary *)supportedImageMIMETypesToExtensionTypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + OWSMimeTypeImageJpeg : @"jpeg", + @"image/pjpeg" : @"jpeg", + OWSMimeTypeImagePng : @"png", + @"image/tiff" : @"tif", + @"image/x-tiff" : @"tif", + @"image/bmp" : @"bmp", + @"image/x-windows-bmp" : @"bmp", + @"image/gif" : @"gif" + }; + }); + return result; +} + ++ (NSDictionary *)supportedAnimatedMIMETypesToExtensionTypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + OWSMimeTypeImageGif : @"gif", + }; + }); + return result; +} + ++ (NSDictionary *)supportedBinaryDataMIMETypesToExtensionTypes +{ + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + OWSMimeTypeApplicationOctetStream : @"dat", + }; + }); + return result; +} + ++ (NSDictionary *)supportedVideoExtensionTypesToMIMETypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"3gp" : @"video/3gpp", + @"3gpp" : @"video/3gpp", + @"3gp2" : @"video/3gpp2", + @"3gpp2" : @"video/3gpp2", + @"mp4" : @"video/mp4", + @"mov" : @"video/quicktime", + @"mqv" : @"video/quicktime", + @"m4v" : @"video/x-m4v", + @"mpg" : @"video/mpeg", + @"mpeg" : @"video/mpeg", + }; + }); + return result; +} + ++ (NSDictionary *)supportedAudioExtensionTypesToMIMETypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"3gp" : @"audio/3gpp", + @"3gpp" : @"@audio/3gpp", + @"3g2" : @"audio/3gpp2", + @"3gp2" : @"audio/3gpp2", + @"aiff" : @"audio/aiff", + @"aif" : @"audio/aiff", + @"aifc" : @"audio/aiff", + @"cdda" : @"audio/aiff", + @"mp3" : @"audio/mp3", + @"swa" : @"audio/mp3", + @"mp4" : @"audio/mp4", + @"wav" : @"audio/wav", + @"bwf" : @"audio/wav", + @"m4a" : @"audio/x-m4a", + @"m4b" : @"audio/x-m4b", + @"m4p" : @"audio/x-m4p" + }; + }); + return result; +} + ++ (NSDictionary *)supportedImageExtensionTypesToMIMETypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"png" : OWSMimeTypeImagePng, + @"x-png" : OWSMimeTypeImagePng, + @"jfif" : @"image/jpeg", + @"jfif" : @"image/pjpeg", + @"jfif-tbnl" : @"image/jpeg", + @"jpe" : @"image/jpeg", + @"jpe" : @"image/pjpeg", + @"jpeg" : @"image/jpeg", + @"jpg" : @"image/jpeg", + @"tif" : @"image/tiff", + @"tiff" : @"image/tiff" + }; + }); + return result; +} + ++ (NSDictionary *)supportedAnimatedExtensionTypesToMIMETypes { + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"gif" : OWSMimeTypeImageGif, + }; + }); + return result; +} + ++ (BOOL)isSupportedVideoMIMEType:(NSString *)contentType { + return [[self supportedVideoMIMETypesToExtensionTypes] objectForKey:contentType] != nil; +} + ++ (BOOL)isSupportedAudioMIMEType:(NSString *)contentType { + return [[self supportedAudioMIMETypesToExtensionTypes] objectForKey:contentType] != nil; +} + ++ (BOOL)isSupportedImageMIMEType:(NSString *)contentType { + return [[self supportedImageMIMETypesToExtensionTypes] objectForKey:contentType] != nil; +} + ++ (BOOL)isSupportedAnimatedMIMEType:(NSString *)contentType { + return [[self supportedAnimatedMIMETypesToExtensionTypes] objectForKey:contentType] != nil; +} + ++ (BOOL)isSupportedBinaryDataMIMEType:(NSString *)contentType +{ + return [[self supportedBinaryDataMIMETypesToExtensionTypes] objectForKey:contentType] != nil; +} + ++ (BOOL)isSupportedVideoFile:(NSString *)filePath { + return [[self supportedVideoExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; +} + ++ (BOOL)isSupportedAudioFile:(NSString *)filePath { + return [[self supportedAudioExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; +} + ++ (BOOL)isSupportedImageFile:(NSString *)filePath { + return [[self supportedImageExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; +} + ++ (BOOL)isSupportedAnimatedFile:(NSString *)filePath { + return + [[self supportedAnimatedExtensionTypesToMIMETypes] objectForKey:filePath.pathExtension.lowercaseString] != nil; +} + ++ (nullable NSString *)getSupportedExtensionFromVideoMIMEType:(NSString *)supportedMIMEType +{ + return [[self supportedVideoMIMETypesToExtensionTypes] objectForKey:supportedMIMEType]; +} + ++ (nullable NSString *)getSupportedExtensionFromAudioMIMEType:(NSString *)supportedMIMEType +{ + return [[self supportedAudioMIMETypesToExtensionTypes] objectForKey:supportedMIMEType]; +} + ++ (nullable NSString *)getSupportedExtensionFromImageMIMEType:(NSString *)supportedMIMEType +{ + return [[self supportedImageMIMETypesToExtensionTypes] objectForKey:supportedMIMEType]; +} + ++ (nullable NSString *)getSupportedExtensionFromAnimatedMIMEType:(NSString *)supportedMIMEType +{ + return [[self supportedAnimatedMIMETypesToExtensionTypes] objectForKey:supportedMIMEType]; +} + ++ (nullable NSString *)getSupportedExtensionFromBinaryDataMIMEType:(NSString *)supportedMIMEType +{ + return [[self supportedBinaryDataMIMETypesToExtensionTypes] objectForKey:supportedMIMEType]; +} + +#pragma mark full attachment utilities ++ (BOOL)isAnimated:(NSString *)contentType { + return [MIMETypeUtil isSupportedAnimatedMIMEType:contentType]; +} + ++ (BOOL)isBinaryData:(NSString *)contentType +{ + return [MIMETypeUtil isSupportedBinaryDataMIMEType:contentType]; +} + ++ (BOOL)isImage:(NSString *)contentType { + return [MIMETypeUtil isSupportedImageMIMEType:contentType]; +} + ++ (BOOL)isVideo:(NSString *)contentType { + return [MIMETypeUtil isSupportedVideoMIMEType:contentType]; +} + ++ (BOOL)isAudio:(NSString *)contentType { + return [MIMETypeUtil isSupportedAudioMIMEType:contentType]; +} + ++ (BOOL)isVisualMedia:(NSString *)contentType +{ + if ([self isImage:contentType]) { + return YES; + } + + if ([self isVideo:contentType]) { + return YES; + } + + if ([self isAnimated:contentType]) { + return YES; + } + + return NO; +} + ++ (nullable NSString *)filePathForAttachment:(NSString *)uniqueId + ofMIMEType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + inFolder:(NSString *)folder +{ + NSString *kDefaultFileExtension = @"bin"; + + if (sourceFilename.length > 0) { + NSString *normalizedFilename = + [sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + // Ensure that the filename is a valid filesystem name, + // replacing invalid characters with an underscore. + for (NSCharacterSet *invalidCharacterSet in @[ + [NSCharacterSet whitespaceAndNewlineCharacterSet], + [NSCharacterSet illegalCharacterSet], + [NSCharacterSet controlCharacterSet], + [NSCharacterSet characterSetWithCharactersInString:@"<>|\\:()&;?*/~"], + ]) { + normalizedFilename = [[normalizedFilename componentsSeparatedByCharactersInSet:invalidCharacterSet] + componentsJoinedByString:@"_"]; + } + + // Remove leading periods to prevent hidden files, + // "." and ".." special file names. + while ([normalizedFilename hasPrefix:@"."]) { + normalizedFilename = [normalizedFilename substringFromIndex:1]; + } + + NSString *fileExtension = [[normalizedFilename pathExtension] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *filenameWithoutExtension = [[[normalizedFilename lastPathComponent] stringByDeletingPathExtension] + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + // If the filename has not file extension, deduce one + // from the MIME type. + if (fileExtension.length < 1) { + fileExtension = [self fileExtensionForMIMEType:contentType]; + if (fileExtension.length < 1) { + fileExtension = kDefaultFileExtension; + } + } + fileExtension = [fileExtension lowercaseString]; + + if (filenameWithoutExtension.length > 0) { + // Store the file in a subdirectory whose name is the uniqueId of this attachment, + // to avoid collisions between multiple attachments with the same name. + NSString *attachmentFolderPath = [folder stringByAppendingPathComponent:uniqueId]; + if (![OWSFileSystem ensureDirectoryExists:attachmentFolderPath]) { + return nil; + } + return [attachmentFolderPath + stringByAppendingPathComponent:[NSString + stringWithFormat:@"%@.%@", filenameWithoutExtension, fileExtension]]; + } + } + + if ([self isVideo:contentType]) { + return [MIMETypeUtil filePathForVideo:uniqueId ofMIMEType:contentType inFolder:folder]; + } else if ([self isAudio:contentType]) { + return [MIMETypeUtil filePathForAudio:uniqueId ofMIMEType:contentType inFolder:folder]; + } else if ([self isImage:contentType]) { + return [MIMETypeUtil filePathForImage:uniqueId ofMIMEType:contentType inFolder:folder]; + } else if ([self isAnimated:contentType]) { + return [MIMETypeUtil filePathForAnimated:uniqueId ofMIMEType:contentType inFolder:folder]; + } else if ([self isBinaryData:contentType]) { + return [MIMETypeUtil filePathForBinaryData:uniqueId ofMIMEType:contentType inFolder:folder]; + } else if ([contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) { + // We need to use a ".txt" file extension since this file extension is used + // by UIActivityViewController to determine which kinds of sharing are + // appropriate for this text. + // be used outside the app. + return [self filePathForData:uniqueId withFileExtension:@"txt" inFolder:folder]; + } else if ([contentType isEqualToString:OWSMimeTypeUnknownForTests]) { + // This file extension is arbitrary - it should never be exposed to the user or + // be used outside the app. + return [self filePathForData:uniqueId withFileExtension:@"unknown" inFolder:folder]; + } + + NSString *fileExtension = [self fileExtensionForMIMEType:contentType]; + if (fileExtension) { + return [self filePathForData:uniqueId withFileExtension:fileExtension inFolder:folder]; + } + + OWSLogError(@"Got asked for path of file %@ which is unsupported", contentType); + // Use a fallback file extension. + return [self filePathForData:uniqueId withFileExtension:kDefaultFileExtension inFolder:folder]; +} + ++ (NSString *)filePathForImage:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromImageMIMEType:contentType] + inFolder:folder]; +} + ++ (NSString *)filePathForVideo:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromVideoMIMEType:contentType] + inFolder:folder]; +} + ++ (NSString *)filePathForAudio:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromAudioMIMEType:contentType] + inFolder:folder]; +} + ++ (NSString *)filePathForAnimated:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromAnimatedMIMEType:contentType] + inFolder:folder]; +} + ++ (NSString *)filePathForBinaryData:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder +{ + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromBinaryDataMIMEType:contentType] + inFolder:folder]; +} + ++ (NSString *)filePathForData:(NSString *)uniqueId + withFileExtension:(NSString *)fileExtension + inFolder:(NSString *)folder +{ + return [folder stringByAppendingPathComponent:[uniqueId stringByAppendingPathExtension:fileExtension]]; +} + ++ (nullable NSString *)utiTypeForMIMEType:(NSString *)mimeType +{ + NSString *utiType = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag( + kUTTagClassMIMEType, (__bridge CFStringRef)mimeType, NULL); + + if (!utiType) { + if ([mimeType isEqualToString:@"audio/amr"]) { + utiType = @"org.3gpp.adaptive-multi-rate-audio"; + } else if ([mimeType isEqualToString:@"audio/mp3"] || [mimeType isEqualToString:@"audio/x-mpeg"] || + [mimeType isEqualToString:@"audio/mpeg"] || [mimeType isEqualToString:@"audio/mpeg3"] || + [mimeType isEqualToString:@"audio/x-mp3"] || [mimeType isEqualToString:@"audio/x-mpeg3"]) { + utiType = (NSString *)kUTTypeMP3; + } else if ([mimeType isEqualToString:@"audio/aac"] || [mimeType isEqualToString:@"audio/x-m4a"]) { + utiType = (NSString *)kUTTypeMPEG4Audio; + } else if ([mimeType isEqualToString:@"audio/aiff"] || [mimeType isEqualToString:@"audio/x-aiff"]) { + utiType = (NSString *)kUTTypeAudioInterchangeFileFormat; + } + } + + return utiType; +} + ++ (nullable NSString *)fileExtensionForUTIType:(NSString *)utiType +{ + // Special-case the "aac" filetype we use for voice messages (for legacy reasons) + // to use a .m4a file extension, not .aac, since AVAudioPlayer can't handle .aac + // properly. Doesn't affect file contents. + if ([utiType isEqualToString:@"public.aac-audio"]) { + return @"m4a"; + } + CFStringRef fileExtension + = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)utiType, kUTTagClassFilenameExtension); + return (__bridge_transfer NSString *)fileExtension; +} + ++ (nullable NSString *)fileExtensionForMIMETypeViaUTIType:(NSString *)mimeType +{ + NSString *utiType = [self utiTypeForMIMEType:mimeType]; + if (!utiType) { + return nil; + } + NSString *fileExtension = [self fileExtensionForUTIType:utiType]; + return fileExtension; +} + ++ (NSSet *)utiTypesForMIMETypes:(NSArray *)mimeTypes +{ + NSMutableSet *result = [NSMutableSet new]; + for (NSString *mimeType in mimeTypes) { + NSString *_Nullable utiType = [self utiTypeForMIMEType:mimeType]; + if (!utiType) { + OWSFailDebug(@"unknown utiType for mimetype: %@", mimeType); + continue; + } + [result addObject:utiType]; + } + return result; +} + ++ (NSSet *)supportedVideoUTITypes +{ + static NSSet *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = [self utiTypesForMIMETypes:[self supportedVideoMIMETypesToExtensionTypes].allKeys]; + }); + return result; +} + ++ (NSSet *)supportedAudioUTITypes +{ + static NSSet *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = [self utiTypesForMIMETypes:[self supportedAudioMIMETypesToExtensionTypes].allKeys]; + }); + return result; +} + ++ (NSSet *)supportedImageUTITypes +{ + static NSSet *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = [self utiTypesForMIMETypes:[self supportedImageMIMETypesToExtensionTypes].allKeys]; + }); + return result; +} + ++ (NSSet *)supportedAnimatedImageUTITypes +{ + static NSSet *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = [self utiTypesForMIMETypes:[self supportedAnimatedMIMETypesToExtensionTypes].allKeys]; + }); + return result; +} + ++ (NSDictionary *)genericMIMETypesToExtensionTypes +{ + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"application/acad" : @"dwg", + @"application/andrew-inset" : @"ez", + @"application/applixware" : @"aw", + @"application/arj" : @"arj", + @"application/atom+xml" : @"atom", + @"application/atomcat+xml" : @"atomcat", + @"application/atomsvc+xml" : @"atomsvc", + @"application/binhex" : @"hqx", + @"application/binhex4" : @"hqx", + @"application/book" : @"book", + @"application/ccxml+xml" : @"ccxml", + @"application/cdf" : @"cdf", + @"application/cdmi-capability" : @"cdmia", + @"application/cdmi-container" : @"cdmic", + @"application/cdmi-domain" : @"cdmid", + @"application/cdmi-object" : @"cdmio", + @"application/cdmi-queue" : @"cdmiq", + @"application/clariscad" : @"ccad", + @"application/commonground" : @"dp", + @"application/cu-seeme" : @"cu", + @"application/davmount+xml" : @"davmount", + @"application/docbook+xml" : @"dbk", + @"application/drafting" : @"drw", + @"application/dsptype" : @"tsp", + @"application/dssc+der" : @"dssc", + @"application/dssc+xml" : @"xdssc", + @"application/dxf" : @"dxf", + @"application/ecmascript" : @"js", + @"application/emma+xml" : @"emma", + @"application/envoy" : @"evy", + @"application/epub+zip" : @"epub", + @"application/excel" : @"xls", + @"application/exi" : @"exi", + @"application/font-tdpfr" : @"pfr", + @"application/font-woff" : @"woff", + @"application/fractals" : @"fif", + @"application/freeloader" : @"frl", + @"application/futuresplash" : @"spl", + @"application/gml+xml" : @"gml", + @"application/gnutar" : @"tgz", + @"application/gpx+xml" : @"gpx", + @"application/groupwise" : @"vew", + @"application/gxf" : @"gxf", + @"application/hlp" : @"hlp", + @"application/hta" : @"hta", + @"application/hyperstudio" : @"stk", + @"application/i-deas" : @"unv", + @"application/iges" : @"iges", + @"application/inf" : @"inf", + @"application/inkml+xml" : @"ink", + @"application/internet-property-stream" : @"acx", + @"application/ipfix" : @"ipfix", + @"application/java" : @"class", + @"application/java-archive" : @"jar", + @"application/java-byte-code" : @"class", + @"application/java-serialized-object" : @"ser", + @"application/java-vm" : @"class", + @"application/javascript" : @"js", + @"application/json" : @"json", + @"application/jsonml+json" : @"jsonml", + @"application/lha" : @"lha", + @"application/lost+xml" : @"lostxml", + @"application/lzx" : @"lzx", + @"application/mac-binary" : @"bin", + @"application/mac-binhex" : @"hqx", + @"application/mac-binhex40" : @"hqx", + @"application/mac-compactpro" : @"cpt", + @"application/macbinary" : @"bin", + @"application/mads+xml" : @"mads", + @"application/marc" : @"mrc", + @"application/marcxml+xml" : @"mrcx", + @"application/mathematica" : @"ma", + @"application/mathml+xml" : @"mathml", + @"application/mbedlet" : @"mbd", + @"application/mbox" : @"mbox", + @"application/mcad" : @"mcd", + @"application/mediaservercontrol+xml" : @"mscml", + @"application/metalink+xml" : @"metalink", + @"application/metalink4+xml" : @"meta4", + @"application/mets+xml" : @"mets", + @"application/mime" : @"aps", + @"application/mods+xml" : @"mods", + @"application/mp21" : @"m21", + @"application/mp4" : @"mp4", + @"application/mspowerpoint" : @"ppt", + @"application/msword" : @"doc", + @"application/mswrite" : @"wri", + @"application/mxf" : @"mxf", + @"application/netmc" : @"mcp", + @"application/octet-stream" : @"bin", + @"application/oda" : @"oda", + @"application/oebps-package+xml" : @"opf", + @"application/ogg" : @"oga", + @"application/olescript" : @"axs", + @"application/omdoc+xml" : @"omdoc", + @"application/onenote" : @"onetoc", + @"application/oxps" : @"oxps", + @"application/patch-ops-error+xml" : @"xer", + @"application/pdf" : @"pdf", + @"application/pgp-encrypted" : @"pgp", + @"application/pgp-signature" : @"sig", + @"application/pics-rules" : @"prf", + @"application/pkcs-12" : @"p12", + @"application/pkcs-crl" : @"crl", + @"application/pkcs10" : @"p10", + @"application/pkcs7-mime" : @"p7m", + @"application/pkcs7-signature" : @"p7s", + @"application/pkcs8" : @"p8", + @"application/pkix-attr-cert" : @"ac", + @"application/pkix-cert" : @"cer", + @"application/pkix-crl" : @"crl", + @"application/pkix-pkipath" : @"pkipath", + @"application/pkixcmp" : @"pki", + @"application/plain" : @"text", + @"application/pls+xml" : @"pls", + @"application/postscript" : @"ps", + @"application/powerpoint" : @"ppt", + @"application/prs.cww" : @"cww", + @"application/pskc+xml" : @"pskcxml", + @"application/rdf+xml" : @"rdf", + @"application/reginfo+xml" : @"rif", + @"application/relax-ng-compact-syntax" : @"rnc", + @"application/resource-lists+xml" : @"rl", + @"application/resource-lists-diff+xml" : @"rld", + @"application/ringing-tones" : @"rng", + @"application/rls-services+xml" : @"rs", + @"application/rpki-ghostbusters" : @"gbr", + @"application/rpki-manifest" : @"mft", + @"application/rpki-roa" : @"roa", + @"application/rsd+xml" : @"rsd", + @"application/rss+xml" : @"rss", + @"application/rtf" : @"rtf", + @"application/sbml+xml" : @"sbml", + @"application/scvp-cv-request" : @"scq", + @"application/scvp-cv-response" : @"scs", + @"application/scvp-vp-request" : @"spq", + @"application/scvp-vp-response" : @"spp", + @"application/sdp" : @"sdp", + @"application/sea" : @"sea", + @"application/set" : @"set", + @"application/set-payment-initiation" : @"setpay", + @"application/set-registration-initiation" : @"setreg", + @"application/shf+xml" : @"shf", + @"application/sla" : @"stl", + @"application/smil" : @"smi", + @"application/smil+xml" : @"smi", + @"application/solids" : @"sol", + @"application/sounder" : @"sdr", + @"application/sparql-query" : @"rq", + @"application/sparql-results+xml" : @"srx", + @"application/srgs" : @"gram", + @"application/srgs+xml" : @"grxml", + @"application/sru+xml" : @"sru", + @"application/ssdl+xml" : @"ssdl", + @"application/ssml+xml" : @"ssml", + @"application/step" : @"step", + @"application/streamingmedia" : @"ssm", + @"application/tei+xml" : @"tei", + @"application/thraud+xml" : @"tfi", + @"application/timestamped-data" : @"tsd", + @"application/toolbook" : @"tbk", + @"application/vda" : @"vda", + @"application/vnd.3gpp.pic-bw-large" : @"plb", + @"application/vnd.3gpp.pic-bw-small" : @"psb", + @"application/vnd.3gpp.pic-bw-var" : @"pvb", + @"application/vnd.3gpp2.tcap" : @"tcap", + @"application/vnd.3m.post-it-notes" : @"pwn", + @"application/vnd.accpac.simply.aso" : @"aso", + @"application/vnd.accpac.simply.imp" : @"imp", + @"application/vnd.acucobol" : @"acu", + @"application/vnd.acucorp" : @"atc", + @"application/vnd.adobe.air-application-installer-package+zip" : @"air", + @"application/vnd.adobe.formscentral.fcdt" : @"fcdt", + @"application/vnd.adobe.fxp" : @"fxp", + @"application/vnd.adobe.xdp+xml" : @"xdp", + @"application/vnd.adobe.xfdf" : @"xfdf", + @"application/vnd.ahead.space" : @"ahead", + @"application/vnd.airzip.filesecure.azf" : @"azf", + @"application/vnd.airzip.filesecure.azs" : @"azs", + @"application/vnd.amazon.ebook" : @"azw", + @"application/vnd.americandynamics.acc" : @"acc", + @"application/vnd.amiga.ami" : @"ami", + @"application/vnd.android.package-archive" : @"apk", + @"application/vnd.anser-web-certificate-issue-initiation" : @"cii", + @"application/vnd.anser-web-funds-transfer-initiation" : @"fti", + @"application/vnd.antix.game-component" : @"atx", + @"application/vnd.apple.installer+xml" : @"mpkg", + @"application/vnd.apple.mpegurl" : @"m3u8", + @"application/vnd.aristanetworks.swi" : @"swi", + @"application/vnd.astraea-software.iota" : @"iota", + @"application/vnd.audiograph" : @"aep", + @"application/vnd.blueice.multipass" : @"mpm", + @"application/vnd.bmi" : @"bmi", + @"application/vnd.businessobjects" : @"rep", + @"application/vnd.chemdraw+xml" : @"cdxml", + @"application/vnd.chipnuts.karaoke-mmd" : @"mmd", + @"application/vnd.cinderella" : @"cdy", + @"application/vnd.claymore" : @"cla", + @"application/vnd.cloanto.rp9" : @"rp9", + @"application/vnd.clonk.c4group" : @"c4g", + @"application/vnd.cluetrust.cartomobile-config" : @"c11amc", + @"application/vnd.cluetrust.cartomobile-config-pkg" : @"c11amz", + @"application/vnd.commonspace" : @"csp", + @"application/vnd.contact.cmsg" : @"cdbcmsg", + @"application/vnd.cosmocaller" : @"cmc", + @"application/vnd.crick.clicker" : @"clkx", + @"application/vnd.crick.clicker.keyboard" : @"clkk", + @"application/vnd.crick.clicker.palette" : @"clkp", + @"application/vnd.crick.clicker.template" : @"clkt", + @"application/vnd.crick.clicker.wordbank" : @"clkw", + @"application/vnd.criticaltools.wbs+xml" : @"wbs", + @"application/vnd.ctc-posml" : @"pml", + @"application/vnd.cups-ppd" : @"ppd", + @"application/vnd.curl.car" : @"car", + @"application/vnd.curl.pcurl" : @"pcurl", + @"application/vnd.dart" : @"dart", + @"application/vnd.data-vision.rdz" : @"rdz", + @"application/vnd.dece.data" : @"uvf", + @"application/vnd.dece.ttml+xml" : @"uvt", + @"application/vnd.dece.unspecified" : @"uvx", + @"application/vnd.dece.zip" : @"uvz", + @"application/vnd.denovo.fcselayout-link" : @"fe_launch", + @"application/vnd.dna" : @"dna", + @"application/vnd.dolby.mlp" : @"mlp", + @"application/vnd.dpgraph" : @"dpg", + @"application/vnd.dreamfactory" : @"dfac", + @"application/vnd.ds-keypoint" : @"kpxx", + @"application/vnd.dvb.ait" : @"ait", + @"application/vnd.dvb.service" : @"svc", + @"application/vnd.dynageo" : @"geo", + @"application/vnd.ecowin.chart" : @"mag", + @"application/vnd.enliven" : @"nml", + @"application/vnd.epson.esf" : @"esf", + @"application/vnd.epson.msf" : @"msf", + @"application/vnd.epson.quickanime" : @"qam", + @"application/vnd.epson.salt" : @"slt", + @"application/vnd.epson.ssf" : @"ssf", + @"application/vnd.eszigno3+xml" : @"es3", + @"application/vnd.ezpix-album" : @"ez2", + @"application/vnd.ezpix-package" : @"ez3", + @"application/vnd.fdf" : @"fdf", + @"application/vnd.fdsn.mseed" : @"mseed", + @"application/vnd.fdsn.seed" : @"seed", + @"application/vnd.flographit" : @"gph", + @"application/vnd.fluxtime.clip" : @"ftc", + @"application/vnd.framemaker" : @"fm", + @"application/vnd.frogans.fnc" : @"fnc", + @"application/vnd.frogans.ltf" : @"ltf", + @"application/vnd.fsc.weblaunch" : @"fsc", + @"application/vnd.fujitsu.oasys" : @"oas", + @"application/vnd.fujitsu.oasys2" : @"oa2", + @"application/vnd.fujitsu.oasys3" : @"oa3", + @"application/vnd.fujitsu.oasysgp" : @"fg5", + @"application/vnd.fujitsu.oasysprs" : @"bh2", + @"application/vnd.fujixerox.ddd" : @"ddd", + @"application/vnd.fujixerox.docuworks" : @"xdw", + @"application/vnd.fujixerox.docuworks.binder" : @"xbd", + @"application/vnd.fuzzysheet" : @"fzs", + @"application/vnd.genomatix.tuxedo" : @"txd", + @"application/vnd.geogebra.file" : @"ggb", + @"application/vnd.geogebra.tool" : @"ggt", + @"application/vnd.geometry-explorer" : @"gex", + @"application/vnd.geonext" : @"gxt", + @"application/vnd.geoplan" : @"g2w", + @"application/vnd.geospace" : @"g3w", + @"application/vnd.gmx" : @"gmx", + @"application/vnd.google-earth.kml+xml" : @"kml", + @"application/vnd.google-earth.kmz" : @"kmz", + @"application/vnd.grafeq" : @"gqf", + @"application/vnd.groove-account" : @"gac", + @"application/vnd.groove-help" : @"ghf", + @"application/vnd.groove-identity-message" : @"gim", + @"application/vnd.groove-injector" : @"grv", + @"application/vnd.groove-tool-message" : @"gtm", + @"application/vnd.groove-tool-template" : @"tpl", + @"application/vnd.groove-vcard" : @"vcg", + @"application/vnd.hal+xml" : @"hal", + @"application/vnd.handheld-entertainment+xml" : @"zmm", + @"application/vnd.hbci" : @"hbci", + @"application/vnd.hhe.lesson-player" : @"les", + @"application/vnd.hp-hpgl" : @"hpgl", + @"application/vnd.hp-hpid" : @"hpid", + @"application/vnd.hp-hps" : @"hps", + @"application/vnd.hp-jlyt" : @"jlt", + @"application/vnd.hp-pcl" : @"pcl", + @"application/vnd.hp-pclxl" : @"pclxl", + @"application/vnd.hydrostatix.sof-data" : @"sfd-hdstx", + @"application/vnd.ibm.minipay" : @"mpy", + @"application/vnd.ibm.modcap" : @"afp", + @"application/vnd.ibm.rights-management" : @"irm", + @"application/vnd.ibm.secure-container" : @"sc", + @"application/vnd.iccprofile" : @"icc", + @"application/vnd.igloader" : @"igl", + @"application/vnd.immervision-ivp" : @"ivp", + @"application/vnd.immervision-ivu" : @"ivu", + @"application/vnd.insors.igm" : @"igm", + @"application/vnd.intercon.formnet" : @"xpw", + @"application/vnd.intergeo" : @"i2g", + @"application/vnd.intu.qbo" : @"qbo", + @"application/vnd.intu.qfx" : @"qfx", + @"application/vnd.ipunplugged.rcprofile" : @"rcprofile", + @"application/vnd.irepository.package+xml" : @"irp", + @"application/vnd.is-xpr" : @"xpr", + @"application/vnd.isac.fcs" : @"fcs", + @"application/vnd.jam" : @"jam", + @"application/vnd.jcp.javame.midlet-rms" : @"rms", + @"application/vnd.jisp" : @"jisp", + @"application/vnd.joost.joda-archive" : @"joda", + @"application/vnd.kahootz" : @"ktz", + @"application/vnd.kde.karbon" : @"karbon", + @"application/vnd.kde.kchart" : @"chrt", + @"application/vnd.kde.kformula" : @"kfo", + @"application/vnd.kde.kivio" : @"flw", + @"application/vnd.kde.kontour" : @"kon", + @"application/vnd.kde.kpresenter" : @"kpr", + @"application/vnd.kde.kspread" : @"ksp", + @"application/vnd.kde.kword" : @"kwd", + @"application/vnd.kenameaapp" : @"htke", + @"application/vnd.kidspiration" : @"kia", + @"application/vnd.kinar" : @"kne", + @"application/vnd.koan" : @"skp", + @"application/vnd.kodak-descriptor" : @"sse", + @"application/vnd.las.las+xml" : @"lasxml", + @"application/vnd.llamagraphics.life-balance.desktop" : @"lbd", + @"application/vnd.llamagraphics.life-balance.exchange+xml" : @"lbe", + @"application/vnd.lotus-1-2-3" : @"123", + @"application/vnd.lotus-approach" : @"apr", + @"application/vnd.lotus-freelance" : @"pre", + @"application/vnd.lotus-notes" : @"nsf", + @"application/vnd.lotus-organizer" : @"org", + @"application/vnd.lotus-screencam" : @"scm", + @"application/vnd.lotus-wordpro" : @"lwp", + @"application/vnd.macports.portpkg" : @"portpkg", + @"application/vnd.mcd" : @"mcd", + @"application/vnd.medcalcdata" : @"mc1", + @"application/vnd.mediastation.cdkey" : @"cdkey", + @"application/vnd.mfer" : @"mwf", + @"application/vnd.mfmp" : @"mfm", + @"application/vnd.micrografx.flo" : @"flo", + @"application/vnd.micrografx.igx" : @"igx", + @"application/vnd.mif" : @"mif", + @"application/vnd.mobius.daf" : @"daf", + @"application/vnd.mobius.dis" : @"dis", + @"application/vnd.mobius.mbk" : @"mbk", + @"application/vnd.mobius.mqy" : @"mqy", + @"application/vnd.mobius.msl" : @"msl", + @"application/vnd.mobius.plc" : @"plc", + @"application/vnd.mobius.txf" : @"txf", + @"application/vnd.mophun.application" : @"mpn", + @"application/vnd.mophun.certificate" : @"mpc", + @"application/vnd.mozilla.xul+xml" : @"xul", + @"application/vnd.ms-artgalry" : @"cil", + @"application/vnd.ms-cab-compressed" : @"cab", + @"application/vnd.ms-excel" : @"xls", + @"application/vnd.ms-excel.addin.macroenabled.12" : @"xlam", + @"application/vnd.ms-excel.sheet.binary.macroenabled.12" : @"xlsb", + @"application/vnd.ms-excel.sheet.macroenabled.12" : @"xlsm", + @"application/vnd.ms-excel.template.macroenabled.12" : @"xltm", + @"application/vnd.ms-fontobject" : @"eot", + @"application/vnd.ms-htmlhelp" : @"chm", + @"application/vnd.ms-ims" : @"ims", + @"application/vnd.ms-lrm" : @"lrm", + @"application/vnd.ms-officetheme" : @"thmx", + @"application/vnd.ms-outlook" : @"msg", + @"application/vnd.ms-pki.certstore" : @"sst", + @"application/vnd.ms-pki.pko" : @"pko", + @"application/vnd.ms-pki.seccat" : @"cat", + @"application/vnd.ms-pki.stl" : @"stl", + @"application/vnd.ms-pkicertstore" : @"sst", + @"application/vnd.ms-pkiseccat" : @"cat", + @"application/vnd.ms-pkistl" : @"stl", + @"application/vnd.ms-powerpoint" : @"ppt", + @"application/vnd.ms-powerpoint.addin.macroenabled.12" : @"ppam", + @"application/vnd.ms-powerpoint.presentation.macroenabled.12" : @"pptm", + @"application/vnd.ms-powerpoint.slide.macroenabled.12" : @"sldm", + @"application/vnd.ms-powerpoint.slideshow.macroenabled.12" : @"ppsm", + @"application/vnd.ms-powerpoint.template.macroenabled.12" : @"potm", + @"application/vnd.ms-project" : @"mpp", + @"application/vnd.ms-word.document.macroenabled.12" : @"docm", + @"application/vnd.ms-word.template.macroenabled.12" : @"dotm", + @"application/vnd.ms-works" : @"wps", + @"application/vnd.ms-wpl" : @"wpl", + @"application/vnd.ms-xpsdocument" : @"xps", + @"application/vnd.mseq" : @"mseq", + @"application/vnd.musician" : @"mus", + @"application/vnd.muvee.style" : @"msty", + @"application/vnd.mynfc" : @"taglet", + @"application/vnd.neurolanguage.nlu" : @"nlu", + @"application/vnd.nitf" : @"ntf", + @"application/vnd.noblenet-directory" : @"nnd", + @"application/vnd.noblenet-sealer" : @"nns", + @"application/vnd.noblenet-web" : @"nnw", + @"application/vnd.nokia.configuration-message" : @"ncm", + @"application/vnd.nokia.n-gage.data" : @"ngdat", + @"application/vnd.nokia.n-gage.symbian.install" : @"n-gage", + @"application/vnd.nokia.radio-preset" : @"rpst", + @"application/vnd.nokia.radio-presets" : @"rpss", + @"application/vnd.nokia.ringing-tone" : @"rng", + @"application/vnd.novadigm.edm" : @"edm", + @"application/vnd.novadigm.edx" : @"edx", + @"application/vnd.novadigm.ext" : @"ext", + @"application/vnd.oasis.opendocument.chart" : @"odc", + @"application/vnd.oasis.opendocument.chart-template" : @"otc", + @"application/vnd.oasis.opendocument.database" : @"odb", + @"application/vnd.oasis.opendocument.formula" : @"odf", + @"application/vnd.oasis.opendocument.formula-template" : @"odft", + @"application/vnd.oasis.opendocument.graphics" : @"odg", + @"application/vnd.oasis.opendocument.graphics-template" : @"otg", + @"application/vnd.oasis.opendocument.image" : @"odi", + @"application/vnd.oasis.opendocument.image-template" : @"oti", + @"application/vnd.oasis.opendocument.presentation" : @"odp", + @"application/vnd.oasis.opendocument.presentation-template" : @"otp", + @"application/vnd.oasis.opendocument.spreadsheet" : @"ods", + @"application/vnd.oasis.opendocument.spreadsheet-template" : @"ots", + @"application/vnd.oasis.opendocument.text" : @"odt", + @"application/vnd.oasis.opendocument.text-master" : @"odm", + @"application/vnd.oasis.opendocument.text-template" : @"ott", + @"application/vnd.oasis.opendocument.text-web" : @"oth", + @"application/vnd.olpc-sugar" : @"xo", + @"application/vnd.oma.dd2+xml" : @"dd2", + @"application/vnd.openofficeorg.extension" : @"oxt", + @"application/vnd.openxmlformats-officedocument.presentationml.presentation" : @"pptx", + @"application/vnd.openxmlformats-officedocument.presentationml.slide" : @"sldx", + @"application/vnd.openxmlformats-officedocument.presentationml.slideshow" : @"ppsx", + @"application/vnd.openxmlformats-officedocument.presentationml.template" : @"potx", + @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : @"xlsx", + @"application/vnd.openxmlformats-officedocument.spreadsheetml.template" : @"xltx", + @"application/vnd.openxmlformats-officedocument.wordprocessingml.document" : @"docx", + @"application/vnd.openxmlformats-officedocument.wordprocessingml.template" : @"dotx", + @"application/vnd.osgeo.mapguide.package" : @"mgp", + @"application/vnd.osgi.dp" : @"dp", + @"application/vnd.osgi.subsystem" : @"esa", + @"application/vnd.palm" : @"pdb", + @"application/vnd.pawaafile" : @"paw", + @"application/vnd.pg.format" : @"str", + @"application/vnd.pg.osasli" : @"ei6", + @"application/vnd.picsel" : @"efif", + @"application/vnd.pmi.widget" : @"wg", + @"application/vnd.pocketlearn" : @"plf", + @"application/vnd.powerbuilder6" : @"pbd", + @"application/vnd.previewsystems.box" : @"box", + @"application/vnd.proteus.magazine" : @"mgz", + @"application/vnd.publishare-delta-tree" : @"qps", + @"application/vnd.pvi.ptid1" : @"ptid", + @"application/vnd.quark.quarkxpress" : @"qxd", + @"application/vnd.realvnc.bed" : @"bed", + @"application/vnd.recordare.musicxml" : @"mxl", + @"application/vnd.recordare.musicxml+xml" : @"musicxml", + @"application/vnd.rig.cryptonote" : @"cryptonote", + @"application/vnd.rim.cod" : @"cod", + @"application/vnd.rn-realmedia" : @"rm", + @"application/vnd.rn-realmedia-vbr" : @"rmvb", + @"application/vnd.rn-realplayer" : @"rnx", + @"application/vnd.route66.link66+xml" : @"link66", + @"application/vnd.sailingtracker.track" : @"st", + @"application/vnd.seemail" : @"see", + @"application/vnd.sema" : @"sema", + @"application/vnd.semd" : @"semd", + @"application/vnd.semf" : @"semf", + @"application/vnd.shana.informed.formdata" : @"ifm", + @"application/vnd.shana.informed.formtemplate" : @"itp", + @"application/vnd.shana.informed.interchange" : @"iif", + @"application/vnd.shana.informed.package" : @"ipk", + @"application/vnd.simtech-mindmapper" : @"twd", + @"application/vnd.smaf" : @"mmf", + @"application/vnd.smart.teacher" : @"teacher", + @"application/vnd.solent.sdkm+xml" : @"sdkm", + @"application/vnd.spotfire.dxp" : @"dxp", + @"application/vnd.spotfire.sfs" : @"sfs", + @"application/vnd.stardivision.calc" : @"sdc", + @"application/vnd.stardivision.draw" : @"sda", + @"application/vnd.stardivision.impress" : @"sdd", + @"application/vnd.stardivision.math" : @"smf", + @"application/vnd.stardivision.writer" : @"sdw", + @"application/vnd.stardivision.writer-global" : @"sgl", + @"application/vnd.stepmania.package" : @"smzip", + @"application/vnd.stepmania.stepchart" : @"sm", + @"application/vnd.sun.xml.calc" : @"sxc", + @"application/vnd.sun.xml.calc.template" : @"stc", + @"application/vnd.sun.xml.draw" : @"sxd", + @"application/vnd.sun.xml.draw.template" : @"std", + @"application/vnd.sun.xml.impress" : @"sxi", + @"application/vnd.sun.xml.impress.template" : @"sti", + @"application/vnd.sun.xml.math" : @"sxm", + @"application/vnd.sun.xml.writer" : @"sxw", + @"application/vnd.sun.xml.writer.global" : @"sxg", + @"application/vnd.sun.xml.writer.template" : @"stw", + @"application/vnd.sus-calendar" : @"sus", + @"application/vnd.svd" : @"svd", + @"application/vnd.symbian.install" : @"sis", + @"application/vnd.syncml+xml" : @"xsm", + @"application/vnd.syncml.dm+wbxml" : @"bdm", + @"application/vnd.syncml.dm+xml" : @"xdm", + @"application/vnd.tao.intent-module-archive" : @"tao", + @"application/vnd.tcpdump.pcap" : @"pcap", + @"application/vnd.tmobile-livetv" : @"tmo", + @"application/vnd.trid.tpt" : @"tpt", + @"application/vnd.triscape.mxs" : @"mxs", + @"application/vnd.trueapp" : @"tra", + @"application/vnd.ufdl" : @"ufd", + @"application/vnd.uiq.theme" : @"utz", + @"application/vnd.umajin" : @"umj", + @"application/vnd.unity" : @"unityweb", + @"application/vnd.uoml+xml" : @"uoml", + @"application/vnd.vcx" : @"vcx", + @"application/vnd.visio" : @"vsd", + @"application/vnd.visio2013" : @"vsdx", + @"application/vnd.visionary" : @"vis", + @"application/vnd.vsf" : @"vsf", + @"application/vnd.wap.wbxml" : @"wbxml", + @"application/vnd.wap.wmlc" : @"wmlc", + @"application/vnd.wap.wmlscriptc" : @"wmlsc", + @"application/vnd.webturbo" : @"wtb", + @"application/vnd.wolfram.player" : @"nbp", + @"application/vnd.wordperfect" : @"wpd", + @"application/vnd.wqd" : @"wqd", + @"application/vnd.wt.stf" : @"stf", + @"application/vnd.xara" : @"xar", + @"application/vnd.xfdl" : @"xfdl", + @"application/vnd.yamaha.hv-dic" : @"hvd", + @"application/vnd.yamaha.hv-script" : @"hvs", + @"application/vnd.yamaha.hv-voice" : @"hvp", + @"application/vnd.yamaha.openscoreformat" : @"osf", + @"application/vnd.yamaha.openscoreformat.osfpvg+xml" : @"osfpvg", + @"application/vnd.yamaha.smaf-audio" : @"saf", + @"application/vnd.yamaha.smaf-phrase" : @"spf", + @"application/vnd.yellowriver-custom-menu" : @"cmp", + @"application/vnd.zul" : @"zir", + @"application/vnd.zzazz.deck+xml" : @"zaz", + @"application/vocaltec-media-desc" : @"vmd", + @"application/vocaltec-media-file" : @"vmf", + @"application/voicexml+xml" : @"vxml", + @"application/widget" : @"wgt", + @"application/winhlp" : @"hlp", + @"application/wordperfect" : @"wp", + @"application/wordperfect6.0" : @"w60", + @"application/wordperfect6.1" : @"w61", + @"application/wsdl+xml" : @"wsdl", + @"application/wspolicy+xml" : @"wspolicy", + @"application/x-123" : @"wk1", + @"application/x-7z-compressed" : @"7z", + @"application/x-abiword" : @"abw", + @"application/x-ace-compressed" : @"ace", + @"application/x-aim" : @"aim", + @"application/x-apple-diskimage" : @"dmg", + @"application/x-authorware-bin" : @"aab", + @"application/x-authorware-map" : @"aam", + @"application/x-authorware-seg" : @"aas", + @"application/x-bcpio" : @"bcpio", + @"application/x-binary" : @"bin", + @"application/x-binhex40" : @"hqx", + @"application/x-bittorrent" : @"torrent", + @"application/x-blorb" : @"blb", + @"application/x-bsh" : @"sh", + @"application/x-bytecode.elisp" : @"elc", + @"application/x-bytecode.python" : @"pyc", + @"application/x-bzip" : @"bz", + @"application/x-bzip2" : @"bz2", + @"application/x-cbr" : @"cbr", + @"application/x-cdf" : @"cdf", + @"application/x-cdlink" : @"vcd", + @"application/x-cfs-compressed" : @"cfs", + @"application/x-chat" : @"chat", + @"application/x-chess-pgn" : @"pgn", + @"application/x-cmu-raster" : @"ras", + @"application/x-cocoa" : @"cco", + @"application/x-compactpro" : @"cpt", + @"application/x-compress" : @"z", + @"application/x-conference" : @"nsc", + @"application/x-cpio" : @"cpio", + @"application/x-cpt" : @"cpt", + @"application/x-csh" : @"csh", + @"application/x-debian-package" : @"deb", + @"application/x-deepv" : @"deepv", + @"application/x-dgc-compressed" : @"dgc", + @"application/x-director" : @"dir", + @"application/x-doom" : @"wad", + @"application/x-dtbncx+xml" : @"ncx", + @"application/x-dtbook+xml" : @"dtb", + @"application/x-dtbresource+xml" : @"res", + @"application/x-dvi" : @"dvi", + @"application/x-elc" : @"elc", + @"application/x-envoy" : @"evy", + @"application/x-esrehber" : @"es", + @"application/x-eva" : @"eva", + @"application/x-excel" : @"xls", + @"application/x-font-bdf" : @"bdf", + @"application/x-font-ghostscript" : @"gsf", + @"application/x-font-linux-psf" : @"psf", + @"application/x-font-otf" : @"otf", + @"application/x-font-pcf" : @"pcf", + @"application/x-font-snf" : @"snf", + @"application/x-font-ttf" : @"ttf", + @"application/x-font-type1" : @"pfa", + @"application/x-font-woff" : @"woff", + @"application/x-frame" : @"mif", + @"application/x-freearc" : @"arc", + @"application/x-freelance" : @"pre", + @"application/x-futuresplash" : @"spl", + @"application/x-gca-compressed" : @"gca", + @"application/x-glulx" : @"ulx", + @"application/x-gnumeric" : @"gnumeric", + @"application/x-gramps-xml" : @"gramps", + @"application/x-gsp" : @"gsp", + @"application/x-gss" : @"gss", + @"application/x-gtar" : @"gtar", + @"application/x-gzip" : @"gz", + @"application/x-hdf" : @"hdf", + @"application/x-httpd-imap" : @"imap", + @"application/x-ima" : @"ima", + @"application/x-install-instructions" : @"install", + @"application/x-internett-signup" : @"ins", + @"application/x-inventor" : @"iv", + @"application/x-ip2" : @"ip", + @"application/x-iphone" : @"iii", + @"application/x-iso9660-image" : @"iso", + @"application/x-java-class" : @"class", + @"application/x-java-commerce" : @"jcm", + @"application/x-java-jnlp-file" : @"jnlp", + @"application/x-javascript" : @"js", + @"application/x-ksh" : @"ksh", + @"application/x-latex" : @"ltx", + @"application/x-lha" : @"lha", + @"application/x-lisp" : @"lsp", + @"application/x-livescreen" : @"ivy", + @"application/x-lotus" : @"wq1", + @"application/x-lotusscreencam" : @"scm", + @"application/x-lzh" : @"lzh", + @"application/x-lzh-compressed" : @"lzh", + @"application/x-lzx" : @"lzx", + @"application/x-mac-binhex40" : @"hqx", + @"application/x-macbinary" : @"bin", + @"application/x-magic-cap-package-1.0" : @"mc$", + @"application/x-mathcad" : @"mcd", + @"application/x-meme" : @"mm", + @"application/x-midi" : @"midi", + @"application/x-mie" : @"mie", + @"application/x-mif" : @"mif", + @"application/x-mix-transfer" : @"nix", + @"application/x-mobipocket-ebook" : @"prc", + @"application/x-mplayer2" : @"asx", + @"application/x-ms-application" : @"application", + @"application/x-ms-shortcut" : @"lnk", + @"application/x-ms-wmd" : @"wmd", + @"application/x-ms-wmz" : @"wmz", + @"application/x-ms-xbap" : @"xbap", + @"application/x-msaccess" : @"mdb", + @"application/x-msbinder" : @"obd", + @"application/x-mscardfile" : @"crd", + @"application/x-msclip" : @"clp", + @"application/x-msdownload" : @"exe", + @"application/x-msexcel" : @"xls", + @"application/x-msmediaview" : @"mvb", + @"application/x-msmetafile" : @"wmf", + @"application/x-msmoney" : @"mny", + @"application/x-mspowerpoint" : @"ppt", + @"application/x-mspublisher" : @"pub", + @"application/x-msschedule" : @"scd", + @"application/x-msterminal" : @"trm", + @"application/x-mswrite" : @"wri", + @"application/x-navi-animation" : @"ani", + @"application/x-navidoc" : @"nvd", + @"application/x-navimap" : @"map", + @"application/x-navistyle" : @"stl", + @"application/x-netcdf" : @"nc", + @"application/x-newton-compatible-pkg" : @"pkg", + @"application/x-nokia-9000-communicator-add-on-software" : @"aos", + @"application/x-nzb" : @"nzb", + @"application/x-omc" : @"omc", + @"application/x-omcdatamaker" : @"omcd", + @"application/x-omcregerator" : @"omcr", + @"application/x-pcl" : @"pcl", + @"application/x-pixclscript" : @"plx", + @"application/x-pkcs10" : @"p10", + @"application/x-pkcs12" : @"p12", + @"application/x-pkcs7-certificates" : @"p7b", + @"application/x-pkcs7-certreqresp" : @"p7r", + @"application/x-pkcs7-mime" : @"p7m", + @"application/x-pkcs7-signature" : @"p7s", + @"application/x-pointplus" : @"css", + @"application/x-portable-anymap" : @"pnm", + @"application/x-qpro" : @"wb1", + @"application/x-rar-compressed" : @"rar", + @"application/x-research-info-systems" : @"ris", + @"application/x-rtf" : @"rtf", + @"application/x-sdp" : @"sdp", + @"application/x-sea" : @"sea", + @"application/x-seelogo" : @"sl", + @"application/x-sh" : @"sh", + @"application/x-shar" : @"shar", + @"application/x-shockwave-flash" : @"swf", + @"application/x-silverlight-app" : @"xap", + @"application/x-sit" : @"sit", + @"application/x-sprite" : @"spr", + @"application/x-sql" : @"sql", + @"application/x-stuffit" : @"sit", + @"application/x-stuffitx" : @"sitx", + @"application/x-subrip" : @"srt", + @"application/x-sv4cpio" : @"sv4cpio", + @"application/x-sv4crc" : @"sv4crc", + @"application/x-t3vm-image" : @"t3", + @"application/x-tads" : @"gam", + @"application/x-tar" : @"tar", + @"application/x-tbook" : @"tbk", + @"application/x-tcl" : @"tcl", + @"application/x-tex" : @"tex", + @"application/x-tex-tfm" : @"tfm", + @"application/x-texinfo" : @"texinfo", + @"application/x-tgif" : @"obj", + @"application/x-troff-man" : @"man", + @"application/x-troff-me" : @"me", + @"application/x-troff-ms" : @"ms", + @"application/x-troff-msvideo" : @"avi", + @"application/x-ustar" : @"ustar", + @"application/x-visio" : @"vsd", + @"application/x-vnd.audioexplosion.mzz" : @"mzz", + @"application/x-vnd.ls-xpix" : @"xpix", + @"application/x-vrml" : @"vrml", + @"application/x-wais-source" : @"src", + @"application/x-winhelp" : @"hlp", + @"application/x-wintalk" : @"wtk", + @"application/x-wpwin" : @"wpd", + @"application/x-wri" : @"wri", + @"application/x-x509-ca-cert" : @"crt", + @"application/x-x509-user-cert" : @"crt", + @"application/x-xfig" : @"fig", + @"application/x-xliff+xml" : @"xlf", + @"application/x-xpinstall" : @"xpi", + @"application/x-xz" : @"xz", + @"application/x-zip-compressed" : @"zip", + @"application/x-zmachine" : @"z1", + @"application/xaml+xml" : @"xaml", + @"application/xcap-diff+xml" : @"xdf", + @"application/xenc+xml" : @"xenc", + @"application/xhtml+xml" : @"xhtml", + @"application/xml" : @"xml", + @"application/xml-dtd" : @"dtd", + @"application/xop+xml" : @"xop", + @"application/xproc+xml" : @"xpl", + @"application/xslt+xml" : @"xslt", + @"application/xspf+xml" : @"xspf", + @"application/xv+xml" : @"mxml", + @"application/yang" : @"yang", + @"application/yin+xml" : @"yin", + @"application/ynd.ms-pkipko" : @"pko", + OWSMimeTypeApplicationZip : @"zip", + @"audio/aac" : @"aac", + @"audio/adpcm" : @"adp", + @"audio/aiff" : @"aiff", + @"audio/basic" : @"au", + @"audio/it" : @"it", + @"audio/mid" : @"rmi", + @"audio/midi" : @"midi", + @"audio/mod" : @"mod", + @"audio/mp4" : @"m4a", + @"audio/mpeg" : @"mpg", + @"audio/mpeg3" : @"mp3", + @"audio/ogg" : @"oga", + @"audio/s3m" : @"s3m", + @"audio/silk" : @"sil", + @"audio/tsp-audio" : @"tsi", + @"audio/tsplayer" : @"tsp", + @"audio/vnd.dece.audio" : @"uva", + @"audio/vnd.digital-winds" : @"eol", + @"audio/vnd.dra" : @"dra", + @"audio/vnd.dts" : @"dts", + @"audio/vnd.dts.hd" : @"dtshd", + @"audio/vnd.lucent.voice" : @"lvp", + @"audio/vnd.ms-playready.media.pya" : @"pya", + @"audio/vnd.nuera.ecelp4800" : @"ecelp4800", + @"audio/vnd.nuera.ecelp7470" : @"ecelp7470", + @"audio/vnd.nuera.ecelp9600" : @"ecelp9600", + @"audio/vnd.qcelp" : @"qcp", + @"audio/vnd.rip" : @"rip", + @"audio/voc" : @"voc", + @"audio/voxware" : @"vox", + @"audio/wav" : @"wav", + @"audio/webm" : @"weba", + @"audio/x-aac" : @"aac", + @"audio/x-adpcm" : @"snd", + @"audio/x-aiff" : @"aiff", + @"audio/x-au" : @"au", + @"audio/x-caf" : @"caf", + @"audio/x-flac" : @"flac", + @"audio/x-gsm" : @"gsm", + @"audio/x-jam" : @"jam", + @"audio/x-liveaudio" : @"lam", + @"audio/x-matroska" : @"mka", + @"audio/x-mid" : @"midi", + @"audio/x-midi" : @"midi", + @"audio/x-mod" : @"mod", + @"audio/x-mpeg" : @"mp2", + @"audio/x-mpeg-3" : @"mp3", + @"audio/x-mpegurl" : @"m3u", + @"audio/x-mpequrl" : @"m3u", + @"audio/x-ms-wax" : @"wax", + @"audio/x-ms-wma" : @"wma", + @"audio/x-pn-realaudio" : @"ram", + @"audio/x-pn-realaudio-plugin" : @"rmp", + @"audio/x-psid" : @"sid", + @"audio/x-realaudio" : @"ra", + @"audio/x-twinvq" : @"vqf", + @"audio/x-vnd.audioexplosion.mjuicemediafile" : @"mjf", + @"audio/x-voc" : @"voc", + @"audio/x-wav" : @"wav", + @"audio/xm" : @"xm", + @"chemical/x-cdx" : @"cdx", + @"chemical/x-cif" : @"cif", + @"chemical/x-cmdf" : @"cmdf", + @"chemical/x-cml" : @"cml", + @"chemical/x-csml" : @"csml", + @"chemical/x-pdb" : @"pdb", + @"chemical/x-xyz" : @"xyz", + @"drawing/x-dwf" : @"dwf", + @"font/ttf" : @"ttf", + @"font/woff" : @"woff", + @"font/woff2" : @"woff2", + @"i-world/i-vrml" : @"ivr", + @"image/bmp" : @"bmp", + @"image/cgm" : @"cgm", + @"image/cis-cod" : @"cod", + @"image/fif" : @"fif", + @"image/g3fax" : @"g3", + @"image/gif" : @"gif", + @"image/ief" : @"ief", + @"image/jpeg" : @"jpg", + @"image/jutvision" : @"jut", + @"image/ktx" : @"ktx", + @"image/pict" : @"pict", + @"image/pjpeg" : @"jpg", + @"image/png" : @"png", + @"image/prs.btif" : @"btif", + @"image/sgi" : @"sgi", + @"image/svg+xml" : @"svg", + @"image/tiff" : @"tiff", + @"image/vasa" : @"mcf", + @"image/vnd.adobe.photoshop" : @"psd", + @"image/vnd.dece.graphic" : @"uvi", + @"image/vnd.djvu" : @"djvu", + @"image/vnd.dvb.subtitle" : @"sub", + @"image/vnd.dwg" : @"dwg", + @"image/vnd.dxf" : @"dxf", + @"image/vnd.fastbidsheet" : @"fbs", + @"image/vnd.fpx" : @"fpx", + @"image/vnd.fst" : @"fst", + @"image/vnd.fujixerox.edmics-mmr" : @"mmr", + @"image/vnd.fujixerox.edmics-rlc" : @"rlc", + @"image/vnd.ms-modi" : @"mdi", + @"image/vnd.ms-photo" : @"wdp", + @"image/vnd.net-fpx" : @"fpx", + @"image/vnd.rn-realflash" : @"rf", + @"image/vnd.rn-realpix" : @"rp", + @"image/vnd.wap.wbmp" : @"wbmp", + @"image/vnd.xiff" : @"xif", + @"image/webp" : @"webp", + @"image/x-3ds" : @"3ds", + @"image/x-citrix-jpeg" : @"jpg", + @"image/x-citrix-png" : @"png", + @"image/x-cmu-raster" : @"ras", + @"image/x-cmx" : @"cmx", + @"image/x-dwg" : @"dwg", + @"image/x-freehand" : @"fh", + @"image/x-icon" : @"ico", + @"image/x-jg" : @"art", + @"image/x-jps" : @"jps", + @"image/x-mrsid-image" : @"sid", + @"image/x-niff" : @"niff", + @"image/x-pcx" : @"pcx", + @"image/x-pict" : @"pic", + @"image/x-png" : @"png", + @"image/x-portable-anymap" : @"pnm", + @"image/x-portable-bitmap" : @"pbm", + @"image/x-portable-graymap" : @"pgm", + @"image/x-portable-greymap" : @"pgm", + @"image/x-portable-pixmap" : @"ppm", + @"image/x-rgb" : @"rgb", + @"image/x-tga" : @"tga", + @"image/x-tiff" : @"tiff", + @"image/x-windows-bmp" : @"bmp", + @"image/x-xbitmap" : @"xbm", + @"image/x-xbm" : @"xbm", + @"image/x-xpixmap" : @"xpm", + @"image/x-xwd" : @"xwd", + @"image/x-xwindowdump" : @"xwd", + @"image/xbm" : @"xbm", + @"image/xpm" : @"xpm", + @"message/rfc822" : @"eml", + @"model/iges" : @"iges", + @"model/mesh" : @"msh", + @"model/vnd.collada+xml" : @"dae", + @"model/vnd.dwf" : @"dwf", + @"model/vnd.gdl" : @"gdl", + @"model/vnd.gtw" : @"gtw", + @"model/vnd.mts" : @"mts", + @"model/vnd.vtu" : @"vtu", + @"model/vrml" : @"vrml", + @"model/x-pov" : @"pov", + @"model/x3d+binary" : @"x3db", + @"model/x3d+vrml" : @"x3dv", + @"model/x3d+xml" : @"x3d", + @"multipart/x-gzip" : @"gzip", + @"multipart/x-ustar" : @"ustar", + @"multipart/x-zip" : @"zip", + @"music/x-karaoke" : @"kar", + @"paleovu/x-pv" : @"pvu", + @"text/asp" : @"asp", + @"text/cache-manifest" : @"appcache", + @"text/calendar" : @"ics", + @"text/css" : @"css", + @"text/csv" : @"csv", + @"text/ecmascript" : @"js", + @"text/h323" : @"323", + @"text/html" : @"html", + @"text/iuls" : @"uls", + @"text/java" : @"java", + @"text/javascript" : @"js", + @"text/mcf" : @"mcf", + @"text/n3" : @"n3", + @"text/pascal" : @"pas", + @"text/plain" : @"txt", + @"text/plain-bas" : @"par", + @"text/prs.lines.logTag" : @"dsc", + @"text/richtext" : @"rtf", + @"text/scriplet" : @"wsc", + @"text/scriptlet" : @"sct", + @"text/sgml" : @"sgml", + @"text/tab-separated-values" : @"tsv", + @"text/troff" : @"t", + @"text/turtle" : @"ttl", + @"text/uri-list" : @"uri", + @"text/vcard" : @"vcard", + @"text/vnd.abc" : @"abc", + @"text/vnd.curl" : @"curl", + @"text/vnd.curl.dcurl" : @"dcurl", + @"text/vnd.curl.mcurl" : @"mcurl", + @"text/vnd.curl.scurl" : @"scurl", + @"text/vnd.dvb.subtitle" : @"sub", + @"text/vnd.fly" : @"fly", + @"text/vnd.fmi.flexstor" : @"flx", + @"text/vnd.graphviz" : @"gv", + @"text/vnd.in3d.3dml" : @"3dml", + @"text/vnd.in3d.spot" : @"spot", + @"text/vnd.rn-realtext" : @"rt", + @"text/vnd.sun.j2me.app-descriptor" : @"jad", + @"text/vnd.wap.wml" : @"wml", + @"text/vnd.wap.wmlscript" : @"wmls", + @"text/webviewhtml" : @"htt", + @"text/x-asm" : @"asm", + @"text/x-audiosoft-intra" : @"aip", + @"text/x-c" : @"c", + @"text/x-component" : @"htc", + @"text/x-fortran" : @"f", + @"text/x-h" : @"h", + @"text/x-java-source" : @"java", + @"text/x-la-asf" : @"lsx", + @"text/x-m" : @"m", + @"text/x-nfo" : @"nfo", + @"text/x-opml" : @"opml", + @"text/x-pascal" : @"p", + @"text/x-script" : @"hlb", + @"text/x-script.csh" : @"csh", + @"text/x-script.elisp" : @"el", + @"text/x-script.guile" : @"scm", + @"text/x-script.ksh" : @"ksh", + @"text/x-script.lisp" : @"lsp", + @"text/x-script.perl" : @"pl", + @"text/x-script.perl-module" : @"pm", + @"text/x-script.phyton" : @"py", + @"text/x-script.rexx" : @"rexx", + @"text/x-script.scheme" : @"scm", + @"text/x-script.sh" : @"sh", + @"text/x-script.tcl" : @"tcl", + @"text/x-script.tcsh" : @"tcsh", + @"text/x-script.zsh" : @"zsh", + @"text/x-setext" : @"etx", + @"text/x-sfv" : @"sfv", + @"text/x-sgml" : @"sgml", + @"text/x-uil" : @"uil", + @"text/x-uuencode" : @"uu", + @"text/x-vcalendar" : @"vcs", + @"text/x-vcard" : @"vcf", + @"text/xml" : @"xml", + @"text/yaml" : @"yaml", + @"video/3gpp" : @"3gp", + @"video/3gpp2" : @"3g2", + @"video/animaflex" : @"afl", + @"video/avi" : @"avi", + @"video/avs-video" : @"avs", + @"video/dl" : @"dl", + @"video/fli" : @"fli", + @"video/gl" : @"gl", + @"video/h261" : @"h261", + @"video/h263" : @"h263", + @"video/h264" : @"h264", + @"video/jpeg" : @"jpgv", + @"video/jpm" : @"jpm", + @"video/mj2" : @"mj2", + @"video/mp4" : @"mp4", + @"video/mpeg" : @"mpg", + @"video/msvideo" : @"avi", + @"video/ogg" : @"ogv", + @"video/quicktime" : @"mov", + @"video/vdo" : @"vdo", + @"video/vnd.dece.hd" : @"uvh", + @"video/vnd.dece.mobile" : @"uvm", + @"video/vnd.dece.pd" : @"uvp", + @"video/vnd.dece.sd" : @"uvs", + @"video/vnd.dece.video" : @"uvv", + @"video/vnd.dvb.file" : @"dvb", + @"video/vnd.fvt" : @"fvt", + @"video/vnd.mpegurl" : @"mxu", + @"video/vnd.ms-playready.media.pyv" : @"pyv", + @"video/vnd.rn-realvideo" : @"rv", + @"video/vnd.uvvu.mp4" : @"uvu", + @"video/vnd.vivo" : @"viv", + @"video/vosaic" : @"vos", + @"video/webm" : @"webm", + @"video/x-amt-demorun" : @"xdr", + @"video/x-amt-showrun" : @"xsr", + @"video/x-atomic3d-feature" : @"fmf", + @"video/x-dl" : @"dl", + @"video/x-dv" : @"dv", + @"video/x-f4v" : @"f4v", + @"video/x-fli" : @"fli", + @"video/x-flv" : @"flv", + @"video/x-gl" : @"gl", + @"video/x-isvideo" : @"isu", + @"video/x-la-asf" : @"lsf", + @"video/x-m4v" : @"m4v", + @"video/x-matroska" : @"mkv", + @"video/x-mng" : @"mng", + @"video/x-motion-jpeg" : @"mjpg", + @"video/x-mpeg" : @"mpg", + @"video/x-mpeq2a" : @"mp2", + @"video/x-ms-asf" : @"asf", + @"video/x-ms-asf-plugin" : @"asx", + @"video/x-ms-vob" : @"vob", + @"video/x-ms-wm" : @"wm", + @"video/x-ms-wmv" : @"wmv", + @"video/x-ms-wmx" : @"wmx", + @"video/x-ms-wvx" : @"wvx", + @"video/x-msvideo" : @"avi", + @"video/x-qtc" : @"qtc", + @"video/x-scm" : @"scm", + @"video/x-sgi-movie" : @"movie", + @"video/x-smv" : @"smv", + @"windows/metafile" : @"wmf", + @"www/mime" : @"mime", + @"x-conference/x-cooltalk" : @"ice", + @"x-music/x-midi" : @"midi", + @"x-world/x-3dmf" : @"3dmf", + @"x-world/x-svr" : @"svr", + @"x-world/x-vrml" : @"vrml", + @"x-world/x-vrt" : @"vrt", + @"xgl/drawing" : @"xgz", + @"xgl/movie" : @"xmz", + }; + }); + return result; +} + ++ (nullable NSString *)mimeTypeForFileExtension:(NSString *)fileExtension +{ + OWSAssertDebug(fileExtension.length > 0); + + return [self genericExtensionTypesToMIMETypes][fileExtension]; +} + ++ (NSDictionary *)genericExtensionTypesToMIMETypes +{ + static NSDictionary *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + result = @{ + @"123" : @"application/vnd.lotus-1-2-3", + @"3dml" : @"text/vnd.in3d.3dml", + @"3ds" : @"image/x-3ds", + @"3g2" : @"video/3gpp2", + @"3gp" : @"video/3gpp", + @"7z" : @"application/x-7z-compressed", + @"aab" : @"application/x-authorware-bin", + @"aac" : @"audio/x-aac", + @"aam" : @"application/x-authorware-map", + @"aas" : @"application/x-authorware-seg", + @"abw" : @"application/x-abiword", + @"ac" : @"application/pkix-attr-cert", + @"acc" : @"application/vnd.americandynamics.acc", + @"ace" : @"application/x-ace-compressed", + @"acu" : @"application/vnd.acucobol", + @"acutc" : @"application/vnd.acucorp", + @"adp" : @"audio/adpcm", + @"aep" : @"application/vnd.audiograph", + @"afm" : @"application/x-font-type1", + @"afp" : @"application/vnd.ibm.modcap", + @"ahead" : @"application/vnd.ahead.space", + @"ai" : @"application/postscript", + @"aif" : @"audio/x-aiff", + @"aifc" : @"audio/x-aiff", + @"aiff" : @"audio/x-aiff", + @"air" : @"application/vnd.adobe.air-application-installer-package+zip", + @"ait" : @"application/vnd.dvb.ait", + @"ami" : @"application/vnd.amiga.ami", + @"apk" : @"application/vnd.android.package-archive", + @"appcache" : @"text/cache-manifest", + @"application" : @"application/x-ms-application", + @"apr" : @"application/vnd.lotus-approach", + @"arc" : @"application/x-freearc", + @"asc" : @"application/pgp-signature", + @"asf" : @"video/x-ms-asf", + @"asm" : @"text/x-asm", + @"aso" : @"application/vnd.accpac.simply.aso", + @"asx" : @"video/x-ms-asf", + @"atc" : @"application/vnd.acucorp", + @"atom" : @"application/atom+xml", + @"atomcat" : @"application/atomcat+xml", + @"atomsvc" : @"application/atomsvc+xml", + @"atx" : @"application/vnd.antix.game-component", + @"au" : @"audio/basic", + @"avi" : @"video/x-msvideo", + @"aw" : @"application/applixware", + @"azf" : @"application/vnd.airzip.filesecure.azf", + @"azs" : @"application/vnd.airzip.filesecure.azs", + @"azw" : @"application/vnd.amazon.ebook", + @"bat" : @"application/x-msdownload", + @"bcpio" : @"application/x-bcpio", + @"bdf" : @"application/x-font-bdf", + @"bdm" : @"application/vnd.syncml.dm+wbxml", + @"bed" : @"application/vnd.realvnc.bed", + @"bh2" : @"application/vnd.fujitsu.oasysprs", + @"bin" : @"application/octet-stream", + @"blb" : @"application/x-blorb", + @"blorb" : @"application/x-blorb", + @"bmi" : @"application/vnd.bmi", + @"bmp" : @"image/bmp", + @"book" : @"application/vnd.framemaker", + @"box" : @"application/vnd.previewsystems.box", + @"boz" : @"application/x-bzip2", + @"bpk" : @"application/octet-stream", + @"btif" : @"image/prs.btif", + @"bz" : @"application/x-bzip", + @"bz2" : @"application/x-bzip2", + @"c" : @"text/x-c", + @"c11amc" : @"application/vnd.cluetrust.cartomobile-config", + @"c11amz" : @"application/vnd.cluetrust.cartomobile-config-pkg", + @"c4d" : @"application/vnd.clonk.c4group", + @"c4f" : @"application/vnd.clonk.c4group", + @"c4g" : @"application/vnd.clonk.c4group", + @"c4p" : @"application/vnd.clonk.c4group", + @"c4u" : @"application/vnd.clonk.c4group", + @"cab" : @"application/vnd.ms-cab-compressed", + @"caf" : @"audio/x-caf", + @"cap" : @"application/vnd.tcpdump.pcap", + @"car" : @"application/vnd.curl.car", + @"cat" : @"application/vnd.ms-pki.seccat", + @"cb7" : @"application/x-cbr", + @"cba" : @"application/x-cbr", + @"cbr" : @"application/x-cbr", + @"cbt" : @"application/x-cbr", + @"cbz" : @"application/x-cbr", + @"cc" : @"text/x-c", + @"cct" : @"application/x-director", + @"ccxml" : @"application/ccxml+xml", + @"cdbcmsg" : @"application/vnd.contact.cmsg", + @"cdf" : @"application/x-netcdf", + @"cdkey" : @"application/vnd.mediastation.cdkey", + @"cdmia" : @"application/cdmi-capability", + @"cdmic" : @"application/cdmi-container", + @"cdmid" : @"application/cdmi-domain", + @"cdmio" : @"application/cdmi-object", + @"cdmiq" : @"application/cdmi-queue", + @"cdx" : @"chemical/x-cdx", + @"cdxml" : @"application/vnd.chemdraw+xml", + @"cdy" : @"application/vnd.cinderella", + @"cer" : @"application/pkix-cert", + @"cfs" : @"application/x-cfs-compressed", + @"cgm" : @"image/cgm", + @"chat" : @"application/x-chat", + @"chm" : @"application/vnd.ms-htmlhelp", + @"chrt" : @"application/vnd.kde.kchart", + @"cif" : @"chemical/x-cif", + @"cii" : @"application/vnd.anser-web-certificate-issue-initiation", + @"cil" : @"application/vnd.ms-artgalry", + @"cla" : @"application/vnd.claymore", + @"class" : @"application/java-vm", + @"clkk" : @"application/vnd.crick.clicker.keyboard", + @"clkp" : @"application/vnd.crick.clicker.palette", + @"clkt" : @"application/vnd.crick.clicker.template", + @"clkw" : @"application/vnd.crick.clicker.wordbank", + @"clkx" : @"application/vnd.crick.clicker", + @"clp" : @"application/x-msclip", + @"cmc" : @"application/vnd.cosmocaller", + @"cmdf" : @"chemical/x-cmdf", + @"cml" : @"chemical/x-cml", + @"cmp" : @"application/vnd.yellowriver-custom-menu", + @"cmx" : @"image/x-cmx", + @"cod" : @"application/vnd.rim.cod", + @"com" : @"application/x-msdownload", + @"conf" : @"text/plain", + @"cpio" : @"application/x-cpio", + @"cpp" : @"text/x-c", + @"cpt" : @"application/mac-compactpro", + @"crd" : @"application/x-mscardfile", + @"crl" : @"application/pkix-crl", + @"crt" : @"application/x-x509-ca-cert", + @"cryptonote" : @"application/vnd.rig.cryptonote", + @"csh" : @"application/x-csh", + @"csml" : @"chemical/x-csml", + @"csp" : @"application/vnd.commonspace", + @"css" : @"text/css", + @"cst" : @"application/x-director", + @"csv" : @"text/csv", + @"cu" : @"application/cu-seeme", + @"curl" : @"text/vnd.curl", + @"cww" : @"application/prs.cww", + @"cxt" : @"application/x-director", + @"cxx" : @"text/x-c", + @"dae" : @"model/vnd.collada+xml", + @"daf" : @"application/vnd.mobius.daf", + @"dart" : @"application/vnd.dart", + @"dataless" : @"application/vnd.fdsn.seed", + @"davmount" : @"application/davmount+xml", + @"dbk" : @"application/docbook+xml", + @"dcr" : @"application/x-director", + @"dcurl" : @"text/vnd.curl.dcurl", + @"dd2" : @"application/vnd.oma.dd2+xml", + @"ddd" : @"application/vnd.fujixerox.ddd", + @"deb" : @"application/x-debian-package", + @"def" : @"text/plain", + @"deploy" : @"application/octet-stream", + @"der" : @"application/x-x509-ca-cert", + @"dfac" : @"application/vnd.dreamfactory", + @"dgc" : @"application/x-dgc-compressed", + @"dic" : @"text/x-c", + @"dir" : @"application/x-director", + @"dis" : @"application/vnd.mobius.dis", + @"dist" : @"application/octet-stream", + @"distz" : @"application/octet-stream", + @"djv" : @"image/vnd.djvu", + @"djvu" : @"image/vnd.djvu", + @"dll" : @"application/x-msdownload", + @"dmg" : @"application/x-apple-diskimage", + @"dmp" : @"application/vnd.tcpdump.pcap", + @"dms" : @"application/octet-stream", + @"dna" : @"application/vnd.dna", + @"doc" : @"application/msword", + @"docm" : @"application/vnd.ms-word.document.macroenabled.12", + @"docx" : @"application/vnd.openxmlformats-officedocument.wordprocessingml.document", + @"dot" : @"application/msword", + @"dotm" : @"application/vnd.ms-word.template.macroenabled.12", + @"dotx" : @"application/vnd.openxmlformats-officedocument.wordprocessingml.template", + @"dp" : @"application/vnd.osgi.dp", + @"dpg" : @"application/vnd.dpgraph", + @"dra" : @"audio/vnd.dra", + @"dsc" : @"text/prs.lines.logTag", + @"dssc" : @"application/dssc+der", + @"dtb" : @"application/x-dtbook+xml", + @"dtd" : @"application/xml-dtd", + @"dts" : @"audio/vnd.dts", + @"dtshd" : @"audio/vnd.dts.hd", + @"dump" : @"application/octet-stream", + @"dvb" : @"video/vnd.dvb.file", + @"dvi" : @"application/x-dvi", + @"dwf" : @"model/vnd.dwf", + @"dwg" : @"image/vnd.dwg", + @"dxf" : @"image/vnd.dxf", + @"dxp" : @"application/vnd.spotfire.dxp", + @"dxr" : @"application/x-director", + @"ecelp4800" : @"audio/vnd.nuera.ecelp4800", + @"ecelp7470" : @"audio/vnd.nuera.ecelp7470", + @"ecelp9600" : @"audio/vnd.nuera.ecelp9600", + @"ecma" : @"application/ecmascript", + @"edm" : @"application/vnd.novadigm.edm", + @"edx" : @"application/vnd.novadigm.edx", + @"efif" : @"application/vnd.picsel", + @"ei6" : @"application/vnd.pg.osasli", + @"elc" : @"application/octet-stream", + @"emf" : @"application/x-msmetafile", + @"eml" : @"message/rfc822", + @"emma" : @"application/emma+xml", + @"emz" : @"application/x-msmetafile", + @"eol" : @"audio/vnd.digital-winds", + @"eot" : @"application/vnd.ms-fontobject", + @"eps" : @"application/postscript", + @"epub" : @"application/epub+zip", + @"es3" : @"application/vnd.eszigno3+xml", + @"esa" : @"application/vnd.osgi.subsystem", + @"esf" : @"application/vnd.epson.esf", + @"et3" : @"application/vnd.eszigno3+xml", + @"etx" : @"text/x-setext", + @"eva" : @"application/x-eva", + @"evy" : @"application/x-envoy", + @"exe" : @"application/x-msdownload", + @"exi" : @"application/exi", + @"ext" : @"application/vnd.novadigm.ext", + @"ez" : @"application/andrew-inset", + @"ez2" : @"application/vnd.ezpix-album", + @"ez3" : @"application/vnd.ezpix-package", + @"f" : @"text/x-fortran", + @"f4v" : @"video/x-f4v", + @"f77" : @"text/x-fortran", + @"f90" : @"text/x-fortran", + @"fbs" : @"image/vnd.fastbidsheet", + @"fcdt" : @"application/vnd.adobe.formscentral.fcdt", + @"fcs" : @"application/vnd.isac.fcs", + @"fdf" : @"application/vnd.fdf", + @"fe_launch" : @"application/vnd.denovo.fcselayout-link", + @"fg5" : @"application/vnd.fujitsu.oasysgp", + @"fgd" : @"application/x-director", + @"fh" : @"image/x-freehand", + @"fh4" : @"image/x-freehand", + @"fh5" : @"image/x-freehand", + @"fh7" : @"image/x-freehand", + @"fhc" : @"image/x-freehand", + @"fig" : @"application/x-xfig", + @"flac" : @"audio/x-flac", + @"fli" : @"video/x-fli", + @"flo" : @"application/vnd.micrografx.flo", + @"flv" : @"video/x-flv", + @"flw" : @"application/vnd.kde.kivio", + @"flx" : @"text/vnd.fmi.flexstor", + @"fly" : @"text/vnd.fly", + @"fm" : @"application/vnd.framemaker", + @"fnc" : @"application/vnd.frogans.fnc", + @"for" : @"text/x-fortran", + @"fpx" : @"image/vnd.fpx", + @"frame" : @"application/vnd.framemaker", + @"fsc" : @"application/vnd.fsc.weblaunch", + @"fst" : @"image/vnd.fst", + @"ftc" : @"application/vnd.fluxtime.clip", + @"fti" : @"application/vnd.anser-web-funds-transfer-initiation", + @"fvt" : @"video/vnd.fvt", + @"fxp" : @"application/vnd.adobe.fxp", + @"fxpl" : @"application/vnd.adobe.fxp", + @"fzs" : @"application/vnd.fuzzysheet", + @"g2w" : @"application/vnd.geoplan", + @"g3" : @"image/g3fax", + @"g3w" : @"application/vnd.geospace", + @"gac" : @"application/vnd.groove-account", + @"gam" : @"application/x-tads", + @"gbr" : @"application/rpki-ghostbusters", + @"gca" : @"application/x-gca-compressed", + @"gdl" : @"model/vnd.gdl", + @"geo" : @"application/vnd.dynageo", + @"gex" : @"application/vnd.geometry-explorer", + @"ggb" : @"application/vnd.geogebra.file", + @"ggt" : @"application/vnd.geogebra.tool", + @"ghf" : @"application/vnd.groove-help", + @"gif" : @"image/gif", + @"gim" : @"application/vnd.groove-identity-message", + @"gml" : @"application/gml+xml", + @"gmx" : @"application/vnd.gmx", + @"gnumeric" : @"application/x-gnumeric", + @"gph" : @"application/vnd.flographit", + @"gpx" : @"application/gpx+xml", + @"gqf" : @"application/vnd.grafeq", + @"gqs" : @"application/vnd.grafeq", + @"gram" : @"application/srgs", + @"gramps" : @"application/x-gramps-xml", + @"gre" : @"application/vnd.geometry-explorer", + @"grv" : @"application/vnd.groove-injector", + @"grxml" : @"application/srgs+xml", + @"gsf" : @"application/x-font-ghostscript", + @"gtar" : @"application/x-gtar", + @"gtm" : @"application/vnd.groove-tool-message", + @"gtw" : @"model/vnd.gtw", + @"gv" : @"text/vnd.graphviz", + @"gxf" : @"application/gxf", + @"gxt" : @"application/vnd.geonext", + @"h" : @"text/x-c", + @"h261" : @"video/h261", + @"h263" : @"video/h263", + @"h264" : @"video/h264", + @"hal" : @"application/vnd.hal+xml", + @"hbci" : @"application/vnd.hbci", + @"hdf" : @"application/x-hdf", + @"hh" : @"text/x-c", + @"hlp" : @"application/winhlp", + @"hpgl" : @"application/vnd.hp-hpgl", + @"hpid" : @"application/vnd.hp-hpid", + @"hps" : @"application/vnd.hp-hps", + @"hqx" : @"application/mac-binhex40", + @"htke" : @"application/vnd.kenameaapp", + @"htm" : @"text/html", + @"html" : @"text/html", + @"hvd" : @"application/vnd.yamaha.hv-dic", + @"hvp" : @"application/vnd.yamaha.hv-voice", + @"hvs" : @"application/vnd.yamaha.hv-script", + @"i2g" : @"application/vnd.intergeo", + @"icc" : @"application/vnd.iccprofile", + @"ice" : @"x-conference/x-cooltalk", + @"icm" : @"application/vnd.iccprofile", + @"ico" : @"image/x-icon", + @"ics" : @"text/calendar", + @"ief" : @"image/ief", + @"ifb" : @"text/calendar", + @"ifm" : @"application/vnd.shana.informed.formdata", + @"iges" : @"model/iges", + @"igl" : @"application/vnd.igloader", + @"igm" : @"application/vnd.insors.igm", + @"igs" : @"model/iges", + @"igx" : @"application/vnd.micrografx.igx", + @"iif" : @"application/vnd.shana.informed.interchange", + @"imp" : @"application/vnd.accpac.simply.imp", + @"ims" : @"application/vnd.ms-ims", + @"in" : @"text/plain", + @"ink" : @"application/inkml+xml", + @"inkml" : @"application/inkml+xml", + @"install" : @"application/x-install-instructions", + @"iota" : @"application/vnd.astraea-software.iota", + @"ipfix" : @"application/ipfix", + @"ipk" : @"application/vnd.shana.informed.package", + @"irm" : @"application/vnd.ibm.rights-management", + @"irp" : @"application/vnd.irepository.package+xml", + @"iso" : @"application/x-iso9660-image", + @"itp" : @"application/vnd.shana.informed.formtemplate", + @"ivp" : @"application/vnd.immervision-ivp", + @"ivu" : @"application/vnd.immervision-ivu", + @"jad" : @"text/vnd.sun.j2me.app-descriptor", + @"jam" : @"application/vnd.jam", + @"jar" : @"application/java-archive", + @"java" : @"text/x-java-source", + @"jisp" : @"application/vnd.jisp", + @"jlt" : @"application/vnd.hp-jlyt", + @"jnlp" : @"application/x-java-jnlp-file", + @"joda" : @"application/vnd.joost.joda-archive", + @"jpe" : @"image/jpeg", + @"jpeg" : @"image/jpeg", + @"jpg" : @"image/jpeg", + @"jpgm" : @"video/jpm", + @"jpgv" : @"video/jpeg", + @"jpm" : @"video/jpm", + @"js" : @"application/javascript", + @"json" : @"application/json", + @"jsonml" : @"application/jsonml+json", + @"kar" : @"audio/midi", + @"karbon" : @"application/vnd.kde.karbon", + @"kfo" : @"application/vnd.kde.kformula", + @"kia" : @"application/vnd.kidspiration", + @"kml" : @"application/vnd.google-earth.kml+xml", + @"kmz" : @"application/vnd.google-earth.kmz", + @"kne" : @"application/vnd.kinar", + @"knp" : @"application/vnd.kinar", + @"kon" : @"application/vnd.kde.kontour", + @"kpr" : @"application/vnd.kde.kpresenter", + @"kpt" : @"application/vnd.kde.kpresenter", + @"kpxx" : @"application/vnd.ds-keypoint", + @"ksp" : @"application/vnd.kde.kspread", + @"ktr" : @"application/vnd.kahootz", + @"ktx" : @"image/ktx", + @"ktz" : @"application/vnd.kahootz", + @"kwd" : @"application/vnd.kde.kword", + @"kwt" : @"application/vnd.kde.kword", + @"lasxml" : @"application/vnd.las.las+xml", + @"latex" : @"application/x-latex", + @"lbd" : @"application/vnd.llamagraphics.life-balance.desktop", + @"lbe" : @"application/vnd.llamagraphics.life-balance.exchange+xml", + @"les" : @"application/vnd.hhe.lesson-player", + @"lha" : @"application/x-lzh-compressed", + @"link66" : @"application/vnd.route66.link66+xml", + @"list" : @"text/plain", + @"list3820" : @"application/vnd.ibm.modcap", + @"listafp" : @"application/vnd.ibm.modcap", + @"lnk" : @"application/x-ms-shortcut", + @"log" : @"text/plain", + @"lostxml" : @"application/lost+xml", + @"lrf" : @"application/octet-stream", + @"lrm" : @"application/vnd.ms-lrm", + @"ltf" : @"application/vnd.frogans.ltf", + @"lvp" : @"audio/vnd.lucent.voice", + @"lwp" : @"application/vnd.lotus-wordpro", + @"lzh" : @"application/x-lzh-compressed", + @"m13" : @"application/x-msmediaview", + @"m14" : @"application/x-msmediaview", + @"m1v" : @"video/mpeg", + @"m21" : @"application/mp21", + @"m2a" : @"audio/mpeg", + @"m2v" : @"video/mpeg", + @"m3a" : @"audio/mpeg", + @"m3u" : @"audio/x-mpegurl", + @"m3u8" : @"application/vnd.apple.mpegurl", + @"m4a" : @"audio/mp4", + @"m4u" : @"video/vnd.mpegurl", + @"m4v" : @"video/x-m4v", + @"ma" : @"application/mathematica", + @"mads" : @"application/mads+xml", + @"mag" : @"application/vnd.ecowin.chart", + @"maker" : @"application/vnd.framemaker", + @"man" : @"text/troff", + @"mar" : @"application/octet-stream", + @"mathml" : @"application/mathml+xml", + @"mb" : @"application/mathematica", + @"mbk" : @"application/vnd.mobius.mbk", + @"mbox" : @"application/mbox", + @"mc1" : @"application/vnd.medcalcdata", + @"mcd" : @"application/vnd.mcd", + @"mcurl" : @"text/vnd.curl.mcurl", + @"mdb" : @"application/x-msaccess", + @"mdi" : @"image/vnd.ms-modi", + @"me" : @"text/troff", + @"mesh" : @"model/mesh", + @"meta4" : @"application/metalink4+xml", + @"metalink" : @"application/metalink+xml", + @"mets" : @"application/mets+xml", + @"mfm" : @"application/vnd.mfmp", + @"mft" : @"application/rpki-manifest", + @"mgp" : @"application/vnd.osgeo.mapguide.package", + @"mgz" : @"application/vnd.proteus.magazine", + @"mid" : @"audio/midi", + @"midi" : @"audio/midi", + @"mie" : @"application/x-mie", + @"mif" : @"application/vnd.mif", + @"mime" : @"message/rfc822", + @"mj2" : @"video/mj2", + @"mjp2" : @"video/mj2", + @"mk3d" : @"video/x-matroska", + @"mka" : @"audio/x-matroska", + @"mks" : @"video/x-matroska", + @"mkv" : @"video/x-matroska", + @"mlp" : @"application/vnd.dolby.mlp", + @"mmd" : @"application/vnd.chipnuts.karaoke-mmd", + @"mmf" : @"application/vnd.smaf", + @"mmr" : @"image/vnd.fujixerox.edmics-mmr", + @"mng" : @"video/x-mng", + @"mny" : @"application/x-msmoney", + @"mobi" : @"application/x-mobipocket-ebook", + @"mods" : @"application/mods+xml", + @"mov" : @"video/quicktime", + @"movie" : @"video/x-sgi-movie", + @"mp2" : @"audio/mpeg", + @"mp21" : @"application/mp21", + @"mp2a" : @"audio/mpeg", + @"mp3" : @"audio/mpeg", + @"mp4" : @"video/mp4", + @"mp4a" : @"audio/mp4", + @"mp4s" : @"application/mp4", + @"mp4v" : @"video/mp4", + @"mpc" : @"application/vnd.mophun.certificate", + @"mpe" : @"video/mpeg", + @"mpeg" : @"video/mpeg", + @"mpg" : @"video/mpeg", + @"mpg4" : @"video/mp4", + @"mpga" : @"audio/mpeg", + @"mpkg" : @"application/vnd.apple.installer+xml", + @"mpm" : @"application/vnd.blueice.multipass", + @"mpn" : @"application/vnd.mophun.application", + @"mpp" : @"application/vnd.ms-project", + @"mpt" : @"application/vnd.ms-project", + @"mpy" : @"application/vnd.ibm.minipay", + @"mqy" : @"application/vnd.mobius.mqy", + @"mrc" : @"application/marc", + @"mrcx" : @"application/marcxml+xml", + @"ms" : @"text/troff", + @"mscml" : @"application/mediaservercontrol+xml", + @"mseed" : @"application/vnd.fdsn.mseed", + @"mseq" : @"application/vnd.mseq", + @"msf" : @"application/vnd.epson.msf", + @"msh" : @"model/mesh", + @"msi" : @"application/x-msdownload", + @"msl" : @"application/vnd.mobius.msl", + @"msty" : @"application/vnd.muvee.style", + @"mts" : @"model/vnd.mts", + @"mus" : @"application/vnd.musician", + @"musicxml" : @"application/vnd.recordare.musicxml+xml", + @"mvb" : @"application/x-msmediaview", + @"mwf" : @"application/vnd.mfer", + @"mxf" : @"application/mxf", + @"mxl" : @"application/vnd.recordare.musicxml", + @"mxml" : @"application/xv+xml", + @"mxs" : @"application/vnd.triscape.mxs", + @"mxu" : @"video/vnd.mpegurl", + @"n-gage" : @"application/vnd.nokia.n-gage.symbian.install", + @"n3" : @"text/n3", + @"nb" : @"application/mathematica", + @"nbp" : @"application/vnd.wolfram.player", + @"nc" : @"application/x-netcdf", + @"ncx" : @"application/x-dtbncx+xml", + @"nfo" : @"text/x-nfo", + @"ngdat" : @"application/vnd.nokia.n-gage.data", + @"nitf" : @"application/vnd.nitf", + @"nlu" : @"application/vnd.neurolanguage.nlu", + @"nml" : @"application/vnd.enliven", + @"nnd" : @"application/vnd.noblenet-directory", + @"nns" : @"application/vnd.noblenet-sealer", + @"nnw" : @"application/vnd.noblenet-web", + @"npx" : @"image/vnd.net-fpx", + @"nsc" : @"application/x-conference", + @"nsf" : @"application/vnd.lotus-notes", + @"ntf" : @"application/vnd.nitf", + @"nzb" : @"application/x-nzb", + @"oa2" : @"application/vnd.fujitsu.oasys2", + @"oa3" : @"application/vnd.fujitsu.oasys3", + @"oas" : @"application/vnd.fujitsu.oasys", + @"obd" : @"application/x-msbinder", + @"obj" : @"application/x-tgif", + @"oda" : @"application/oda", + @"odb" : @"application/vnd.oasis.opendocument.database", + @"odc" : @"application/vnd.oasis.opendocument.chart", + @"odf" : @"application/vnd.oasis.opendocument.formula", + @"odft" : @"application/vnd.oasis.opendocument.formula-template", + @"odg" : @"application/vnd.oasis.opendocument.graphics", + @"odi" : @"application/vnd.oasis.opendocument.image", + @"odm" : @"application/vnd.oasis.opendocument.text-master", + @"odp" : @"application/vnd.oasis.opendocument.presentation", + @"ods" : @"application/vnd.oasis.opendocument.spreadsheet", + @"odt" : @"application/vnd.oasis.opendocument.text", + @"oga" : @"audio/ogg", + @"ogg" : @"audio/ogg", + @"ogv" : @"video/ogg", + @"ogx" : @"application/ogg", + @"omdoc" : @"application/omdoc+xml", + @"onepkg" : @"application/onenote", + @"onetmp" : @"application/onenote", + @"onetoc" : @"application/onenote", + @"onetoc2" : @"application/onenote", + @"opf" : @"application/oebps-package+xml", + @"opml" : @"text/x-opml", + @"oprc" : @"application/vnd.palm", + @"org" : @"application/vnd.lotus-organizer", + @"osf" : @"application/vnd.yamaha.openscoreformat", + @"osfpvg" : @"application/vnd.yamaha.openscoreformat.osfpvg+xml", + @"otc" : @"application/vnd.oasis.opendocument.chart-template", + @"otf" : @"application/x-font-otf", + @"otg" : @"application/vnd.oasis.opendocument.graphics-template", + @"oth" : @"application/vnd.oasis.opendocument.text-web", + @"oti" : @"application/vnd.oasis.opendocument.image-template", + @"otp" : @"application/vnd.oasis.opendocument.presentation-template", + @"ots" : @"application/vnd.oasis.opendocument.spreadsheet-template", + @"ott" : @"application/vnd.oasis.opendocument.text-template", + @"oxps" : @"application/oxps", + @"oxt" : @"application/vnd.openofficeorg.extension", + @"p" : @"text/x-pascal", + @"p10" : @"application/pkcs10", + @"p12" : @"application/x-pkcs12", + @"p7b" : @"application/x-pkcs7-certificates", + @"p7c" : @"application/pkcs7-mime", + @"p7m" : @"application/pkcs7-mime", + @"p7r" : @"application/x-pkcs7-certreqresp", + @"p7s" : @"application/pkcs7-signature", + @"p8" : @"application/pkcs8", + @"pas" : @"text/x-pascal", + @"paw" : @"application/vnd.pawaafile", + @"pbd" : @"application/vnd.powerbuilder6", + @"pbm" : @"image/x-portable-bitmap", + @"pcap" : @"application/vnd.tcpdump.pcap", + @"pcf" : @"application/x-font-pcf", + @"pcl" : @"application/vnd.hp-pcl", + @"pclxl" : @"application/vnd.hp-pclxl", + @"pct" : @"image/x-pict", + @"pcurl" : @"application/vnd.curl.pcurl", + @"pcx" : @"image/x-pcx", + @"pdb" : @"application/vnd.palm", + @"pdf" : @"application/pdf", + @"pfa" : @"application/x-font-type1", + @"pfb" : @"application/x-font-type1", + @"pfm" : @"application/x-font-type1", + @"pfr" : @"application/font-tdpfr", + @"pfx" : @"application/x-pkcs12", + @"pgm" : @"image/x-portable-graymap", + @"pgn" : @"application/x-chess-pgn", + @"pgp" : @"application/pgp-encrypted", + @"pic" : @"image/x-pict", + @"pkg" : @"application/octet-stream", + @"pki" : @"application/pkixcmp", + @"pkipath" : @"application/pkix-pkipath", + @"plb" : @"application/vnd.3gpp.pic-bw-large", + @"plc" : @"application/vnd.mobius.plc", + @"plf" : @"application/vnd.pocketlearn", + @"pls" : @"application/pls+xml", + @"pml" : @"application/vnd.ctc-posml", + @"png" : @"image/png", + @"pnm" : @"image/x-portable-anymap", + @"portpkg" : @"application/vnd.macports.portpkg", + @"pot" : @"application/vnd.ms-powerpoint", + @"potm" : @"application/vnd.ms-powerpoint.template.macroenabled.12", + @"potx" : @"application/vnd.openxmlformats-officedocument.presentationml.template", + @"ppam" : @"application/vnd.ms-powerpoint.addin.macroenabled.12", + @"ppd" : @"application/vnd.cups-ppd", + @"ppm" : @"image/x-portable-pixmap", + @"pps" : @"application/vnd.ms-powerpoint", + @"ppsm" : @"application/vnd.ms-powerpoint.slideshow.macroenabled.12", + @"ppsx" : @"application/vnd.openxmlformats-officedocument.presentationml.slideshow", + @"ppt" : @"application/vnd.ms-powerpoint", + @"pptm" : @"application/vnd.ms-powerpoint.presentation.macroenabled.12", + @"pptx" : @"application/vnd.openxmlformats-officedocument.presentationml.presentation", + @"pqa" : @"application/vnd.palm", + @"prc" : @"application/x-mobipocket-ebook", + @"pre" : @"application/vnd.lotus-freelance", + @"prf" : @"application/pics-rules", + @"ps" : @"application/postscript", + @"psb" : @"application/vnd.3gpp.pic-bw-small", + @"psd" : @"image/vnd.adobe.photoshop", + @"psf" : @"application/x-font-linux-psf", + @"pskcxml" : @"application/pskc+xml", + @"ptid" : @"application/vnd.pvi.ptid1", + @"pub" : @"application/x-mspublisher", + @"pvb" : @"application/vnd.3gpp.pic-bw-var", + @"pwn" : @"application/vnd.3m.post-it-notes", + @"pya" : @"audio/vnd.ms-playready.media.pya", + @"pyv" : @"video/vnd.ms-playready.media.pyv", + @"qam" : @"application/vnd.epson.quickanime", + @"qbo" : @"application/vnd.intu.qbo", + @"qfx" : @"application/vnd.intu.qfx", + @"qps" : @"application/vnd.publishare-delta-tree", + @"qt" : @"video/quicktime", + @"qwd" : @"application/vnd.quark.quarkxpress", + @"qwt" : @"application/vnd.quark.quarkxpress", + @"qxb" : @"application/vnd.quark.quarkxpress", + @"qxd" : @"application/vnd.quark.quarkxpress", + @"qxl" : @"application/vnd.quark.quarkxpress", + @"qxt" : @"application/vnd.quark.quarkxpress", + @"ra" : @"audio/x-pn-realaudio", + @"ram" : @"audio/x-pn-realaudio", + @"rar" : @"application/x-rar-compressed", + @"ras" : @"image/x-cmu-raster", + @"rcprofile" : @"application/vnd.ipunplugged.rcprofile", + @"rdf" : @"application/rdf+xml", + @"rdz" : @"application/vnd.data-vision.rdz", + @"rep" : @"application/vnd.businessobjects", + @"res" : @"application/x-dtbresource+xml", + @"rgb" : @"image/x-rgb", + @"rif" : @"application/reginfo+xml", + @"rip" : @"audio/vnd.rip", + @"ris" : @"application/x-research-info-systems", + @"rl" : @"application/resource-lists+xml", + @"rlc" : @"image/vnd.fujixerox.edmics-rlc", + @"rld" : @"application/resource-lists-diff+xml", + @"rm" : @"application/vnd.rn-realmedia", + @"rmi" : @"audio/midi", + @"rmp" : @"audio/x-pn-realaudio-plugin", + @"rms" : @"application/vnd.jcp.javame.midlet-rms", + @"rmvb" : @"application/vnd.rn-realmedia-vbr", + @"rnc" : @"application/relax-ng-compact-syntax", + @"roa" : @"application/rpki-roa", + @"roff" : @"text/troff", + @"rp9" : @"application/vnd.cloanto.rp9", + @"rpss" : @"application/vnd.nokia.radio-presets", + @"rpst" : @"application/vnd.nokia.radio-preset", + @"rq" : @"application/sparql-query", + @"rs" : @"application/rls-services+xml", + @"rsd" : @"application/rsd+xml", + @"rss" : @"application/rss+xml", + @"rtf" : @"application/rtf", + @"rtx" : @"text/richtext", + @"s" : @"text/x-asm", + @"s3m" : @"audio/s3m", + @"saf" : @"application/vnd.yamaha.smaf-audio", + @"sbml" : @"application/sbml+xml", + @"sc" : @"application/vnd.ibm.secure-container", + @"scd" : @"application/x-msschedule", + @"scm" : @"application/vnd.lotus-screencam", + @"scq" : @"application/scvp-cv-request", + @"scs" : @"application/scvp-cv-response", + @"scurl" : @"text/vnd.curl.scurl", + @"sda" : @"application/vnd.stardivision.draw", + @"sdc" : @"application/vnd.stardivision.calc", + @"sdd" : @"application/vnd.stardivision.impress", + @"sdkd" : @"application/vnd.solent.sdkm+xml", + @"sdkm" : @"application/vnd.solent.sdkm+xml", + @"sdp" : @"application/sdp", + @"sdw" : @"application/vnd.stardivision.writer", + @"see" : @"application/vnd.seemail", + @"seed" : @"application/vnd.fdsn.seed", + @"sema" : @"application/vnd.sema", + @"semd" : @"application/vnd.semd", + @"semf" : @"application/vnd.semf", + @"ser" : @"application/java-serialized-object", + @"setpay" : @"application/set-payment-initiation", + @"setreg" : @"application/set-registration-initiation", + @"sfd-hdstx" : @"application/vnd.hydrostatix.sof-data", + @"sfs" : @"application/vnd.spotfire.sfs", + @"sfv" : @"text/x-sfv", + @"sgi" : @"image/sgi", + @"sgl" : @"application/vnd.stardivision.writer-global", + @"sgm" : @"text/sgml", + @"sgml" : @"text/sgml", + @"sh" : @"application/x-sh", + @"shar" : @"application/x-shar", + @"shf" : @"application/shf+xml", + @"sid" : @"image/x-mrsid-image", + @"sig" : @"application/pgp-signature", + @"sil" : @"audio/silk", + @"silo" : @"model/mesh", + @"sis" : @"application/vnd.symbian.install", + @"sisx" : @"application/vnd.symbian.install", + @"sit" : @"application/x-stuffit", + @"sitx" : @"application/x-stuffitx", + @"skd" : @"application/vnd.koan", + @"skm" : @"application/vnd.koan", + @"skp" : @"application/vnd.koan", + @"skt" : @"application/vnd.koan", + @"sldm" : @"application/vnd.ms-powerpoint.slide.macroenabled.12", + @"sldx" : @"application/vnd.openxmlformats-officedocument.presentationml.slide", + @"slt" : @"application/vnd.epson.salt", + @"sm" : @"application/vnd.stepmania.stepchart", + @"smf" : @"application/vnd.stardivision.math", + @"smi" : @"application/smil+xml", + @"smil" : @"application/smil+xml", + @"smv" : @"video/x-smv", + @"smzip" : @"application/vnd.stepmania.package", + @"snd" : @"audio/basic", + @"snf" : @"application/x-font-snf", + @"so" : @"application/octet-stream", + @"spc" : @"application/x-pkcs7-certificates", + @"spf" : @"application/vnd.yamaha.smaf-phrase", + @"spl" : @"application/x-futuresplash", + @"spot" : @"text/vnd.in3d.spot", + @"spp" : @"application/scvp-vp-response", + @"spq" : @"application/scvp-vp-request", + @"spx" : @"audio/ogg", + @"sql" : @"application/x-sql", + @"src" : @"application/x-wais-source", + @"srt" : @"application/x-subrip", + @"sru" : @"application/sru+xml", + @"srx" : @"application/sparql-results+xml", + @"ssdl" : @"application/ssdl+xml", + @"sse" : @"application/vnd.kodak-descriptor", + @"ssf" : @"application/vnd.epson.ssf", + @"ssml" : @"application/ssml+xml", + @"st" : @"application/vnd.sailingtracker.track", + @"stc" : @"application/vnd.sun.xml.calc.template", + @"std" : @"application/vnd.sun.xml.draw.template", + @"stf" : @"application/vnd.wt.stf", + @"sti" : @"application/vnd.sun.xml.impress.template", + @"stk" : @"application/hyperstudio", + @"stl" : @"application/vnd.ms-pki.stl", + @"str" : @"application/vnd.pg.format", + @"stw" : @"application/vnd.sun.xml.writer.template", + @"sub" : @"text/vnd.dvb.subtitle", + @"sus" : @"application/vnd.sus-calendar", + @"susp" : @"application/vnd.sus-calendar", + @"sv4cpio" : @"application/x-sv4cpio", + @"sv4crc" : @"application/x-sv4crc", + @"svc" : @"application/vnd.dvb.service", + @"svd" : @"application/vnd.svd", + @"svg" : @"image/svg+xml", + @"svgz" : @"image/svg+xml", + @"swa" : @"application/x-director", + @"swf" : @"application/x-shockwave-flash", + @"swi" : @"application/vnd.aristanetworks.swi", + @"sxc" : @"application/vnd.sun.xml.calc", + @"sxd" : @"application/vnd.sun.xml.draw", + @"sxg" : @"application/vnd.sun.xml.writer.global", + @"sxi" : @"application/vnd.sun.xml.impress", + @"sxm" : @"application/vnd.sun.xml.math", + @"sxw" : @"application/vnd.sun.xml.writer", + @"t" : @"text/troff", + @"t3" : @"application/x-t3vm-image", + @"taglet" : @"application/vnd.mynfc", + @"tao" : @"application/vnd.tao.intent-module-archive", + @"tar" : @"application/x-tar", + @"tcap" : @"application/vnd.3gpp2.tcap", + @"tcl" : @"application/x-tcl", + @"teacher" : @"application/vnd.smart.teacher", + @"tei" : @"application/tei+xml", + @"teicorpus" : @"application/tei+xml", + @"tex" : @"application/x-tex", + @"texi" : @"application/x-texinfo", + @"texinfo" : @"application/x-texinfo", + @"text" : @"text/plain", + @"tfi" : @"application/thraud+xml", + @"tfm" : @"application/x-tex-tfm", + @"tga" : @"image/x-tga", + @"thmx" : @"application/vnd.ms-officetheme", + @"tif" : @"image/tiff", + @"tiff" : @"image/tiff", + @"tmo" : @"application/vnd.tmobile-livetv", + @"torrent" : @"application/x-bittorrent", + @"tpl" : @"application/vnd.groove-tool-template", + @"tpt" : @"application/vnd.trid.tpt", + @"tr" : @"text/troff", + @"tra" : @"application/vnd.trueapp", + @"trm" : @"application/x-msterminal", + @"tsd" : @"application/timestamped-data", + @"tsv" : @"text/tab-separated-values", + @"ttc" : @"application/x-font-ttf", + @"ttf" : @"application/x-font-ttf", + @"ttl" : @"text/turtle", + @"twd" : @"application/vnd.simtech-mindmapper", + @"twds" : @"application/vnd.simtech-mindmapper", + @"txd" : @"application/vnd.genomatix.tuxedo", + @"txf" : @"application/vnd.mobius.txf", + @"txt" : @"text/plain", + @"u32" : @"application/x-authorware-bin", + @"udeb" : @"application/x-debian-package", + @"ufd" : @"application/vnd.ufdl", + @"ufdl" : @"application/vnd.ufdl", + @"ulx" : @"application/x-glulx", + @"umj" : @"application/vnd.umajin", + @"unityweb" : @"application/vnd.unity", + @"uoml" : @"application/vnd.uoml+xml", + @"uri" : @"text/uri-list", + @"uris" : @"text/uri-list", + @"urls" : @"text/uri-list", + @"ustar" : @"application/x-ustar", + @"utz" : @"application/vnd.uiq.theme", + @"uu" : @"text/x-uuencode", + @"uva" : @"audio/vnd.dece.audio", + @"uvd" : @"application/vnd.dece.data", + @"uvf" : @"application/vnd.dece.data", + @"uvg" : @"image/vnd.dece.graphic", + @"uvh" : @"video/vnd.dece.hd", + @"uvi" : @"image/vnd.dece.graphic", + @"uvm" : @"video/vnd.dece.mobile", + @"uvp" : @"video/vnd.dece.pd", + @"uvs" : @"video/vnd.dece.sd", + @"uvt" : @"application/vnd.dece.ttml+xml", + @"uvu" : @"video/vnd.uvvu.mp4", + @"uvv" : @"video/vnd.dece.video", + @"uvva" : @"audio/vnd.dece.audio", + @"uvvd" : @"application/vnd.dece.data", + @"uvvf" : @"application/vnd.dece.data", + @"uvvg" : @"image/vnd.dece.graphic", + @"uvvh" : @"video/vnd.dece.hd", + @"uvvi" : @"image/vnd.dece.graphic", + @"uvvm" : @"video/vnd.dece.mobile", + @"uvvp" : @"video/vnd.dece.pd", + @"uvvs" : @"video/vnd.dece.sd", + @"uvvt" : @"application/vnd.dece.ttml+xml", + @"uvvu" : @"video/vnd.uvvu.mp4", + @"uvvv" : @"video/vnd.dece.video", + @"uvvx" : @"application/vnd.dece.unspecified", + @"uvvz" : @"application/vnd.dece.zip", + @"uvx" : @"application/vnd.dece.unspecified", + @"uvz" : @"application/vnd.dece.zip", + @"vcard" : @"text/vcard", + @"vcd" : @"application/x-cdlink", + @"vcf" : @"text/x-vcard", + @"vcg" : @"application/vnd.groove-vcard", + @"vcs" : @"text/x-vcalendar", + @"vcx" : @"application/vnd.vcx", + @"vis" : @"application/vnd.visionary", + @"viv" : @"video/vnd.vivo", + @"vob" : @"video/x-ms-vob", + @"vor" : @"application/vnd.stardivision.writer", + @"vox" : @"application/x-authorware-bin", + @"vrml" : @"model/vrml", + @"vsd" : @"application/vnd.visio", + @"vsf" : @"application/vnd.vsf", + @"vss" : @"application/vnd.visio", + @"vst" : @"application/vnd.visio", + @"vsw" : @"application/vnd.visio", + @"vtu" : @"model/vnd.vtu", + @"vxml" : @"application/voicexml+xml", + @"w3d" : @"application/x-director", + @"wad" : @"application/x-doom", + @"wav" : @"audio/x-wav", + @"wax" : @"audio/x-ms-wax", + @"wbmp" : @"image/vnd.wap.wbmp", + @"wbs" : @"application/vnd.criticaltools.wbs+xml", + @"wbxml" : @"application/vnd.wap.wbxml", + @"wcm" : @"application/vnd.ms-works", + @"wdb" : @"application/vnd.ms-works", + @"wdp" : @"image/vnd.ms-photo", + @"weba" : @"audio/webm", + @"webm" : @"video/webm", + @"webp" : @"image/webp", + @"wg" : @"application/vnd.pmi.widget", + @"wgt" : @"application/widget", + @"wks" : @"application/vnd.ms-works", + @"wm" : @"video/x-ms-wm", + @"wma" : @"audio/x-ms-wma", + @"wmd" : @"application/x-ms-wmd", + @"wmf" : @"application/x-msmetafile", + @"wml" : @"text/vnd.wap.wml", + @"wmlc" : @"application/vnd.wap.wmlc", + @"wmls" : @"text/vnd.wap.wmlscript", + @"wmlsc" : @"application/vnd.wap.wmlscriptc", + @"wmv" : @"video/x-ms-wmv", + @"wmx" : @"video/x-ms-wmx", + @"wmz" : @"application/x-msmetafile", + @"woff" : @"application/font-woff", + @"wpd" : @"application/vnd.wordperfect", + @"wpl" : @"application/vnd.ms-wpl", + @"wps" : @"application/vnd.ms-works", + @"wqd" : @"application/vnd.wqd", + @"wri" : @"application/x-mswrite", + @"wrl" : @"model/vrml", + @"wsdl" : @"application/wsdl+xml", + @"wspolicy" : @"application/wspolicy+xml", + @"wtb" : @"application/vnd.webturbo", + @"wvx" : @"video/x-ms-wvx", + @"x32" : @"application/x-authorware-bin", + @"x3d" : @"model/x3d+xml", + @"x3db" : @"model/x3d+binary", + @"x3dbz" : @"model/x3d+binary", + @"x3dv" : @"model/x3d+vrml", + @"x3dvz" : @"model/x3d+vrml", + @"x3dz" : @"model/x3d+xml", + @"xaml" : @"application/xaml+xml", + @"xap" : @"application/x-silverlight-app", + @"xar" : @"application/vnd.xara", + @"xbap" : @"application/x-ms-xbap", + @"xbd" : @"application/vnd.fujixerox.docuworks.binder", + @"xbm" : @"image/x-xbitmap", + @"xdf" : @"application/xcap-diff+xml", + @"xdm" : @"application/vnd.syncml.dm+xml", + @"xdp" : @"application/vnd.adobe.xdp+xml", + @"xdssc" : @"application/dssc+xml", + @"xdw" : @"application/vnd.fujixerox.docuworks", + @"xenc" : @"application/xenc+xml", + @"xer" : @"application/patch-ops-error+xml", + @"xfdf" : @"application/vnd.adobe.xfdf", + @"xfdl" : @"application/vnd.xfdl", + @"xht" : @"application/xhtml+xml", + @"xhtml" : @"application/xhtml+xml", + @"xhvml" : @"application/xv+xml", + @"xif" : @"image/vnd.xiff", + @"xla" : @"application/vnd.ms-excel", + @"xlam" : @"application/vnd.ms-excel.addin.macroenabled.12", + @"xlc" : @"application/vnd.ms-excel", + @"xlf" : @"application/x-xliff+xml", + @"xlm" : @"application/vnd.ms-excel", + @"xls" : @"application/vnd.ms-excel", + @"xlsb" : @"application/vnd.ms-excel.sheet.binary.macroenabled.12", + @"xlsm" : @"application/vnd.ms-excel.sheet.macroenabled.12", + @"xlsx" : @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + @"xlt" : @"application/vnd.ms-excel", + @"xltm" : @"application/vnd.ms-excel.template.macroenabled.12", + @"xltx" : @"application/vnd.openxmlformats-officedocument.spreadsheetml.template", + @"xlw" : @"application/vnd.ms-excel", + @"xm" : @"audio/xm", + @"xml" : @"application/xml", + @"xo" : @"application/vnd.olpc-sugar", + @"xop" : @"application/xop+xml", + @"xpi" : @"application/x-xpinstall", + @"xpl" : @"application/xproc+xml", + @"xpm" : @"image/x-xpixmap", + @"xpr" : @"application/vnd.is-xpr", + @"xps" : @"application/vnd.ms-xpsdocument", + @"xpw" : @"application/vnd.intercon.formnet", + @"xpx" : @"application/vnd.intercon.formnet", + @"xsl" : @"application/xml", + @"xslt" : @"application/xslt+xml", + @"xsm" : @"application/vnd.syncml+xml", + @"xspf" : @"application/xspf+xml", + @"xul" : @"application/vnd.mozilla.xul+xml", + @"xvm" : @"application/xv+xml", + @"xvml" : @"application/xv+xml", + @"xwd" : @"image/x-xwindowdump", + @"xyz" : @"chemical/x-xyz", + @"xz" : @"application/x-xz", + @"yang" : @"application/yang", + @"yin" : @"application/yin+xml", + @"z1" : @"application/x-zmachine", + @"z2" : @"application/x-zmachine", + @"z3" : @"application/x-zmachine", + @"z4" : @"application/x-zmachine", + @"z5" : @"application/x-zmachine", + @"z6" : @"application/x-zmachine", + @"z7" : @"application/x-zmachine", + @"z8" : @"application/x-zmachine", + @"zaz" : @"application/vnd.zzazz.deck+xml", + @"zip" : OWSMimeTypeApplicationZip, + @"zir" : @"application/vnd.zul", + @"zirz" : @"application/vnd.zul", + @"zmm" : @"application/vnd.handheld-entertainment+xml", + }; + }); + return result; +} + ++ (nullable NSString *)fileExtensionForMIMETypeViaLookup:(NSString *)mimeType +{ + return [[self genericMIMETypesToExtensionTypes] objectForKey:mimeType]; +} + ++ (nullable NSString *)fileExtensionForMIMEType:(NSString *)mimeType +{ + // Try to deduce the file extension by using a lookup table. + // + // This should be more accurate than deducing the file extension by + // converting to a UTI type. For example, .m4a files will have a + // UTI type of kUTTypeMPEG4Audio which incorrectly yields the file + // extension .mp4 instead of .m4a. + NSString *_Nullable fileExtension = [self fileExtensionForMIMETypeViaLookup:mimeType]; + if (!fileExtension) { + // Try to deduce the file extension by converting to a UTI type. + fileExtension = [self fileExtensionForMIMETypeViaUTIType:mimeType]; + } + return fileExtension; +} + ++ (nullable NSString *)utiTypeForFileExtension:(NSString *)fileExtension +{ + OWSAssertDebug(fileExtension.length > 0); + + NSString *_Nullable utiType = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag( + kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL); + return utiType; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Mention.swift b/SignalUtilitiesKit/Mention.swift new file mode 100644 index 000000000..c064a5122 --- /dev/null +++ b/SignalUtilitiesKit/Mention.swift @@ -0,0 +1,15 @@ + +@objc(LKMention) +public final class Mention : NSObject { + @objc public let publicKey: String + @objc public let displayName: String + + @objc public init(publicKey: String, displayName: String) { + self.publicKey = publicKey + self.displayName = displayName + } + + @objc public func isContained(in string: String) -> Bool { + return string.contains(displayName) + } +} diff --git a/SignalUtilitiesKit/MentionsManager.swift b/SignalUtilitiesKit/MentionsManager.swift new file mode 100644 index 000000000..bc1588e0c --- /dev/null +++ b/SignalUtilitiesKit/MentionsManager.swift @@ -0,0 +1,92 @@ +import PromiseKit + +@objc(LKMentionsManager) +public final class MentionsManager : NSObject { + + /// A mapping from thread ID to set of user hex encoded public keys. + /// + /// - Note: Should only be accessed from the main queue to avoid race conditions. + @objc public static var userPublicKeyCache: [String:Set] = [:] + + internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + + // MARK: Settings + private static var userIDScanLimit: UInt = 4096 + + // MARK: Initialization + private override init() { } + + // MARK: Implementation + @objc public static func cache(_ publicKey: String, for threadID: String) { + if let cache = userPublicKeyCache[threadID] { + userPublicKeyCache[threadID] = cache.union([ publicKey ]) + } else { + userPublicKeyCache[threadID] = [ publicKey ] + } + } + + @objc public static func getMentionCandidates(for query: String, in threadID: String) -> [Mention] { + // Prepare + guard let cache = userPublicKeyCache[threadID] else { return [] } + var candidates: [Mention] = [] + // Gather candidates + var publicChat: OpenGroup? + storage.dbReadConnection.read { transaction in + publicChat = LokiDatabaseUtilities.getPublicChat(for: threadID, in: transaction) + } + storage.dbReadConnection.read { transaction in + candidates = cache.flatMap { publicKey in + let uncheckedDisplayName: String? + if let publicChat = publicChat { + uncheckedDisplayName = UserDisplayNameUtilities.getPublicChatDisplayName(for: publicKey, in: publicChat.channel, on: publicChat.server) + } else { + uncheckedDisplayName = UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) + } + guard let displayName = uncheckedDisplayName else { return nil } + guard !displayName.hasPrefix("Anonymous") else { return nil } + return Mention(publicKey: publicKey, displayName: displayName) + } + } + candidates = candidates.filter { $0.publicKey != getUserHexEncodedPublicKey() } + // Sort alphabetically first + candidates.sort { $0.displayName < $1.displayName } + if query.count >= 2 { + // Filter out any non-matching candidates + candidates = candidates.filter { $0.displayName.lowercased().contains(query.lowercased()) } + // Sort based on where in the candidate the query occurs + candidates.sort { + $0.displayName.lowercased().range(of: query.lowercased())!.lowerBound < $1.displayName.lowercased().range(of: query.lowercased())!.lowerBound + } + } + // Return + return candidates + } + + @objc public static func populateUserPublicKeyCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadTransaction? = nil) { + var result: Set = [] + func populate(in transaction: YapDatabaseReadTransaction) { + guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return } + if let groupThread = thread as? TSGroupThread, groupThread.groupModel.groupType == .closedGroup { + result = result.union(groupThread.groupModel.groupMemberIds).subtracting([ getUserHexEncodedPublicKey() ]) + } else { + guard userPublicKeyCache[threadID] == nil else { return } + let interactions = transaction.ext(TSMessageDatabaseViewExtensionName) as! YapDatabaseViewTransaction + interactions.enumerateKeysAndObjects(inGroup: threadID) { _, _, object, index, _ in + guard let message = object as? TSIncomingMessage, index < userIDScanLimit else { return } + result.insert(message.authorId) + } + } + result.insert(getUserHexEncodedPublicKey()) + } + if let transaction = transaction { + populate(in: transaction) + } else { + storage.dbReadConnection.read { transaction in + populate(in: transaction) + } + } + if !result.isEmpty { + userPublicKeyCache[threadID] = result + } + } +} diff --git a/SignalUtilitiesKit/MessageSender+Promise.swift b/SignalUtilitiesKit/MessageSender+Promise.swift new file mode 100644 index 000000000..ac426fbb8 --- /dev/null +++ b/SignalUtilitiesKit/MessageSender+Promise.swift @@ -0,0 +1,25 @@ +// +// 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 { + let promise: Promise = 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 + } +} diff --git a/SignalUtilitiesKit/MessageSenderJobQueue.swift b/SignalUtilitiesKit/MessageSenderJobQueue.swift new file mode 100644 index 000000000..c6136f0bd --- /dev/null +++ b/SignalUtilitiesKit/MessageSenderJobQueue.swift @@ -0,0 +1,256 @@ +// +// 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) + } + } + } +} diff --git a/SignalUtilitiesKit/MessageWrapper.swift b/SignalUtilitiesKit/MessageWrapper.swift new file mode 100644 index 000000000..f19626906 --- /dev/null +++ b/SignalUtilitiesKit/MessageWrapper.swift @@ -0,0 +1,72 @@ + +public enum MessageWrapper { + + public enum Error : LocalizedError { + case failedToWrapData + case failedToWrapMessageInEnvelope + case failedToWrapEnvelopeInWebSocketMessage + case failedToUnwrapData + + public var errorDescription: String? { + switch self { + case .failedToWrapData: return "Failed to wrap data." + case .failedToWrapMessageInEnvelope: return "Failed to wrap message in envelope." + case .failedToWrapEnvelopeInWebSocketMessage: return "Failed to wrap envelope in web socket message." + case .failedToUnwrapData: return "Failed to unwrap data." + } + } + } + + /// Wraps `message` in an `SSKProtoEnvelope` and then a `WebSocketProtoWebSocketMessage` to match the desktop application. + public static func wrap(message: SignalMessage) throws -> Data { + do { + let envelope = try createEnvelope(around: message) + let webSocketMessage = try createWebSocketMessage(around: envelope) + return try webSocketMessage.serializedData() + } catch let error { + throw error as? Error ?? Error.failedToWrapData + } + } + + private static func createEnvelope(around message: SignalMessage) throws -> SSKProtoEnvelope { + do { + let builder = SSKProtoEnvelope.builder(type: message.type, timestamp: message.timestamp) + builder.setSource(message.senderPublicKey) + builder.setSourceDevice(message.senderDeviceID) + if let content = try Data(base64Encoded: message.content, options: .ignoreUnknownCharacters) { + builder.setContent(content) + } else { + throw Error.failedToWrapMessageInEnvelope + } + return try builder.build() + } catch let error { + print("[Loki] Failed to wrap message in envelope: \(error).") + throw Error.failedToWrapMessageInEnvelope + } + } + + private static func createWebSocketMessage(around envelope: SSKProtoEnvelope) throws -> WebSocketProtoWebSocketMessage { + do { + let requestBuilder = WebSocketProtoWebSocketRequestMessage.builder(verb: "PUT", path: "/api/v1/message", requestID: UInt64.random(in: 1.. SSKProtoEnvelope { + do { + let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data) + let envelope = webSocketMessage.request!.body! + return try SSKProtoEnvelope.parseData(envelope) + } catch let error { + print("[Loki] Failed to unwrap data: \(error).") + throw Error.failedToUnwrapData + } + } +} diff --git a/SignalUtilitiesKit/Meta/Info.plist b/SignalUtilitiesKit/Meta/Info.plist new file mode 100644 index 000000000..9bcb24442 --- /dev/null +++ b/SignalUtilitiesKit/Meta/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h new file mode 100644 index 000000000..9e44cd19f --- /dev/null +++ b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h @@ -0,0 +1,72 @@ +#import + +FOUNDATION_EXPORT double SignalUtilitiesKitVersionNumber; +FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[]; + +@import SessionMessagingKit; +@import SessionProtocolKit; +@import SessionSnodeKit; +@import SessionUtilitiesKit; + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/SignalUtilitiesKit/Mnemonic.swift b/SignalUtilitiesKit/Mnemonic.swift new file mode 100644 index 000000000..d7b01aa7c --- /dev/null +++ b/SignalUtilitiesKit/Mnemonic.swift @@ -0,0 +1,162 @@ +import CryptoSwift + +/// Based on [mnemonic.js](https://github.com/loki-project/loki-messenger/blob/development/libloki/modules/mnemonic.js) . +public enum Mnemonic { + + public struct Language : Hashable { + fileprivate let filename: String + fileprivate let prefixLength: UInt + + public static let english = Language(filename: "english", prefixLength: 3) + public static let japanese = Language(filename: "japanese", prefixLength: 3) + public static let portuguese = Language(filename: "portuguese", prefixLength: 4) + public static let spanish = Language(filename: "spanish", prefixLength: 4) + + private static var wordSetCache: [Language:[String]] = [:] + private static var truncatedWordSetCache: [Language:[String]] = [:] + + private init(filename: String, prefixLength: UInt) { + self.filename = filename + self.prefixLength = prefixLength + } + + fileprivate func loadWordSet() -> [String] { + if let cachedResult = Language.wordSetCache[self] { + return cachedResult + } else { + let bundleID = "org.cocoapods.SessionServiceKit" + let url = Bundle(identifier: bundleID)!.url(forResource: filename, withExtension: "txt")! + let contents = try! String(contentsOf: url) + let result = contents.split(separator: ",").map { String($0) } + Language.wordSetCache[self] = result + return result + } + } + + fileprivate func loadTruncatedWordSet() -> [String] { + if let cachedResult = Language.truncatedWordSetCache[self] { + return cachedResult + } else { + let result = loadWordSet().map { $0.prefix(length: prefixLength) } + Language.truncatedWordSetCache[self] = result + return result + } + } + } + + public enum DecodingError : LocalizedError { + case generic, inputTooShort, missingLastWord, invalidWord, verificationFailed + + public var errorDescription: String? { + switch self { + case .generic: return NSLocalizedString("Something went wrong. Please check your recovery phrase and try again.", comment: "") + case .inputTooShort: return NSLocalizedString("Looks like you didn't enter enough words. Please check your recovery phrase and try again.", comment: "") + case .missingLastWord: return NSLocalizedString("You seem to be missing the last word of your recovery phrase. Please check what you entered and try again.", comment: "") + case .invalidWord: return NSLocalizedString("There appears to be an invalid word in your recovery phrase. Please check what you entered and try again.", comment: "") + case .verificationFailed: return NSLocalizedString("Your recovery phrase couldn't be verified. Please check what you entered and try again.", comment: "") + } + } + } + + public static func hash(hexEncodedString string: String, language: Language = .english) -> String { + return encode(hexEncodedString: string).split(separator: " ")[0..<3].joined(separator: " ") + } + + public static func encode(hexEncodedString string: String, language: Language = .english) -> String { + var string = string + let wordSet = language.loadWordSet() + let prefixLength = language.prefixLength + var result: [String] = [] + let n = wordSet.count + let characterCount = string.indices.count // Safe for this particular case + for chunkStartIndexAsInt in stride(from: 0, to: characterCount, by: 8) { + let chunkStartIndex = string.index(string.startIndex, offsetBy: chunkStartIndexAsInt) + let chunkEndIndex = string.index(chunkStartIndex, offsetBy: 8) + let p1 = string[string.startIndex.. String { + var words = mnemonic.split(separator: " ").map { String($0) } + let truncatedWordSet = language.loadTruncatedWordSet() + let prefixLength = language.prefixLength + var result = "" + let n = truncatedWordSet.count + // Check preconditions + guard words.count >= 12 else { throw DecodingError.inputTooShort } + guard !words.count.isMultiple(of: 3) else { throw DecodingError.missingLastWord } + // Get checksum word + let checksumWord = words.popLast()! + // Decode + for chunkStartIndex in stride(from: 0, to: words.count, by: 3) { + guard let w1 = truncatedWordSet.firstIndex(of: words[chunkStartIndex].prefix(length: prefixLength)), + let w2 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 1].prefix(length: prefixLength)), + let w3 = truncatedWordSet.firstIndex(of: words[chunkStartIndex + 2].prefix(length: prefixLength)) else { throw DecodingError.invalidWord } + let x = w1 + n * ((n - w1 + w2) % n) + n * n * ((n - w2 + w3) % n) + guard x % n == w1 else { throw DecodingError.generic } + let string = "0000000" + String(x, radix: 16) + result += swap(String(string[string.index(string.endIndex, offsetBy: -8).. String { + func toStringIndex(_ indexAsInt: Int) -> String.Index { + return x.index(x.startIndex, offsetBy: indexAsInt) + } + let p1 = x[toStringIndex(6).. Int { + let checksum = Array(x.map { $0.prefix(length: prefixLength) }.joined().utf8).crc32() + return Int(checksum) % x.count + } +} + +private extension String { + + func prefix(length: UInt) -> String { + return String(self[startIndex.. String { + return Mnemonic.hash(hexEncodedString: string) + } + + @objc(encodeHexEncodedString:) + public static func encode(hexEncodedString string: String) -> String { + return Mnemonic.encode(hexEncodedString: string) + } +} diff --git a/SignalUtilitiesKit/MultiDeviceProtocol.swift b/SignalUtilitiesKit/MultiDeviceProtocol.swift new file mode 100644 index 000000000..e4332ded9 --- /dev/null +++ b/SignalUtilitiesKit/MultiDeviceProtocol.swift @@ -0,0 +1,274 @@ +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 { + var result: Set = [] + 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) -> 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 { + let (threadPromise, threadPromiseSeal) = Promise.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 in + let message = messageSend.message + let messageSender = SSKEnvironment.shared.messageSender + let (promise, seal) = Promise.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] = [] + 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 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> { + let (promise, seal) = Promise>.pending() + func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) { + storage.dbReadConnection.read { transaction in + var destinations: Set = [] + 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 + } +} diff --git a/SignalUtilitiesKit/NSArray+Functional.h b/SignalUtilitiesKit/NSArray+Functional.h new file mode 100644 index 000000000..e8a293376 --- /dev/null +++ b/SignalUtilitiesKit/NSArray+Functional.h @@ -0,0 +1,9 @@ +#import + +@interface NSArray (Functional) + +- (BOOL)contains:(BOOL (^)(id))predicate; +- (NSArray *)filtered:(BOOL (^)(id))isIncluded; +- (NSArray *)map:(id (^)(id))transform; + +@end diff --git a/SignalUtilitiesKit/NSArray+Functional.m b/SignalUtilitiesKit/NSArray+Functional.m new file mode 100644 index 000000000..8e8e5a131 --- /dev/null +++ b/SignalUtilitiesKit/NSArray+Functional.m @@ -0,0 +1,32 @@ +#import "NSArray+Functional.h" + +@implementation NSArray (Functional) + +- (BOOL)contains:(BOOL (^)(id))predicate { + for (id object in self) { + BOOL isPredicateSatisfied = predicate(object); + if (isPredicateSatisfied) { return YES; } + } + return NO; +} + +- (NSArray *)filtered:(BOOL (^)(id))isIncluded { + NSMutableArray *result = [NSMutableArray new]; + for (id object in self) { + if (isIncluded(object)) { + [result addObject:object]; + } + } + return result; +} + +- (NSArray *)map:(id (^)(id))transform { + NSMutableArray *result = [NSMutableArray new]; + for (id object in self) { + id transformedObject = transform(object); + [result addObject:transformedObject]; + } + return result; +} + +@end diff --git a/SignalUtilitiesKit/NSArray+OWS.h b/SignalUtilitiesKit/NSArray+OWS.h new file mode 100644 index 000000000..17d2b34e2 --- /dev/null +++ b/SignalUtilitiesKit/NSArray+OWS.h @@ -0,0 +1,15 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray (OWS) + +- (NSArray *)uniqueIds; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSArray+OWS.m b/SignalUtilitiesKit/NSArray+OWS.m new file mode 100644 index 000000000..191e915c4 --- /dev/null +++ b/SignalUtilitiesKit/NSArray+OWS.m @@ -0,0 +1,26 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "NSArray+OWS.h" +#import "TSYapDatabaseObject.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSArray (OWS) + +- (NSArray *)uniqueIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (id object in self) { + OWSAssertDebug([object isKindOfClass:[TSYapDatabaseObject class]]); + TSYapDatabaseObject *dbObject = object; + [result addObject:dbObject.uniqueId]; + } + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSData+Image.h b/SignalUtilitiesKit/NSData+Image.h new file mode 100644 index 000000000..07d0899b0 --- /dev/null +++ b/SignalUtilitiesKit/NSData+Image.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (Image) + +// If mimeType is non-nil, we ensure that the magic numbers agree with the +// mimeType. ++ (BOOL)ows_isValidImageAtPath:(NSString *)filePath; ++ (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType; +- (BOOL)ows_isValidImage; +- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType; +- (NSString *_Nullable)ows_guessMimeType; + +// Returns the image size in pixels. +// +// Returns CGSizeZero on error. ++ (CGSize)imageSizeForFilePath:(NSString *)filePath mimeType:(NSString *)mimeType; + ++ (BOOL)hasAlphaForValidImageFilePath:(NSString *)filePath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSData+Image.m b/SignalUtilitiesKit/NSData+Image.m new file mode 100644 index 000000000..07f9be595 --- /dev/null +++ b/SignalUtilitiesKit/NSData+Image.m @@ -0,0 +1,420 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "NSData+Image.h" +#import "MIMETypeUtil.h" +#import "OWSFileSystem.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, ImageFormat) { + ImageFormat_Unknown, + ImageFormat_Png, + ImageFormat_Gif, + ImageFormat_Tiff, + ImageFormat_Jpeg, + ImageFormat_Bmp, +}; + +@implementation NSData (Image) + ++ (BOOL)ows_isValidImageAtPath:(NSString *)filePath +{ + return [self ows_isValidImageAtPath:filePath mimeType:nil]; +} + +- (BOOL)ows_isValidImage +{ + ImageFormat imageFormat = [self ows_guessImageFormat]; + + BOOL isAnimated = imageFormat == ImageFormat_Gif; + + const NSUInteger kMaxFileSize + = (isAnimated ? OWSMediaUtils.kMaxFileSizeAnimatedImage : OWSMediaUtils.kMaxFileSizeImage); + NSUInteger fileSize = self.length; + if (fileSize > kMaxFileSize) { + OWSLogWarn(@"Oversize image."); + return NO; + } + + if (![self ows_isValidImageWithMimeType:nil imageFormat:imageFormat]) { + return NO; + } + + if (![self ows_hasValidImageDimensionsWithIsAnimated:isAnimated]) { + return NO; + } + + return YES; +} + ++ (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType +{ + if (mimeType.length < 1) { + NSString *fileExtension = [filePath pathExtension].lowercaseString; + mimeType = [MIMETypeUtil mimeTypeForFileExtension:fileExtension]; + } + if (mimeType.length < 1) { + OWSLogError(@"Image has unknown MIME type."); + return NO; + } + NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:filePath]; + if (!fileSize) { + OWSLogError(@"Could not determine file size."); + return NO; + } + + BOOL isAnimated = [MIMETypeUtil isSupportedAnimatedMIMEType:mimeType]; + if (isAnimated) { + if (fileSize.unsignedIntegerValue > OWSMediaUtils.kMaxFileSizeAnimatedImage) { + OWSLogWarn(@"Oversize animated image."); + return NO; + } + } else if ([MIMETypeUtil isSupportedImageMIMEType:mimeType]) { + if (fileSize.unsignedIntegerValue > OWSMediaUtils.kMaxFileSizeImage) { + OWSLogWarn(@"Oversize still image."); + return NO; + } + } else { + OWSLogError(@"Image has unsupported MIME type."); + return NO; + } + + NSError *error = nil; + NSData *_Nullable data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error]; + if (!data || error) { + OWSLogError(@"Could not read image data: %@", error); + return NO; + } + + if (![data ows_isValidImageWithMimeType:mimeType]) { + return NO; + } + + if (![self ows_hasValidImageDimensionsAtPath:filePath isAnimated:isAnimated]) { + OWSLogError(@"%@ image had invalid dimensions.", self.logTag); + return NO; + } + + return YES; +} + +- (BOOL)ows_hasValidImageDimensionsWithIsAnimated:(BOOL)isAnimated +{ + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self, NULL); + if (imageSource == NULL) { + return NO; + } + BOOL result = [NSData ows_hasValidImageDimensionWithImageSource:imageSource isAnimated:isAnimated]; + CFRelease(imageSource); + return result; +} + ++ (BOOL)ows_hasValidImageDimensionsAtPath:(NSString *)path isAnimated:(BOOL)isAnimated +{ + NSURL *url = [NSURL fileURLWithPath:path]; + if (!url) { + return NO; + } + + CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL); + if (imageSource == NULL) { + return NO; + } + BOOL result = [self ows_hasValidImageDimensionWithImageSource:imageSource isAnimated:isAnimated]; + CFRelease(imageSource); + return result; +} + ++ (BOOL)ows_hasValidImageDimensionWithImageSource:(CGImageSourceRef)imageSource isAnimated:(BOOL)isAnimated +{ + OWSAssertDebug(imageSource); + + NSDictionary *imageProperties + = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); + + if (!imageProperties) { + return NO; + } + + NSNumber *widthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelWidth]; + if (!widthNumber) { + OWSLogError(@"widthNumber was unexpectedly nil"); + return NO; + } + CGFloat width = widthNumber.floatValue; + + NSNumber *heightNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelHeight]; + if (!heightNumber) { + OWSLogError(@"heightNumber was unexpectedly nil"); + return NO; + } + CGFloat height = heightNumber.floatValue; + + /* The number of bits in each color sample of each pixel. The value of this + * key is a CFNumberRef. */ + NSNumber *depthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyDepth]; + if (!depthNumber) { + OWSLogError(@"depthNumber was unexpectedly nil"); + return NO; + } + NSUInteger depthBits = depthNumber.unsignedIntegerValue; + // This should usually be 1. + CGFloat depthBytes = (CGFloat)ceil(depthBits / 8.f); + + /* The color model of the image such as "RGB", "CMYK", "Gray", or "Lab". + * The value of this key is CFStringRef. */ + NSString *colorModel = imageProperties[(__bridge NSString *)kCGImagePropertyColorModel]; + if (!colorModel) { + OWSLogError(@"colorModel was unexpectedly nil"); + return NO; + } + if (![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelRGB] + && ![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelGray]) { + OWSLogError(@"Invalid colorModel: %@", colorModel); + return NO; + } + + // We only support (A)RGB and (A)Grayscale, so worst case is 4. + const CGFloat kWorseCastComponentsPerPixel = 4; + CGFloat bytesPerPixel = kWorseCastComponentsPerPixel * depthBytes; + + const CGFloat kExpectedBytePerPixel = 4; + CGFloat kMaxValidImageDimension + = (isAnimated ? OWSMediaUtils.kMaxAnimatedImageDimensions : OWSMediaUtils.kMaxStillImageDimensions); + CGFloat kMaxBytes = kMaxValidImageDimension * kMaxValidImageDimension * kExpectedBytePerPixel; + CGFloat actualBytes = width * height * bytesPerPixel; + if (actualBytes > kMaxBytes) { + OWSLogWarn(@"invalid dimensions width: %f, height %f, bytesPerPixel: %f", width, height, bytesPerPixel); + return NO; + } + + return YES; +} + +- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType +{ + ImageFormat imageFormat = [self ows_guessImageFormat]; + return [self ows_isValidImageWithMimeType:mimeType imageFormat:imageFormat]; +} + +- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType imageFormat:(ImageFormat)imageFormat +{ + // Don't trust the file extension; iOS (e.g. UIKit, Core Graphics) will happily + // load a .gif with a .png file extension. + // + // Instead, use the "magic numbers" in the file data to determine the image format. + // + // If the image has a declared MIME type, ensure that agrees with the + // deduced image format. + switch (imageFormat) { + case ImageFormat_Unknown: + return NO; + case ImageFormat_Png: + return (mimeType == nil || [mimeType isEqualToString:OWSMimeTypeImagePng]); + case ImageFormat_Gif: + if (![self ows_hasValidGifSize]) { + return NO; + } + return (mimeType == nil || [mimeType isEqualToString:OWSMimeTypeImageGif]); + case ImageFormat_Tiff: + return (mimeType == nil || [mimeType isEqualToString:OWSMimeTypeImageTiff1] || + [mimeType isEqualToString:OWSMimeTypeImageTiff2]); + case ImageFormat_Jpeg: + return (mimeType == nil || [mimeType isEqualToString:OWSMimeTypeImageJpeg]); + case ImageFormat_Bmp: + return (mimeType == nil || [mimeType isEqualToString:OWSMimeTypeImageBmp1] || + [mimeType isEqualToString:OWSMimeTypeImageBmp2]); + } +} + +- (ImageFormat)ows_guessImageFormat +{ + const NSUInteger kTwoBytesLength = 2; + if (self.length < kTwoBytesLength) { + return ImageFormat_Unknown; + } + + unsigned char bytes[kTwoBytesLength]; + [self getBytes:&bytes range:NSMakeRange(0, kTwoBytesLength)]; + + unsigned char byte0 = bytes[0]; + unsigned char byte1 = bytes[1]; + + if (byte0 == 0x47 && byte1 == 0x49) { + return ImageFormat_Gif; + } else if (byte0 == 0x89 && byte1 == 0x50) { + return ImageFormat_Png; + } else if (byte0 == 0xff && byte1 == 0xd8) { + return ImageFormat_Jpeg; + } else if (byte0 == 0x42 && byte1 == 0x4d) { + return ImageFormat_Bmp; + } else if (byte0 == 0x4D && byte1 == 0x4D) { + // Motorola byte order TIFF + return ImageFormat_Tiff; + } else if (byte0 == 0x49 && byte1 == 0x49) { + // Intel byte order TIFF + return ImageFormat_Tiff; + } + + return ImageFormat_Unknown; +} + +- (NSString *_Nullable)ows_guessMimeType +{ + ImageFormat format = [self ows_guessImageFormat]; + switch (format) { + case ImageFormat_Gif: return OWSMimeTypeImageGif; + case ImageFormat_Png: return OWSMimeTypeImagePng; + case ImageFormat_Jpeg: return OWSMimeTypeImageJpeg; + default: return nil; + } +} + ++ (BOOL)ows_areByteArraysEqual:(NSUInteger)length left:(unsigned char *)left right:(unsigned char *)right +{ + for (NSUInteger i = 0; i < length; i++) { + if (left[i] != right[i]) { + return NO; + } + } + return YES; +} + +// Parse the GIF header to prevent the "GIF of death" issue. +// +// See: https://blog.flanker017.me/cve-2017-2416-gif-remote-exec/ +// See: https://www.w3.org/Graphics/GIF/spec-gif89a.txt +- (BOOL)ows_hasValidGifSize +{ + const NSUInteger kSignatureLength = 3; + const NSUInteger kVersionLength = 3; + const NSUInteger kWidthLength = 2; + const NSUInteger kHeightLength = 2; + const NSUInteger kPrefixLength = kSignatureLength + kVersionLength; + const NSUInteger kBufferLength = kSignatureLength + kVersionLength + kWidthLength + kHeightLength; + + if (self.length < kBufferLength) { + return NO; + } + + unsigned char bytes[kBufferLength]; + [self getBytes:&bytes range:NSMakeRange(0, kBufferLength)]; + + unsigned char kGif87APrefix[kPrefixLength] = { + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, + }; + unsigned char kGif89APrefix[kPrefixLength] = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, + }; + if (![NSData ows_areByteArraysEqual:kPrefixLength left:bytes right:kGif87APrefix] + && ![NSData ows_areByteArraysEqual:kPrefixLength left:bytes right:kGif89APrefix]) { + return NO; + } + NSUInteger width = ((NSUInteger)bytes[kPrefixLength + 0]) | (((NSUInteger)bytes[kPrefixLength + 1] << 8)); + NSUInteger height = ((NSUInteger)bytes[kPrefixLength + 2]) | (((NSUInteger)bytes[kPrefixLength + 3] << 8)); + + // We need to ensure that the image size is "reasonable". + // We impose an arbitrary "very large" limit on image size + // to eliminate harmful values. + const NSUInteger kMaxValidSize = 1 << 18; + + return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize); +} + ++ (CGSize)imageSizeForFilePath:(NSString *)filePath mimeType:(NSString *)mimeType +{ + if (![NSData ows_isValidImageAtPath:filePath mimeType:mimeType]) { + OWSLogError(@"Invalid image."); + return CGSizeZero; + } + NSURL *url = [NSURL fileURLWithPath:filePath]; + + // With CGImageSource we avoid loading the whole image into memory. + CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL); + if (!source) { + OWSFailDebug(@"Could not load image: %@", url); + return CGSizeZero; + } + + NSDictionary *options = @{ + (NSString *)kCGImageSourceShouldCache : @(NO), + }; + NSDictionary *properties + = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options); + CGSize imageSize = CGSizeZero; + if (properties) { + NSNumber *orientation = properties[(NSString *)kCGImagePropertyOrientation]; + NSNumber *width = properties[(NSString *)kCGImagePropertyPixelWidth]; + NSNumber *height = properties[(NSString *)kCGImagePropertyPixelHeight]; + + if (width && height) { + imageSize = CGSizeMake(width.floatValue, height.floatValue); + + if (orientation) { + imageSize = [self applyImageOrientation:(UIImageOrientation)orientation.intValue toImageSize:imageSize]; + } + } else { + OWSFailDebug(@"Could not determine size of image: %@", url); + } + } + CFRelease(source); + return imageSize; +} + ++ (CGSize)applyImageOrientation:(UIImageOrientation)orientation toImageSize:(CGSize)imageSize +{ + switch (orientation) { + case UIImageOrientationUp: // EXIF = 1 + case UIImageOrientationUpMirrored: // EXIF = 2 + case UIImageOrientationDown: // EXIF = 3 + case UIImageOrientationDownMirrored: // EXIF = 4 + return imageSize; + case UIImageOrientationLeftMirrored: // EXIF = 5 + case UIImageOrientationLeft: // EXIF = 6 + case UIImageOrientationRightMirrored: // EXIF = 7 + case UIImageOrientationRight: // EXIF = 8 + return CGSizeMake(imageSize.height, imageSize.width); + default: + return imageSize; + } +} + ++ (BOOL)hasAlphaForValidImageFilePath:(NSString *)filePath +{ + NSURL *url = [NSURL fileURLWithPath:filePath]; + + // With CGImageSource we avoid loading the whole image into memory. + CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL); + if (!source) { + OWSFailDebug(@"Could not load image: %@", url); + return NO; + } + + NSDictionary *options = @{ + (NSString *)kCGImageSourceShouldCache : @(NO), + }; + NSDictionary *properties + = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options); + BOOL result = NO; + if (properties) { + NSNumber *_Nullable hasAlpha = properties[(NSString *)kCGImagePropertyHasAlpha]; + if (hasAlpha) { + result = hasAlpha.boolValue; + } else { + // This is not an error; kCGImagePropertyHasAlpha is an optional + // property. + OWSLogWarn(@"Could not determine transparency of image: %@", url); + result = NO; + } + } + CFRelease(source); + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSError+MessageSending.h b/SignalUtilitiesKit/NSError+MessageSending.h new file mode 100644 index 000000000..804c512c9 --- /dev/null +++ b/SignalUtilitiesKit/NSError+MessageSending.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSError (MessageSending) + +@property (nonatomic) BOOL isRetryable; +@property (nonatomic) BOOL isFatal; +@property (nonatomic) BOOL shouldBeIgnoredForGroups; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSError+MessageSending.m b/SignalUtilitiesKit/NSError+MessageSending.m new file mode 100644 index 000000000..04f84e03a --- /dev/null +++ b/SignalUtilitiesKit/NSError+MessageSending.m @@ -0,0 +1,61 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "NSError+MessageSending.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +static void *kNSError_MessageSender_IsRetryable = &kNSError_MessageSender_IsRetryable; +static void *kNSError_MessageSender_ShouldBeIgnoredForGroups = &kNSError_MessageSender_ShouldBeIgnoredForGroups; +static void *kNSError_MessageSender_IsFatal = &kNSError_MessageSender_IsFatal; + +// isRetryable and isFatal are opposites but not redundant. +// +// If a group message send fails, the send will be retried if any of the errors were retryable UNLESS +// any of the errors were fatal. Fatal errors trump retryable errors. +@implementation NSError (MessageSending) + +- (BOOL)isRetryable +{ + NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsRetryable); + // This value should always be set for all errors by the time OWSSendMessageOperation + // queries it's value. If not, default to retrying in production. + return value ? [value boolValue] : YES; +} + +- (void)setIsRetryable:(BOOL)value +{ + objc_setAssociatedObject(self, kNSError_MessageSender_IsRetryable, @(value), OBJC_ASSOCIATION_COPY); +} + +- (BOOL)shouldBeIgnoredForGroups +{ + NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups); + // This value will NOT always be set for all errors by the time we query it's value. + // Default to NOT ignoring. + return value ? [value boolValue] : NO; +} + +- (void)setShouldBeIgnoredForGroups:(BOOL)value +{ + objc_setAssociatedObject(self, kNSError_MessageSender_ShouldBeIgnoredForGroups, @(value), OBJC_ASSOCIATION_COPY); +} + +- (BOOL)isFatal +{ + NSNumber *value = objc_getAssociatedObject(self, kNSError_MessageSender_IsFatal); + // This value will NOT always be set for all errors by the time we query it's value. + // Default to NOT fatal. + return value ? [value boolValue] : NO; +} + +- (void)setIsFatal:(BOOL)value +{ + objc_setAssociatedObject(self, kNSError_MessageSender_IsFatal, @(value), OBJC_ASSOCIATION_COPY); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSNotificationCenter+OWS.h b/SignalUtilitiesKit/NSNotificationCenter+OWS.h new file mode 100644 index 000000000..2a97bda43 --- /dev/null +++ b/SignalUtilitiesKit/NSNotificationCenter+OWS.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// We often use notifications as way to publish events. +// +// We never need these events to be received synchronously, +// so we should always send them asynchronously to avoid any +// possible risk of deadlock. These methods also ensure that +// the notifications are always fired on the main thread. +@interface NSNotificationCenter (OWS) + +- (void)postNotificationNameAsync:(NSNotificationName)name object:(nullable id)object; +- (void)postNotificationNameAsync:(NSNotificationName)name + object:(nullable id)object + userInfo:(nullable NSDictionary *)userInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSNotificationCenter+OWS.m b/SignalUtilitiesKit/NSNotificationCenter+OWS.m new file mode 100644 index 000000000..c406ff6b7 --- /dev/null +++ b/SignalUtilitiesKit/NSNotificationCenter+OWS.m @@ -0,0 +1,29 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "NSNotificationCenter+OWS.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSNotificationCenter (OWS) + +- (void)postNotificationNameAsync:(NSNotificationName)name object:(nullable id)object +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self postNotificationName:name object:object]; + }); +} + +- (void)postNotificationNameAsync:(NSNotificationName)name + object:(nullable id)object + userInfo:(nullable NSDictionary *)userInfo +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self postNotificationName:name object:object userInfo:userInfo]; + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSObject+Casting.h b/SignalUtilitiesKit/NSObject+Casting.h new file mode 100644 index 000000000..4b502bd31 --- /dev/null +++ b/SignalUtilitiesKit/NSObject+Casting.h @@ -0,0 +1,7 @@ +#import + +@interface NSObject (Casting) + +- (id)as:(Class)cls; + +@end diff --git a/SignalUtilitiesKit/NSObject+Casting.m b/SignalUtilitiesKit/NSObject+Casting.m new file mode 100644 index 000000000..33afb994e --- /dev/null +++ b/SignalUtilitiesKit/NSObject+Casting.m @@ -0,0 +1,10 @@ +#import "NSObject+Casting.h" + +@implementation NSObject (Casting) + +- (id)as:(Class)cls { + if ([self isKindOfClass:cls]) { return self; } + return nil; +} + +@end diff --git a/SignalUtilitiesKit/NSRegularExpression+SSK.swift b/SignalUtilitiesKit/NSRegularExpression+SSK.swift new file mode 100644 index 000000000..e4574467d --- /dev/null +++ b/SignalUtilitiesKit/NSRegularExpression+SSK.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public extension NSRegularExpression { + + @objc + public func hasMatch(input: String) -> Bool { + return self.firstMatch(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count)) != nil + } + + @objc + public class func parseFirstMatch(pattern: String, + text: String, + options: NSRegularExpression.Options = []) -> String? { + do { + let regex = try NSRegularExpression(pattern: pattern, options: options) + guard let match = regex.firstMatch(in: text, + options: [], + range: NSRange(location: 0, length: text.utf16.count)) else { + return nil + } + let matchRange = match.range(at: 1) + guard let textRange = Range(matchRange, in: text) else { + owsFailDebug("Invalid match.") + return nil + } + let substring = String(text[textRange]) + return substring + } catch { + Logger.error("Error: \(error)") + return nil + } + } + + @objc + public func parseFirstMatch(inText text: String, + options: NSRegularExpression.Options = []) -> String? { + guard let match = self.firstMatch(in: text, + options: [], + range: NSRange(location: 0, length: text.utf16.count)) else { + return nil + } + let matchRange = match.range(at: 1) + guard let textRange = Range(matchRange, in: text) else { + owsFailDebug("Invalid match.") + return nil + } + let substring = String(text[textRange]) + return substring + } +} diff --git a/SignalUtilitiesKit/NSSet+Functional.h b/SignalUtilitiesKit/NSSet+Functional.h new file mode 100644 index 000000000..14932e2ff --- /dev/null +++ b/SignalUtilitiesKit/NSSet+Functional.h @@ -0,0 +1,9 @@ +#import + +@interface NSSet (Functional) + +- (BOOL)contains:(BOOL (^)(id))predicate; +- (NSSet *)filtered:(BOOL (^)(id))isIncluded; +- (NSSet *)map:(id (^)(id))transform; + +@end diff --git a/SignalUtilitiesKit/NSSet+Functional.m b/SignalUtilitiesKit/NSSet+Functional.m new file mode 100644 index 000000000..c19d814fd --- /dev/null +++ b/SignalUtilitiesKit/NSSet+Functional.m @@ -0,0 +1,32 @@ +#import "NSSet+Functional.h" + +@implementation NSSet (Functional) + +- (BOOL)contains:(BOOL (^)(id))predicate { + for (id object in self) { + BOOL isPredicateSatisfied = predicate(object); + if (isPredicateSatisfied) { return YES; } + } + return NO; +} + +- (NSSet *)filtered:(BOOL (^)(id))isIncluded { + NSMutableSet *result = [NSMutableSet new]; + for (id object in self) { + if (isIncluded(object)) { + [result addObject:object]; + } + } + return result; +} + +- (NSSet *)map:(id (^)(id))transform { + NSMutableSet *result = [NSMutableSet new]; + for (id object in self) { + id transformedObject = transform(object); + [result addObject:transformedObject]; + } + return result; +} + +@end diff --git a/SignalUtilitiesKit/NSString+SSK.h b/SignalUtilitiesKit/NSString+SSK.h new file mode 100644 index 000000000..f1669bf85 --- /dev/null +++ b/SignalUtilitiesKit/NSString+SSK.h @@ -0,0 +1,15 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (SSK) + +- (NSString *)rtlSafeAppend:(NSString *)string; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSString+SSK.m b/SignalUtilitiesKit/NSString+SSK.m new file mode 100644 index 000000000..6f6ff0aa2 --- /dev/null +++ b/SignalUtilitiesKit/NSString+SSK.m @@ -0,0 +1,26 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "NSString+SSK.h" +#import "AppContext.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSString (SSK) + +- (NSString *)rtlSafeAppend:(NSString *)string +{ + OWSAssertDebug(string); + + if (CurrentAppContext().isRTL) { + return [string stringByAppendingString:self]; + } else { + return [self stringByAppendingString:string]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSTimer+OWS.h b/SignalUtilitiesKit/NSTimer+OWS.h new file mode 100644 index 000000000..b77332dc1 --- /dev/null +++ b/SignalUtilitiesKit/NSTimer+OWS.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSTimer (OWS) + +// This method avoids the classic NSTimer retain cycle bug +// by using a weak reference to the target. ++ (NSTimer *)weakScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(nullable id)userInfo + repeats:(BOOL)repeats; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSTimer+OWS.m b/SignalUtilitiesKit/NSTimer+OWS.m new file mode 100644 index 000000000..5af85fcb7 --- /dev/null +++ b/SignalUtilitiesKit/NSTimer+OWS.m @@ -0,0 +1,70 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "NSTimer+OWS.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSTimerProxy : NSObject + +@property (nonatomic, weak) id target; +@property (nonatomic) SEL selector; + +@end + +#pragma mark - + +@implementation NSTimerProxy + +- (void)timerFired:(NSDictionary *)userInfo +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self.target performSelector:self.selector withObject:userInfo]; +#pragma clang diagnostic pop +} + +@end + +#pragma mark - + +static void *kNSTimer_OWS_Proxy = &kNSTimer_OWS_Proxy; + +@implementation NSTimer (OWS) + +- (NSTimerProxy *)ows_proxy +{ + return objc_getAssociatedObject(self, kNSTimer_OWS_Proxy); +} + +- (void)ows_setProxy:(NSTimerProxy *)proxy +{ + OWSAssertDebug(proxy); + + objc_setAssociatedObject(self, kNSTimer_OWS_Proxy, proxy, OBJC_ASSOCIATION_RETAIN); +} + ++ (NSTimer *)weakScheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval + target:(id)target + selector:(SEL)selector + userInfo:(nullable id)userInfo + repeats:(BOOL)repeats +{ + NSTimerProxy *proxy = [NSTimerProxy new]; + proxy.target = target; + proxy.selector = selector; + NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval + target:proxy + selector:@selector(timerFired:) + userInfo:userInfo + repeats:repeats]; + [timer ows_setProxy:proxy]; + return timer; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSURLSessionDataTask+StatusCode.h b/SignalUtilitiesKit/NSURLSessionDataTask+StatusCode.h new file mode 100644 index 000000000..62718ffe3 --- /dev/null +++ b/SignalUtilitiesKit/NSURLSessionDataTask+StatusCode.h @@ -0,0 +1,15 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSURLSessionTask (StatusCode) + +- (long)statusCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSURLSessionDataTask+StatusCode.m b/SignalUtilitiesKit/NSURLSessionDataTask+StatusCode.m new file mode 100644 index 000000000..212eeac55 --- /dev/null +++ b/SignalUtilitiesKit/NSURLSessionDataTask+StatusCode.m @@ -0,0 +1,18 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "NSURLSessionDataTask+StatusCode.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSURLSessionTask (StatusCode) + +- (long)statusCode { + NSHTTPURLResponse *response = (NSHTTPURLResponse *)self.response; + return response.statusCode; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSUserDefaults+OWS.h b/SignalUtilitiesKit/NSUserDefaults+OWS.h new file mode 100644 index 000000000..562bf6788 --- /dev/null +++ b/SignalUtilitiesKit/NSUserDefaults+OWS.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSUserDefaults (OWS) + ++ (NSUserDefaults *)appUserDefaults; + ++ (void)migrateToSharedUserDefaults; + ++ (void)removeAll; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NSUserDefaults+OWS.m b/SignalUtilitiesKit/NSUserDefaults+OWS.m new file mode 100644 index 000000000..4fa8b7b80 --- /dev/null +++ b/SignalUtilitiesKit/NSUserDefaults+OWS.m @@ -0,0 +1,52 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "NSUserDefaults+OWS.h" +#import "AppContext.h" +#import "TSConstants.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSUserDefaults (OWS) + ++ (NSUserDefaults *)appUserDefaults +{ + return CurrentAppContext().appUserDefaults; +} + ++ (void)migrateToSharedUserDefaults +{ + OWSLogInfo(@""); + + NSUserDefaults *appUserDefaults = self.appUserDefaults; + + NSDictionary *dictionary = [NSUserDefaults standardUserDefaults].dictionaryRepresentation; + for (NSString *key in dictionary) { + id value = dictionary[key]; + OWSAssertDebug(value); + [appUserDefaults setObject:value forKey:key]; + } +} + ++ (void)removeAll +{ + [NSUserDefaults.standardUserDefaults removeAll]; + [self.appUserDefaults removeAll]; +} + +- (void)removeAll +{ + OWSAssertDebug(CurrentAppContext().isMainApp); + + NSDictionary *dictionary = self.dictionaryRepresentation; + for (NSString *key in dictionary) { + [self removeObjectForKey:key]; + } + [self synchronize]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/NetworkManager.swift b/SignalUtilitiesKit/NetworkManager.swift new file mode 100644 index 000000000..c1892b669 --- /dev/null +++ b/SignalUtilitiesKit/NetworkManager.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +enum NetworkManagerError: Error { + /// Wraps TSNetworkManager failure callback params in a single throwable error + case taskError(task: URLSessionDataTask, underlyingError: Error) +} + +extension NetworkManagerError { + var isNetworkError: Bool { + switch self { + case .taskError(_, let underlyingError): + return IsNSErrorNetworkFailure(underlyingError) + } + } + + var statusCode: Int { + switch self { + case .taskError(let task, _): + return task.statusCode() + } + } +} + +extension TSNetworkManager { + public typealias NetworkManagerResult = (task: URLSessionDataTask, responseObject: Any?) + + public func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> Promise { + return makePromise(request: request, queue: queue) + } + + public func makePromise(request: TSRequest, queue: DispatchQueue = DispatchQueue.main) -> Promise { + let (promise, resolver) = Promise.pending() + + self.makeRequest(request, + completionQueue: queue, + success: { task, responseObject in + resolver.fulfill((task: task, responseObject: responseObject)) + }, + failure: { task, error in + let nmError = NetworkManagerError.taskError(task: task, underlyingError: error) + let nsError: NSError = nmError as NSError + nsError.isRetryable = (error as NSError).isRetryable + resolver.reject(nsError) + }) + + return promise + } +} diff --git a/SignalUtilitiesKit/Notification+Loki.swift b/SignalUtilitiesKit/Notification+Loki.swift new file mode 100644 index 000000000..da6894d6d --- /dev/null +++ b/SignalUtilitiesKit/Notification+Loki.swift @@ -0,0 +1,52 @@ + +public extension Notification.Name { + + // State changes + public static let blockedContactsUpdated = Notification.Name("blockedContactsUpdated") + public static let contactOnlineStatusChanged = Notification.Name("contactOnlineStatusChanged") + public static let groupThreadUpdated = Notification.Name("groupThreadUpdated") + public static let threadDeleted = Notification.Name("threadDeleted") + public static let threadSessionRestoreDevicesChanged = Notification.Name("threadSessionRestoreDevicesChanged") + // Message status changes + public static let calculatingPoW = Notification.Name("calculatingPoW") + public static let routing = Notification.Name("routing") + public static let messageSending = Notification.Name("messageSending") + public static let messageSent = Notification.Name("messageSent") + public static let messageFailed = Notification.Name("messageFailed") + // Onboarding + public static let seedViewed = Notification.Name("seedViewed") + // Interaction + public static let dataNukeRequested = Notification.Name("dataNukeRequested") + // Device linking + public static let unexpectedDeviceLinkRequestReceived = Notification.Name("unexpectedDeviceLinkRequestReceived") + // Onion requests + public static let buildingPaths = Notification.Name("buildingPaths") + public static let pathsBuilt = Notification.Name("pathsBuilt") + public static let onionRequestPathCountriesLoaded = Notification.Name("onionRequestPathCountriesLoaded") +} + +@objc public extension NSNotification { + + // State changes + @objc public static let blockedContactsUpdated = Notification.Name.blockedContactsUpdated.rawValue as NSString + @objc public static let contactOnlineStatusChanged = Notification.Name.contactOnlineStatusChanged.rawValue as NSString + @objc public static let groupThreadUpdated = Notification.Name.groupThreadUpdated.rawValue as NSString + @objc public static let threadDeleted = Notification.Name.threadDeleted.rawValue as NSString + @objc public static let threadSessionRestoreDevicesChanged = Notification.Name.threadSessionRestoreDevicesChanged.rawValue as NSString + // Message statuses + @objc public static let calculatingPoW = Notification.Name.calculatingPoW.rawValue as NSString + @objc public static let routing = Notification.Name.routing.rawValue as NSString + @objc public static let messageSending = Notification.Name.messageSending.rawValue as NSString + @objc public static let messageSent = Notification.Name.messageSent.rawValue as NSString + @objc public static let messageFailed = Notification.Name.messageFailed.rawValue as NSString + // Onboarding + @objc public static let seedViewed = Notification.Name.seedViewed.rawValue as NSString + // Interaction + @objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString + // Device linking + @objc public static let unexpectedDeviceLinkRequestReceived = Notification.Name.unexpectedDeviceLinkRequestReceived.rawValue as NSString + // Onion requests + @objc public static let buildingPaths = Notification.Name.buildingPaths.rawValue as NSString + @objc public static let pathsBuilt = Notification.Name.pathsBuilt.rawValue as NSString + @objc public static let onionRequestPathCountriesLoaded = Notification.Name.onionRequestPathCountriesLoaded.rawValue as NSString +} diff --git a/SignalUtilitiesKit/NotificationsProtocol.h b/SignalUtilitiesKit/NotificationsProtocol.h new file mode 100644 index 000000000..29093da1e --- /dev/null +++ b/SignalUtilitiesKit/NotificationsProtocol.h @@ -0,0 +1,34 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSErrorMessage; +@class TSIncomingMessage; +@class TSThread; +@class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; + +@protocol ContactsManagerProtocol; + +@protocol NotificationsProtocol + +- (void)notifyUserForIncomingMessage:(TSIncomingMessage *)incomingMessage + inThread:(TSThread *)thread + transaction:(YapDatabaseReadTransaction *)transaction; + +- (void)notifyUserForErrorMessage:(TSErrorMessage *)error + thread:(TSThread *)thread + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)notifyUserForThreadlessErrorMessage:(TSErrorMessage *)error + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)clearAllNotifications; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWS2FAManager.h b/SignalUtilitiesKit/OWS2FAManager.h new file mode 100644 index 000000000..a41eb6ebc --- /dev/null +++ b/SignalUtilitiesKit/OWS2FAManager.h @@ -0,0 +1,47 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const NSNotificationName_2FAStateDidChange; + +typedef void (^OWS2FASuccess)(void); +typedef void (^OWS2FAFailure)(NSError *error); + +@class OWSPrimaryStorage; + +// This class can be safely accessed and used from any thread. +@interface OWS2FAManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + ++ (instancetype)sharedManager; + +@property (nullable, nonatomic, readonly) NSString *pinCode; + +- (BOOL)is2FAEnabled; +- (BOOL)isDueForReminder; + +// Request with service +- (void)requestEnable2FAWithPin:(NSString *)pin + success:(nullable OWS2FASuccess)success + failure:(nullable OWS2FAFailure)failure; + +// Sore local settings if, used during registration +- (void)mark2FAAsEnabledWithPin:(NSString *)pin; + +- (void)disable2FAWithSuccess:(nullable OWS2FASuccess)success failure:(nullable OWS2FAFailure)failure; + +- (void)updateRepetitionIntervalWithWasSuccessful:(BOOL)wasSuccessful; + +// used for testing +- (void)setDefaultRepetitionInterval; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWS2FAManager.m b/SignalUtilitiesKit/OWS2FAManager.m new file mode 100644 index 000000000..24b208b9e --- /dev/null +++ b/SignalUtilitiesKit/OWS2FAManager.m @@ -0,0 +1,271 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWS2FAManager.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSPrimaryStorage.h" +#import "OWSRequestFactory.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSNetworkManager.h" +#import "YapDatabaseConnection+OWS.h" +#import "SSKAsserts.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const NSNotificationName_2FAStateDidChange = @"NSNotificationName_2FAStateDidChange"; + +NSString *const kOWS2FAManager_Collection = @"kOWS2FAManager_Collection"; +NSString *const kOWS2FAManager_LastSuccessfulReminderDateKey = @"kOWS2FAManager_LastSuccessfulReminderDateKey"; +NSString *const kOWS2FAManager_PinCode = @"kOWS2FAManager_PinCode"; +NSString *const kOWS2FAManager_RepetitionInterval = @"kOWS2FAManager_RepetitionInterval"; + +const NSUInteger kHourSecs = 60 * 60; +const NSUInteger kDaySecs = kHourSecs * 24; + +@interface OWS2FAManager () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@end + +#pragma mark - + +@implementation OWS2FAManager + ++ (instancetype)sharedManager +{ + OWSAssertDebug(SSKEnvironment.shared.ows2FAManager); + + return SSKEnvironment.shared.ows2FAManager; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSAssertDebug(primaryStorage); + + _dbConnection = primaryStorage.newDatabaseConnection; + + OWSSingletonAssert(); + + return self; +} + +#pragma mark - Dependencies + +- (TSNetworkManager *)networkManager { + OWSAssertDebug(SSKEnvironment.shared.networkManager); + + return SSKEnvironment.shared.networkManager; +} + +- (TSAccountManager *)tsAccountManager { + return TSAccountManager.sharedInstance; +} + +#pragma mark - + +- (nullable NSString *)pinCode +{ + return [self.dbConnection objectForKey:kOWS2FAManager_PinCode inCollection:kOWS2FAManager_Collection]; +} + +- (BOOL)is2FAEnabled +{ + return self.pinCode != nil; +} + +- (void)set2FANotEnabled +{ + [self.dbConnection removeObjectForKey:kOWS2FAManager_PinCode inCollection:kOWS2FAManager_Collection]; + + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationName_2FAStateDidChange + object:nil + userInfo:nil]; + + [[self.tsAccountManager updateAccountAttributes] retainUntilComplete]; +} + +- (void)mark2FAAsEnabledWithPin:(NSString *)pin +{ + OWSAssertDebug(pin.length > 0); + + [self.dbConnection setObject:pin forKey:kOWS2FAManager_PinCode inCollection:kOWS2FAManager_Collection]; + + // Schedule next reminder relative to now + self.lastSuccessfulReminderDate = [NSDate new]; + + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationName_2FAStateDidChange + object:nil + userInfo:nil]; + + [[self.tsAccountManager updateAccountAttributes] retainUntilComplete]; +} + +- (void)requestEnable2FAWithPin:(NSString *)pin + success:(nullable OWS2FASuccess)success + failure:(nullable OWS2FAFailure)failure +{ + OWSAssertDebug(pin.length > 0); + OWSAssertDebug(success); + OWSAssertDebug(failure); + + TSRequest *request = [OWSRequestFactory enable2FARequestWithPin:pin]; + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSAssertIsOnMainThread(); + + [self mark2FAAsEnabledWithPin:pin]; + + if (success) { + success(); + } + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSAssertIsOnMainThread(); + + if (failure) { + failure(error); + } + }]; +} + +- (void)disable2FAWithSuccess:(nullable OWS2FASuccess)success failure:(nullable OWS2FAFailure)failure +{ + TSRequest *request = [OWSRequestFactory disable2FARequest]; + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSAssertIsOnMainThread(); + + [self set2FANotEnabled]; + + if (success) { + success(); + } + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSAssertIsOnMainThread(); + + if (failure) { + failure(error); + } + }]; +} + + +#pragma mark - Reminders + +- (nullable NSDate *)lastSuccessfulReminderDate +{ + return [self.dbConnection dateForKey:kOWS2FAManager_LastSuccessfulReminderDateKey + inCollection:kOWS2FAManager_Collection]; +} + +- (void)setLastSuccessfulReminderDate:(nullable NSDate *)date +{ + OWSLogDebug(@"Seting setLastSuccessfulReminderDate:%@", date); + [self.dbConnection setDate:date + forKey:kOWS2FAManager_LastSuccessfulReminderDateKey + inCollection:kOWS2FAManager_Collection]; +} + +- (BOOL)isDueForReminder +{ + if (!self.is2FAEnabled) { + return NO; + } + + return self.nextReminderDate.timeIntervalSinceNow < 0; +} + +- (NSDate *)nextReminderDate +{ + NSDate *lastSuccessfulReminderDate = self.lastSuccessfulReminderDate ?: [NSDate distantPast]; + + return [lastSuccessfulReminderDate dateByAddingTimeInterval:self.repetitionInterval]; +} + +- (NSArray *)allRepetitionIntervals +{ + // Keep sorted monotonically increasing. + return @[ + @(6 * kHourSecs), + @(12 * kHourSecs), + @(1 * kDaySecs), + @(3 * kDaySecs), + @(7 * kDaySecs), + ]; +} + +- (double)defaultRepetitionInterval +{ + return self.allRepetitionIntervals.firstObject.doubleValue; +} + +- (NSTimeInterval)repetitionInterval +{ + return [self.dbConnection doubleForKey:kOWS2FAManager_RepetitionInterval + inCollection:kOWS2FAManager_Collection + defaultValue:self.defaultRepetitionInterval]; +} + +- (void)updateRepetitionIntervalWithWasSuccessful:(BOOL)wasSuccessful +{ + if (wasSuccessful) { + self.lastSuccessfulReminderDate = [NSDate new]; + } + + NSTimeInterval oldInterval = self.repetitionInterval; + NSTimeInterval newInterval = [self adjustRepetitionInterval:oldInterval wasSuccessful:wasSuccessful]; + + OWSLogInfo(@"%@ guess. Updating repetition interval: %f -> %f", + (wasSuccessful ? @"successful" : @"failed"), + oldInterval, + newInterval); + [self.dbConnection setDouble:newInterval + forKey:kOWS2FAManager_RepetitionInterval + inCollection:kOWS2FAManager_Collection]; +} + +- (NSTimeInterval)adjustRepetitionInterval:(NSTimeInterval)oldInterval wasSuccessful:(BOOL)wasSuccessful +{ + NSArray *allIntervals = self.allRepetitionIntervals; + + NSUInteger oldIndex = + [allIntervals indexOfObjectPassingTest:^BOOL(NSNumber *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + return oldInterval <= (NSTimeInterval)obj.doubleValue; + }]; + + NSUInteger newIndex; + if (wasSuccessful) { + newIndex = oldIndex + 1; + } else { + // prevent overflow + newIndex = oldIndex <= 0 ? 0 : oldIndex - 1; + } + + // clamp to be valid + newIndex = MAX(0, MIN(allIntervals.count - 1, newIndex)); + + NSTimeInterval newInterval = allIntervals[newIndex].doubleValue; + return newInterval; +} + +- (void)setDefaultRepetitionInterval +{ + [self.dbConnection setDouble:self.defaultRepetitionInterval + forKey:kOWS2FAManager_RepetitionInterval + inCollection:kOWS2FAManager_Collection]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAddToContactsOfferMessage.h b/SignalUtilitiesKit/OWSAddToContactsOfferMessage.h new file mode 100644 index 000000000..15a2af9d9 --- /dev/null +++ b/SignalUtilitiesKit/OWSAddToContactsOfferMessage.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInfoMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +// This is a deprecated class, we're keeping it around to avoid YapDB serialization errors +// TODO - remove this class, clean up existing instances, ensure any missed ones don't explode (UnknownDBObject) +__attribute__((deprecated)) @interface OWSAddToContactsOfferMessage : TSInfoMessage + ++ (instancetype)addToContactsOfferMessageWithTimestamp:(uint64_t)timestamp + thread:(TSThread *)thread + contactId:(NSString *)contactId; + +@property (nonatomic, readonly) NSString *contactId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAddToContactsOfferMessage.m b/SignalUtilitiesKit/OWSAddToContactsOfferMessage.m new file mode 100644 index 000000000..af1b648c8 --- /dev/null +++ b/SignalUtilitiesKit/OWSAddToContactsOfferMessage.m @@ -0,0 +1,56 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSAddToContactsOfferMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSAddToContactsOfferMessage () + +@property (nonatomic) NSString *contactId; + +@end + +#pragma mark - + +// This is a deprecated class, we're keeping it around to avoid YapDB serialization errors +// TODO - remove this class, clean up existing instances, ensure any missed ones don't explode (UnknownDBObject) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +@implementation OWSAddToContactsOfferMessage +#pragma clang diagnostic pop + ++ (instancetype)addToContactsOfferMessageWithTimestamp:(uint64_t)timestamp + thread:(TSThread *)thread + contactId:(NSString *)contactId +{ + return [[OWSAddToContactsOfferMessage alloc] initWithTimestamp:timestamp thread:thread contactId:contactId]; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp thread:(TSThread *)thread contactId:(NSString *)contactId +{ + self = [super initWithTimestamp:timestamp inThread:thread messageType:TSInfoMessageAddToContactsOffer]; + + if (self) { + _contactId = contactId; + } + + return self; +} + +- (BOOL)shouldUseReceiptDateForSorting +{ + // Use the timestamp, not the "received at" timestamp to sort, + // since we're creating these interactions after the fact and back-dating them. + return NO; +} + +- (BOOL)isDynamicInteraction +{ + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAddToProfileWhitelistOfferMessage.h b/SignalUtilitiesKit/OWSAddToProfileWhitelistOfferMessage.h new file mode 100644 index 000000000..368c93085 --- /dev/null +++ b/SignalUtilitiesKit/OWSAddToProfileWhitelistOfferMessage.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInfoMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +// This is a deprecated class, we're keeping it around to avoid YapDB serialization errors +// TODO - remove this class, clean up existing instances, ensure any missed ones don't explode (UnknownDBObject) +__attribute__((deprecated)) @interface OWSAddToProfileWhitelistOfferMessage : TSInfoMessage + ++ (instancetype)addToProfileWhitelistOfferMessageWithTimestamp:(uint64_t)timestamp thread:(TSThread *)thread; + +@property (nonatomic, readonly) NSString *contactId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAddToProfileWhitelistOfferMessage.m b/SignalUtilitiesKit/OWSAddToProfileWhitelistOfferMessage.m new file mode 100644 index 000000000..0ca611409 --- /dev/null +++ b/SignalUtilitiesKit/OWSAddToProfileWhitelistOfferMessage.m @@ -0,0 +1,40 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSAddToProfileWhitelistOfferMessage.h" +#import "TSThread.h" + +NS_ASSUME_NONNULL_BEGIN + +// This is a deprecated class, we're keeping it around to avoid YapDB serialization errors +// TODO - remove this class, clean up existing instances, ensure any missed ones don't explode (UnknownDBObject) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +@implementation OWSAddToProfileWhitelistOfferMessage +#pragma clang diagnostic pop + ++ (instancetype)addToProfileWhitelistOfferMessageWithTimestamp:(uint64_t)timestamp thread:(TSThread *)thread +{ + return [[OWSAddToProfileWhitelistOfferMessage alloc] + initWithTimestamp:timestamp + inThread:thread + messageType:(thread.isGroupThread ? TSInfoMessageAddGroupToProfileWhitelistOffer + : TSInfoMessageAddUserToProfileWhitelistOffer)]; +} + +- (BOOL)shouldUseReceiptDateForSorting +{ + // Use the timestamp, not the "received at" timestamp to sort, + // since we're creating these interactions after the fact and back-dating them. + return NO; +} + +- (BOOL)isDynamicInteraction +{ + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAnalytics.h b/SignalUtilitiesKit/OWSAnalytics.h new file mode 100755 index 000000000..270423f30 --- /dev/null +++ b/SignalUtilitiesKit/OWSAnalytics.h @@ -0,0 +1,165 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSAnalyticsEvents.h" + +NS_ASSUME_NONNULL_BEGIN + +// TODO: We probably don't need all of these levels. +typedef NS_ENUM(NSUInteger, OWSAnalyticsSeverity) { + // Info events are routine. + // + // It's safe to discard a large fraction of these events. + OWSAnalyticsSeverityInfo = 1, + // Error events should never be discarded. + OWSAnalyticsSeverityError = 3, + // Critical events are special. They are submitted immediately + // and not persisted, since the database may not be working. + OWSAnalyticsSeverityCritical = 4 +}; + +// This is a placeholder. We don't yet serialize or transmit analytics events. +// +// If/when we take this on, we'll want to develop a solution that can be used +// report user activity - especially serious bugs - without compromising user +// privacy in any way. We must _never_ include any identifying information. +@interface OWSAnalytics : NSObject + +// description: A non-empty string without any leading whitespace. +// This should conform to our analytics event naming conventions. +// "category_event_name", e.g. "database_error_no_database_file_found". +// parameters: Optional. +// If non-nil, the keys should all be non-empty NSStrings. +// Values should be NSStrings or NSNumbers. ++ (void)logEvent:(NSString *)eventName + severity:(OWSAnalyticsSeverity)severity + parameters:(nullable NSDictionary *)parameters + location:(const char *)location + line:(int)line; + ++ (void)appLaunchDidBegin; + ++ (long)orderOfMagnitudeOf:(long)value; + +@end + +typedef NSDictionary *_Nonnull (^OWSProdAssertParametersBlock)(void); + +// These methods should be used to assert errors for which we want to fire analytics events. +// +// In production, returns __Value, the assert value, so that we can handle this case. +// In debug builds, asserts. +// +// parametersBlock is of type OWSProdAssertParametersBlock. +// The "C" variants (e.g. OWSProdAssert() vs. OWSProdCAssert() should be used in free functions, +// where there is no self. They can also be used in blocks to avoid capturing a reference to self. +#define OWSProdAssertWParamsTemplate(__value, __eventName, __parametersBlock, __assertMacro) \ + { \ + if (!(BOOL)(__value)) { \ + NSDictionary *__eventParameters = (__parametersBlock ? __parametersBlock() : nil); \ + [DDLog flushLog]; \ + [OWSAnalytics logEvent:__eventName \ + severity:OWSAnalyticsSeverityError \ + parameters:__eventParameters \ + location:__PRETTY_FUNCTION__ \ + line:__LINE__]; \ + } \ + __assertMacro(__value); \ + return (BOOL)(__value); \ + } + +#define OWSProdAssertWParams(__value, __eventName, __parametersBlock) \ + OWSProdAssertWParamsTemplate(__value, __eventName, __parametersBlock, OWSAssert) + +#define OWSProdCAssertWParams(__value, __eventName, __parametersBlock) \ + OWSProdAssertWParamsTemplate(__value, __eventName, __parametersBlock, OWSCAssert) + +#define OWSProdAssert(__value, __eventName) OWSProdAssertWParams(__value, __eventName, nil) + +#define OWSProdCAssert(__value, __eventName) OWSProdCAssertWParams(__value, __eventName, nil) + +#define OWSProdFailWParamsTemplate(__eventName, __parametersBlock, __failMacro) \ + { \ + NSDictionary *__eventParameters \ + = (__parametersBlock ? ((OWSProdAssertParametersBlock)__parametersBlock)() : nil); \ + [OWSAnalytics logEvent:__eventName \ + severity:OWSAnalyticsSeverityCritical \ + parameters:__eventParameters \ + location:__PRETTY_FUNCTION__ \ + line:__LINE__]; \ + __failMacro(__eventName); \ + } + +#define OWSProdFailWParams(__eventName, __parametersBlock) \ + OWSProdFailWParamsTemplate(__eventName, __parametersBlock, OWSFailNoFormat) +#define OWSProdCFailWParams(__eventName, __parametersBlock) \ + OWSProdFailWParamsTemplate(__eventName, __parametersBlock, OWSCFailNoFormat) + +#define OWSProdFail(__eventName) OWSProdFailWParams(__eventName, nil) + +#define OWSProdCFail(__eventName) OWSProdCFailWParams(__eventName, nil) + +#define OWSProdCFail(__eventName) OWSProdCFailWParams(__eventName, nil) + +#define OWSProdEventWParams(__severityLevel, __eventName, __parametersBlock) \ + { \ + NSDictionary *__eventParameters \ + = (__parametersBlock ? ((OWSProdAssertParametersBlock)__parametersBlock)() : nil); \ + [OWSAnalytics logEvent:__eventName \ + severity:__severityLevel \ + parameters:__eventParameters \ + location:__PRETTY_FUNCTION__ \ + line:__LINE__]; \ + } + +#pragma mark - Info Events + +#define OWSProdInfoWParams(__eventName, __parametersBlock) \ + OWSProdEventWParams(OWSAnalyticsSeverityInfo, __eventName, __parametersBlock) + +#define OWSProdInfo(__eventName) OWSProdEventWParams(OWSAnalyticsSeverityInfo, __eventName, nil) + +#pragma mark - Error Events + +#define OWSProdErrorWParams(__eventName, __parametersBlock) \ + OWSProdEventWParams(OWSAnalyticsSeverityError, __eventName, __parametersBlock) + +#define OWSProdError(__eventName) OWSProdEventWParams(OWSAnalyticsSeverityError, __eventName, nil) + +#pragma mark - Critical Events + +#define OWSProdCriticalWParams(__eventName, __parametersBlock) \ + OWSProdEventWParams(OWSAnalyticsSeverityCritical, __eventName, __parametersBlock) + +#define OWSProdCritical(__eventName) OWSProdEventWParams(OWSAnalyticsSeverityCritical, __eventName, nil) + +#pragma mark - OWSMessageManager macros +// Defined here rather than in OWSMessageManager so that our analytic event extraction script +// can properly detect the event names. +// +// The debug logs can be more verbose than the analytics events. +// +// In this case `descriptionForEnvelope` is valuable enough to +// log but too dangerous to include in the analytics event. +#define OWSProdErrorWEnvelope(__analyticsEventName, __envelope) \ + { \ + OWSLogError(@"%s:%d %@: %@", \ + __PRETTY_FUNCTION__, \ + __LINE__, \ + __analyticsEventName, \ + [self descriptionForEnvelope:__envelope]); \ + OWSProdError(__analyticsEventName) \ + } + +#define OWSProdInfoWEnvelope(__analyticsEventName, __envelope) \ + { \ + OWSLogInfo(@"%s:%d %@: %@", \ + __PRETTY_FUNCTION__, \ + __LINE__, \ + __analyticsEventName, \ + [self descriptionForEnvelope:__envelope]); \ + OWSProdInfo(__analyticsEventName) \ + } + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAnalytics.m b/SignalUtilitiesKit/OWSAnalytics.m new file mode 100755 index 000000000..f1edcc830 --- /dev/null +++ b/SignalUtilitiesKit/OWSAnalytics.m @@ -0,0 +1,426 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSAnalytics.h" +#import "AppContext.h" +#import "OWSBackgroundTask.h" +#import "OWSPrimaryStorage.h" +#import "OWSQueues.h" +#import "SSKEnvironment.h" +#import "YapDatabaseConnection+OWS.h" +#import +#import +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +#ifdef DEBUG + +#define NO_SIGNAL_ANALYTICS + +#endif + +NSString *const kOWSAnalytics_EventsCollection = @"kOWSAnalytics_EventsCollection"; + +// Percentage of analytics events to discard. 0 <= x <= 100. +const int kOWSAnalytics_DiscardFrequency = 0; + +NSString *NSStringForOWSAnalyticsSeverity(OWSAnalyticsSeverity severity) +{ + switch (severity) { + case OWSAnalyticsSeverityInfo: + return @"Info"; + case OWSAnalyticsSeverityError: + return @"Error"; + case OWSAnalyticsSeverityCritical: + return @"Critical"; + } +} + +@interface OWSAnalytics () + +@property (nonatomic, readonly) Reachability *reachability; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@property (atomic) BOOL hasRequestInFlight; + +@end + +#pragma mark - + +@implementation OWSAnalytics + ++ (instancetype)sharedInstance +{ + static OWSAnalytics *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] initDefault]; + }); + return instance; +} + +// We lazy-create the analytics DB connection, so that we can handle +// errors that occur while initializing OWSPrimaryStorage. ++ (YapDatabaseConnection *)dbConnection +{ + return SSKEnvironment.shared.analyticsDBConnection; +} + +- (instancetype)initDefault +{ + self = [super init]; + + if (!self) { + return self; + } + + _reachability = [Reachability reachabilityForInternetConnection]; + + [self observeNotifications]; + + OWSSingletonAssert(); + + return self; +} + +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reachabilityChanged) + name:kReachabilityChangedNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive) + name:OWSApplicationDidBecomeActiveNotification + object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)reachabilityChanged +{ + OWSAssertIsOnMainThread(); + + [self tryToSyncEvents]; +} + +- (void)applicationDidBecomeActive +{ + OWSAssertIsOnMainThread(); + + [self tryToSyncEvents]; +} + +- (void)tryToSyncEvents +{ + return; // Loki: Do nothing + dispatch_async(self.serialQueue, ^{ + // Don't try to sync if: + // + // * There's no network available. + // * There's already a sync request in flight. + if (!self.reachability.isReachable) { + OWSLogVerbose(@"Not reachable"); + return; + } + if (self.hasRequestInFlight) { + return; + } + + __block NSString *firstEventKey = nil; + __block NSDictionary *firstEventDictionary = nil; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + // Take any event. We don't need to deliver them in any particular order. + [transaction enumerateKeysInCollection:kOWSAnalytics_EventsCollection + usingBlock:^(NSString *key, BOOL *_Nonnull stop) { + firstEventKey = key; + *stop = YES; + }]; + if (!firstEventKey) { + return; + } + + firstEventDictionary = [transaction objectForKey:firstEventKey inCollection:kOWSAnalytics_EventsCollection]; + OWSAssertDebug(firstEventDictionary); + OWSAssertDebug([firstEventDictionary isKindOfClass:[NSDictionary class]]); + }]; + + if (firstEventDictionary) { + [self sendEvent:firstEventDictionary eventKey:firstEventKey isCritical:NO]; + } + }); +} + +- (void)sendEvent:(NSDictionary *)eventDictionary eventKey:(NSString *)eventKey isCritical:(BOOL)isCritical +{ + return; // Loki: Do nothing + OWSAssertDebug(eventDictionary); + OWSAssertDebug(eventKey); + AssertOnDispatchQueue(self.serialQueue); + + if (isCritical) { + [self submitEvent:eventDictionary + eventKey:eventKey + success:^{ + OWSLogDebug(@"sendEvent[critical] succeeded: %@", eventKey); + } + failure:^{ + OWSLogError(@"sendEvent[critical] failed: %@", eventKey); + }]; + } else { + self.hasRequestInFlight = YES; + __block BOOL isComplete = NO; + [self submitEvent:eventDictionary + eventKey:eventKey + success:^{ + if (isComplete) { + return; + } + isComplete = YES; + OWSLogDebug(@"sendEvent succeeded: %@", eventKey); + dispatch_async(self.serialQueue, ^{ + self.hasRequestInFlight = NO; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + // Remove from queue. + [transaction removeObjectForKey:eventKey inCollection:kOWSAnalytics_EventsCollection]; + }]; + + // Wait a second between network requests / retries. + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self tryToSyncEvents]; + }); + }); + } + failure:^{ + if (isComplete) { + return; + } + isComplete = YES; + OWSLogError(@"sendEvent failed: %@", eventKey); + dispatch_async(self.serialQueue, ^{ + self.hasRequestInFlight = NO; + + // Wait a second between network requests / retries. + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self tryToSyncEvents]; + }); + }); + }]; + } +} + +- (void)submitEvent:(NSDictionary *)eventDictionary + eventKey:(NSString *)eventKey + success:(void (^_Nonnull)(void))successBlock + failure:(void (^_Nonnull)(void))failureBlock +{ + return; // Loki: Do nothing + OWSAssertDebug(eventDictionary); + OWSAssertDebug(eventKey); + AssertOnDispatchQueue(self.serialQueue); + + OWSLogDebug(@"submitting: %@", eventKey); + + __block OWSBackgroundTask *backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__ + completionBlock:^(BackgroundTaskState backgroundTaskState) { + if (backgroundTaskState == BackgroundTaskState_Success) { + successBlock(); + } else { + failureBlock(); + } + }]; + + // Until we integrate with an analytics platform, behave as though all event delivery succeeds. + dispatch_async(self.serialQueue, ^{ + backgroundTask = nil; + }); +} + +- (dispatch_queue_t)serialQueue +{ + static dispatch_queue_t queue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("org.whispersystems.analytics.serial", DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + +- (NSString *)operatingSystemVersionString +{ + static NSString *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSOperatingSystemVersion operatingSystemVersion = [[NSProcessInfo processInfo] operatingSystemVersion]; + result = [NSString stringWithFormat:@"%lu.%lu.%lu", + (unsigned long)operatingSystemVersion.majorVersion, + (unsigned long)operatingSystemVersion.minorVersion, + (unsigned long)operatingSystemVersion.patchVersion]; + }); + return result; +} + +- (NSDictionary *)eventSuperProperties +{ + NSMutableDictionary *result = [NSMutableDictionary new]; + result[@"app_version"] = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + result[@"platform"] = @"ios"; + result[@"ios_version"] = self.operatingSystemVersionString; + return result; +} + +- (long)orderOfMagnitudeOf:(long)value +{ + return [OWSAnalytics orderOfMagnitudeOf:value]; +} + ++ (long)orderOfMagnitudeOf:(long)value +{ + if (value <= 0) { + return 0; + } + return (long)round(pow(10, floor(log10(value)))); +} + +- (void)addEvent:(NSString *)eventName severity:(OWSAnalyticsSeverity)severity properties:(NSDictionary *)properties +{ + return; // Loki: Do nothing + OWSAssertDebug(eventName.length > 0); + OWSAssertDebug(properties); + +#ifndef NO_SIGNAL_ANALYTICS + BOOL isError = severity == OWSAnalyticsSeverityError; + BOOL isCritical = severity == OWSAnalyticsSeverityCritical; + + uint32_t discardValue = arc4random_uniform(101); + if (!isError && !isCritical && discardValue < kOWSAnalytics_DiscardFrequency) { + OWSLogVerbose(@"Discarding event: %@", eventName); + return; + } + + void (^addEvent)(void) = ^{ + // Add super properties. + NSMutableDictionary *eventProperties = (properties ? [properties mutableCopy] : [NSMutableDictionary new]); + [eventProperties addEntriesFromDictionary:self.eventSuperProperties]; + + NSDictionary *eventDictionary = [eventProperties copy]; + OWSAssertDebug(eventDictionary); + NSString *eventKey = [NSUUID UUID].UUIDString; + OWSLogDebug(@"enqueuing event: %@", eventKey); + + if (isCritical) { + // Critical events should not be serialized or enqueued - they should be submitted immediately. + [self sendEvent:eventDictionary eventKey:eventKey isCritical:YES]; + } else { + // Add to queue. + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + const int kMaxQueuedEvents = 5000; + if ([transaction numberOfKeysInCollection:kOWSAnalytics_EventsCollection] > kMaxQueuedEvents) { + OWSLogError(@"Event queue overflow."); + return; + } + + [transaction setObject:eventDictionary forKey:eventKey inCollection:kOWSAnalytics_EventsCollection]; + }]; + + [self tryToSyncEvents]; + } + }; + + if ([self shouldReportAsync:severity]) { + dispatch_async(self.serialQueue, addEvent); + } else { + dispatch_sync(self.serialQueue, addEvent); + } +#endif +} + ++ (void)logEvent:(NSString *)eventName + severity:(OWSAnalyticsSeverity)severity + parameters:(nullable NSDictionary *)parameters + location:(const char *)location + line:(int)line +{ + [[self sharedInstance] logEvent:eventName severity:severity parameters:parameters location:location line:line]; +} + +- (void)logEvent:(NSString *)eventName + severity:(OWSAnalyticsSeverity)severity + parameters:(nullable NSDictionary *)parameters + location:(const char *)location + line:(int)line +{ + return; // Loki: Do nothing + DDLogFlag logFlag; + switch (severity) { + case OWSAnalyticsSeverityInfo: + logFlag = DDLogFlagInfo; + break; + case OWSAnalyticsSeverityError: + logFlag = DDLogFlagError; + break; + case OWSAnalyticsSeverityCritical: + logFlag = DDLogFlagError; + break; + default: + OWSFailDebug(@"Unknown severity."); + logFlag = DDLogFlagDebug; + break; + } + + // Log the event. + NSString *logString = [NSString stringWithFormat:@"%s:%d %@", location, line, eventName]; + if (!parameters) { + LOG_MAYBE([self shouldReportAsync:severity], LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@", logString); + } else { + LOG_MAYBE([self shouldReportAsync:severity], + LOG_LEVEL_DEF, + logFlag, + 0, + nil, + location, + @"%@ %@", + logString, + parameters); + } + if (![self shouldReportAsync:severity]) { + [DDLog flushLog]; + } + + NSMutableDictionary *eventProperties = (parameters ? [parameters mutableCopy] : [NSMutableDictionary new]); + eventProperties[@"event_location"] = [NSString stringWithFormat:@"%s:%d", location, line]; + [self addEvent:eventName severity:severity properties:eventProperties]; +} + +- (BOOL)shouldReportAsync:(OWSAnalyticsSeverity)severity +{ + return severity != OWSAnalyticsSeverityCritical; +} + +#pragma mark - Logging + ++ (void)appLaunchDidBegin +{ + [self.sharedInstance appLaunchDidBegin]; +} + +- (void)appLaunchDidBegin +{ + OWSProdInfo([OWSAnalyticsEvents appLaunch]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAnalyticsEvents.h b/SignalUtilitiesKit/OWSAnalyticsEvents.h new file mode 100755 index 000000000..3f7b33d59 --- /dev/null +++ b/SignalUtilitiesKit/OWSAnalyticsEvents.h @@ -0,0 +1,239 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSAnalyticsEvents : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +// The code between these markers is code-generated by: +// SignalServiceKit/Utilities/extract_analytics_event_names.py +// To add an event, insert your logging event as a string e.g.: +// +// OWSProdFail(@"messageSenderErrorMissingNewPreKeyBundle"); +// +// Then run SignalServiceKit/Utilities/extract_analytics_event_names.py, which +// will extract the string into a named method in this class. +#pragma mark - Code Generation Marker + ++ (NSString *)accountsErrorRegisterPushTokensFailed; + ++ (NSString *)accountsErrorUnregisterAccountRequestFailed; + ++ (NSString *)accountsErrorVerificationCodeRequestFailed; + ++ (NSString *)accountsErrorVerifyAccountRequestFailed; + ++ (NSString *)appDelegateErrorFailedToRegisterForRemoteNotifications; + ++ (NSString *)appLaunch; + ++ (NSString *)appLaunchComplete; + ++ (NSString *)callServiceCallAlreadySet; + ++ (NSString *)callServiceCallIdMismatch; + ++ (NSString *)callServiceCallMismatch; + ++ (NSString *)callServiceCallMissing; + ++ (NSString *)callServiceCallUnexpectedlyIdle; + ++ (NSString *)callServiceCallViewCouldNotPresent; + ++ (NSString *)callServiceCouldNotCreatePeerConnectionClientPromise; + ++ (NSString *)callServiceCouldNotCreateReadyToSendIceUpdatesPromise; + ++ (NSString *)callServiceErrorHandleLocalAddedIceCandidate; + ++ (NSString *)callServiceErrorHandleLocalHungupCall; + ++ (NSString *)callServiceErrorHandleReceivedErrorExternal; + ++ (NSString *)callServiceErrorHandleReceivedErrorInternal; + ++ (NSString *)callServiceErrorHandleRemoteAddedIceCandidate; + ++ (NSString *)callServiceErrorIncomingConnectionFailedExternal; + ++ (NSString *)callServiceErrorIncomingConnectionFailedInternal; + ++ (NSString *)callServiceErrorOutgoingConnectionFailedExternal; + ++ (NSString *)callServiceErrorOutgoingConnectionFailedInternal; + ++ (NSString *)callServiceErrorTimeoutWhileConnectingIncoming; + ++ (NSString *)callServiceErrorTimeoutWhileConnectingOutgoing; + ++ (NSString *)callServiceMissingFulfillReadyToSendIceUpdatesPromise; + ++ (NSString *)callServicePeerConnectionAlreadySet; + ++ (NSString *)callServicePeerConnectionMissing; + ++ (NSString *)callServiceCallDataMissing; + ++ (NSString *)contactsErrorContactsIntersectionFailed; + ++ (NSString *)errorAttachmentRequestFailed; + ++ (NSString *)errorCouldNotPresentViewDueToCall; + ++ (NSString *)errorEnableVideoCallingRequestFailed; + ++ (NSString *)errorGetDevicesFailed; + ++ (NSString *)errorPrekeysAvailablePrekeysRequestFailed; + ++ (NSString *)errorPrekeysCurrentSignedPrekeyRequestFailed; + ++ (NSString *)errorPrekeysUpdateFailedJustSigned; + ++ (NSString *)errorPrekeysUpdateFailedSignedAndOnetime; + ++ (NSString *)errorProvisioningCodeRequestFailed; + ++ (NSString *)errorProvisioningRequestFailed; + ++ (NSString *)errorUnlinkDeviceFailed; + ++ (NSString *)errorUpdateAttributesRequestFailed; + ++ (NSString *)messageSenderErrorMissingNewPreKeyBundle; + ++ (NSString *)messageManagerErrorCallMessageNoActionablePayload; + ++ (NSString *)messageManagerErrorCorruptMessage; + ++ (NSString *)messageManagerErrorCouldNotHandlePrekeyBundle; + ++ (NSString *)messageManagerErrorCouldNotHandleUnidentifiedSenderMessage; + ++ (NSString *)messageManagerErrorCouldNotHandleSecureMessage; + ++ (NSString *)messageManagerErrorEnvelopeNoActionablePayload; + ++ (NSString *)messageManagerErrorEnvelopeTypeKeyExchange; + ++ (NSString *)messageManagerErrorEnvelopeTypeOther; + ++ (NSString *)messageManagerErrorEnvelopeTypeUnknown; + ++ (NSString *)messageManagerErrorInvalidKey; + ++ (NSString *)messageManagerErrorInvalidKeyId; + ++ (NSString *)messageManagerErrorInvalidMessageVersion; + ++ (NSString *)messageManagerErrorInvalidProtocolMessage; + ++ (NSString *)messageManagerErrorMessageEnvelopeHasNoContent; + ++ (NSString *)messageManagerErrorNoSession; + ++ (NSString *)messageManagerErrorOversizeMessage; + ++ (NSString *)messageManagerErrorSyncMessageFromUnknownSource; + ++ (NSString *)messageManagerErrorUntrustedIdentityKeyException; + ++ (NSString *)messageReceiverErrorLargeMessage; + ++ (NSString *)messageReceiverErrorOversizeMessage; + ++ (NSString *)messageSendErrorCouldNotSerializeMessageJson; + ++ (NSString *)messageSendErrorFailedDueToPrekeyUpdateFailures; + ++ (NSString *)messageSendErrorFailedDueToUntrustedKey; + ++ (NSString *)messageSenderErrorCouldNotFindContacts1; + ++ (NSString *)messageSenderErrorCouldNotFindContacts2; + ++ (NSString *)messageSenderErrorCouldNotFindContacts3; + ++ (NSString *)messageSenderErrorCouldNotLoadAttachment; + ++ (NSString *)messageSenderErrorCouldNotParseMismatchedDevicesJson; + ++ (NSString *)messageSenderErrorCouldNotWriteAttachment; + ++ (NSString *)messageSenderErrorGenericSendFailure; + ++ (NSString *)messageSenderErrorInvalidIdentityKeyLength; + ++ (NSString *)messageSenderErrorInvalidIdentityKeyType; + ++ (NSString *)messageSenderErrorNoMissingOrExtraDevices; + ++ (NSString *)messageSenderErrorRecipientPrekeyRequestFailed; + ++ (NSString *)messageSenderErrorSendOperationDidNotComplete; + ++ (NSString *)messageSenderErrorUnexpectedKeyBundle; + ++ (NSString *)peerConnectionClientErrorSendDataChannelMessageFailed; + ++ (NSString *)prekeysDeletedOldAcceptedSignedPrekey; + ++ (NSString *)prekeysDeletedOldSignedPrekey; + ++ (NSString *)prekeysDeletedOldUnacceptedSignedPrekey; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidAcl; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidAlgorithm; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidCredential; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidDate; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidKey; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidPolicy; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidResponse; + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidSignature; + ++ (NSString *)registrationBegan; + ++ (NSString *)registrationRegisteredPhoneNumber; + ++ (NSString *)registrationRegisteringCode; + ++ (NSString *)registrationRegisteringRequestedNewCodeBySms; + ++ (NSString *)registrationRegisteringRequestedNewCodeByVoice; + ++ (NSString *)registrationRegisteringSubmittedCode; + ++ (NSString *)registrationRegistrationFailed; + ++ (NSString *)registrationVerificationBack; + ++ (NSString *)storageErrorCouldNotDecodeClass; + ++ (NSString *)storageErrorCouldNotLoadDatabase; + ++ (NSString *)storageErrorCouldNotLoadDatabaseSecondAttempt; + ++ (NSString *)storageErrorCouldNotStoreKeychainValue; + ++ (NSString *)storageErrorDeserialization; + ++ (NSString *)storageErrorFileProtection; + +#pragma mark - Code Generation Marker + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAnalyticsEvents.m b/SignalUtilitiesKit/OWSAnalyticsEvents.m new file mode 100755 index 000000000..1a4b0cb0c --- /dev/null +++ b/SignalUtilitiesKit/OWSAnalyticsEvents.m @@ -0,0 +1,549 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSAnalyticsEvents.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSAnalyticsEvents + +// The code between these markers is code-generated by: +// SignalServiceKit/Utilities/extract_analytics_event_names.py +#pragma mark - Code Generation Marker + ++ (NSString *)accountsErrorRegisterPushTokensFailed +{ + return @"accounts_error_register_push_tokens_failed"; +} + ++ (NSString *)accountsErrorUnregisterAccountRequestFailed +{ + return @"accounts_error_unregister_account_request_failed"; +} + ++ (NSString *)accountsErrorVerificationCodeRequestFailed +{ + return @"accounts_error_verification_code_request_failed"; +} + ++ (NSString *)accountsErrorVerifyAccountRequestFailed +{ + return @"accounts_error_verify_account_request_failed"; +} + ++ (NSString *)appDelegateErrorFailedToRegisterForRemoteNotifications +{ + return @"app_delegate_error_failed_to_register_for_remote_notifications"; +} + ++ (NSString *)appLaunch +{ + return @"app_launch"; +} + ++ (NSString *)appLaunchComplete +{ + return @"app_launch_complete"; +} + ++ (NSString *)callServiceCallAlreadySet +{ + return @"call_service_call_already_set"; +} + ++ (NSString *)callServiceCallIdMismatch +{ + return @"call_service_call_id_mismatch"; +} + ++ (NSString *)callServiceCallMismatch +{ + return @"call_service_call_mismatch"; +} + ++ (NSString *)callServiceCallMissing +{ + return @"call_service_call_missing"; +} + ++ (NSString *)callServiceCallUnexpectedlyIdle +{ + return @"call_service_call_unexpectedly_idle"; +} + ++ (NSString *)callServiceCallViewCouldNotPresent +{ + return @"call_service_call_view_could_not_present"; +} + ++ (NSString *)callServiceCouldNotCreatePeerConnectionClientPromise +{ + return @"call_service_could_not_create_peer_connection_client_promise"; +} + ++ (NSString *)callServiceCouldNotCreateReadyToSendIceUpdatesPromise +{ + return @"call_service_could_not_create_ready_to_send_ice_updates_promise"; +} + ++ (NSString *)callServiceErrorHandleLocalAddedIceCandidate +{ + return @"call_service_error_handle_local_added_ice_candidate"; +} + ++ (NSString *)callServiceErrorHandleLocalHungupCall +{ + return @"call_service_error_handle_local_hungup_call"; +} + ++ (NSString *)callServiceErrorHandleReceivedErrorExternal +{ + return @"call_service_error_handle_received_error_external"; +} + ++ (NSString *)callServiceErrorHandleReceivedErrorInternal +{ + return @"call_service_error_handle_received_error_internal"; +} + ++ (NSString *)callServiceErrorHandleRemoteAddedIceCandidate +{ + return @"call_service_error_handle_remote_added_ice_candidate"; +} + ++ (NSString *)callServiceErrorIncomingConnectionFailedExternal +{ + return @"call_service_error_incoming_connection_failed_external"; +} + ++ (NSString *)callServiceErrorIncomingConnectionFailedInternal +{ + return @"call_service_error_incoming_connection_failed_internal"; +} + ++ (NSString *)callServiceErrorOutgoingConnectionFailedExternal +{ + return @"call_service_error_outgoing_connection_failed_external"; +} + ++ (NSString *)callServiceErrorOutgoingConnectionFailedInternal +{ + return @"call_service_error_outgoing_connection_failed_internal"; +} + ++ (NSString *)callServiceErrorTimeoutWhileConnectingIncoming +{ + return @"call_service_error_timeout_while_connecting_incoming"; +} + ++ (NSString *)callServiceErrorTimeoutWhileConnectingOutgoing +{ + return @"call_service_error_timeout_while_connecting_outgoing"; +} + ++ (NSString *)callServiceMissingFulfillReadyToSendIceUpdatesPromise +{ + return @"call_service_missing_fulfill_ready_to_send_ice_updates_promise"; +} + ++ (NSString *)callServicePeerConnectionAlreadySet +{ + return @"call_service_peer_connection_already_set"; +} + ++ (NSString *)callServicePeerConnectionMissing +{ + return @"call_service_peer_connection_missing"; +} + ++ (NSString *)callServiceCallDataMissing +{ + return @"call_service_call_data_missing"; +} + ++ (NSString *)contactsErrorContactsIntersectionFailed +{ + return @"contacts_error_contacts_intersection_failed"; +} + ++ (NSString *)errorAttachmentRequestFailed +{ + return @"error_attachment_request_failed"; +} + ++ (NSString *)errorCouldNotPresentViewDueToCall +{ + return @"error_could_not_present_view_due_to_call"; +} + ++ (NSString *)errorEnableVideoCallingRequestFailed +{ + return @"error_enable_video_calling_request_failed"; +} + ++ (NSString *)errorGetDevicesFailed +{ + return @"error_get_devices_failed"; +} + ++ (NSString *)errorPrekeysAvailablePrekeysRequestFailed +{ + return @"error_prekeys_available_prekeys_request_failed"; +} + ++ (NSString *)errorPrekeysCurrentSignedPrekeyRequestFailed +{ + return @"error_prekeys_current_signed_prekey_request_failed"; +} + ++ (NSString *)errorPrekeysUpdateFailedJustSigned +{ + return @"error_prekeys_update_failed_just_signed"; +} + ++ (NSString *)errorPrekeysUpdateFailedSignedAndOnetime +{ + return @"error_prekeys_update_failed_signed_and_onetime"; +} + ++ (NSString *)errorProvisioningCodeRequestFailed +{ + return @"error_provisioning_code_request_failed"; +} + ++ (NSString *)errorProvisioningRequestFailed +{ + return @"error_provisioning_request_failed"; +} + ++ (NSString *)errorUnlinkDeviceFailed +{ + return @"error_unlink_device_failed"; +} + ++ (NSString *)errorUpdateAttributesRequestFailed +{ + return @"error_update_attributes_request_failed"; +} + ++ (NSString *)messageSenderErrorMissingNewPreKeyBundle +{ + return @"messageSenderErrorMissingNewPreKeyBundle"; +} + ++ (NSString *)messageManagerErrorCallMessageNoActionablePayload +{ + return @"message_manager_error_call_message_no_actionable_payload"; +} + ++ (NSString *)messageManagerErrorCorruptMessage +{ + return @"message_manager_error_corrupt_message"; +} + ++ (NSString *)messageManagerErrorCouldNotHandlePrekeyBundle +{ + return @"message_manager_error_could_not_handle_prekey_bundle"; +} + ++ (NSString *)messageManagerErrorCouldNotHandleUnidentifiedSenderMessage +{ + return @"message_manager_error_could_not_handle_unidentified_sender_message"; +} + ++ (NSString *)messageManagerErrorCouldNotHandleSecureMessage +{ + return @"message_manager_error_could_not_handle_secure_message"; +} + ++ (NSString *)messageManagerErrorEnvelopeNoActionablePayload +{ + return @"message_manager_error_envelope_no_actionable_payload"; +} + ++ (NSString *)messageManagerErrorEnvelopeTypeKeyExchange +{ + return @"message_manager_error_envelope_type_key_exchange"; +} + ++ (NSString *)messageManagerErrorEnvelopeTypeOther +{ + return @"message_manager_error_envelope_type_other"; +} + ++ (NSString *)messageManagerErrorEnvelopeTypeUnknown +{ + return @"message_manager_error_envelope_type_unknown"; +} + ++ (NSString *)messageManagerErrorInvalidKey +{ + return @"message_manager_error_invalid_key"; +} + ++ (NSString *)messageManagerErrorInvalidKeyId +{ + return @"message_manager_error_invalid_key_id"; +} + ++ (NSString *)messageManagerErrorInvalidMessageVersion +{ + return @"message_manager_error_invalid_message_version"; +} + ++ (NSString *)messageManagerErrorInvalidProtocolMessage +{ + return @"message_manager_error_invalid_protocol_message"; +} + ++ (NSString *)messageManagerErrorMessageEnvelopeHasNoContent +{ + return @"message_manager_error_message_envelope_has_no_content"; +} + ++ (NSString *)messageManagerErrorNoSession +{ + return @"message_manager_error_no_session"; +} + ++ (NSString *)messageManagerErrorOversizeMessage +{ + return @"message_manager_error_oversize_message"; +} + ++ (NSString *)messageManagerErrorSyncMessageFromUnknownSource +{ + return @"message_manager_error_sync_message_from_unknown_source"; +} + ++ (NSString *)messageManagerErrorUntrustedIdentityKeyException +{ + return @"message_manager_error_untrusted_identity_key_exception"; +} + ++ (NSString *)messageReceiverErrorLargeMessage +{ + return @"message_receiver_error_large_message"; +} + ++ (NSString *)messageReceiverErrorOversizeMessage +{ + return @"message_receiver_error_oversize_message"; +} + ++ (NSString *)messageSendErrorCouldNotSerializeMessageJson +{ + return @"message_send_error_could_not_serialize_message_json"; +} + ++ (NSString *)messageSendErrorFailedDueToPrekeyUpdateFailures +{ + return @"message_send_error_failed_due_to_prekey_update_failures"; +} + ++ (NSString *)messageSendErrorFailedDueToUntrustedKey +{ + return @"message_send_error_failed_due_to_untrusted_key"; +} + ++ (NSString *)messageSenderErrorCouldNotFindContacts1 +{ + return @"message_sender_error_could_not_find_contacts_1"; +} + ++ (NSString *)messageSenderErrorCouldNotFindContacts2 +{ + return @"message_sender_error_could_not_find_contacts_2"; +} + ++ (NSString *)messageSenderErrorCouldNotFindContacts3 +{ + return @"message_sender_error_could_not_find_contacts_3"; +} + ++ (NSString *)messageSenderErrorCouldNotLoadAttachment +{ + return @"message_sender_error_could_not_load_attachment"; +} + ++ (NSString *)messageSenderErrorCouldNotParseMismatchedDevicesJson +{ + return @"message_sender_error_could_not_parse_mismatched_devices_json"; +} + ++ (NSString *)messageSenderErrorCouldNotWriteAttachment +{ + return @"message_sender_error_could_not_write_attachment"; +} + ++ (NSString *)messageSenderErrorGenericSendFailure +{ + return @"message_sender_error_generic_send_failure"; +} + ++ (NSString *)messageSenderErrorInvalidIdentityKeyLength +{ + return @"message_sender_error_invalid_identity_key_length"; +} + ++ (NSString *)messageSenderErrorInvalidIdentityKeyType +{ + return @"message_sender_error_invalid_identity_key_type"; +} + ++ (NSString *)messageSenderErrorNoMissingOrExtraDevices +{ + return @"message_sender_error_no_missing_or_extra_devices"; +} + ++ (NSString *)messageSenderErrorRecipientPrekeyRequestFailed +{ + return @"message_sender_error_recipient_prekey_request_failed"; +} + ++ (NSString *)messageSenderErrorSendOperationDidNotComplete +{ + return @"message_sender_error_send_operation_did_not_complete"; +} + ++ (NSString *)messageSenderErrorUnexpectedKeyBundle +{ + return @"message_sender_error_unexpected_key_bundle"; +} + ++ (NSString *)peerConnectionClientErrorSendDataChannelMessageFailed +{ + return @"peer_connection_client_error_send_data_channel_message_failed"; +} + ++ (NSString *)prekeysDeletedOldAcceptedSignedPrekey +{ + return @"prekeys_deleted_old_accepted_signed_prekey"; +} + ++ (NSString *)prekeysDeletedOldSignedPrekey +{ + return @"prekeys_deleted_old_signed_prekey"; +} + ++ (NSString *)prekeysDeletedOldUnacceptedSignedPrekey +{ + return @"prekeys_deleted_old_unaccepted_signed_prekey"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidAcl +{ + return @"profile_manager_error_avatar_upload_form_invalid_acl"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidAlgorithm +{ + return @"profile_manager_error_avatar_upload_form_invalid_algorithm"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidCredential +{ + return @"profile_manager_error_avatar_upload_form_invalid_credential"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidDate +{ + return @"profile_manager_error_avatar_upload_form_invalid_date"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidKey +{ + return @"profile_manager_error_avatar_upload_form_invalid_key"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidPolicy +{ + return @"profile_manager_error_avatar_upload_form_invalid_policy"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidResponse +{ + return @"profile_manager_error_avatar_upload_form_invalid_response"; +} + ++ (NSString *)profileManagerErrorAvatarUploadFormInvalidSignature +{ + return @"profile_manager_error_avatar_upload_form_invalid_signature"; +} + ++ (NSString *)registrationBegan +{ + return @"registration_began"; +} + ++ (NSString *)registrationRegisteredPhoneNumber +{ + return @"registration_registered_phone_number"; +} + ++ (NSString *)registrationRegisteringCode +{ + return @"registration_registering_code"; +} + ++ (NSString *)registrationRegisteringRequestedNewCodeBySms +{ + return @"registration_registering_requested_new_code_by_sms"; +} + ++ (NSString *)registrationRegisteringRequestedNewCodeByVoice +{ + return @"registration_registering_requested_new_code_by_voice"; +} + ++ (NSString *)registrationRegisteringSubmittedCode +{ + return @"registration_registering_submitted_code"; +} + ++ (NSString *)registrationRegistrationFailed +{ + return @"registration_registration_failed"; +} + ++ (NSString *)registrationVerificationBack +{ + return @"registration_verification_back"; +} + ++ (NSString *)storageErrorCouldNotDecodeClass +{ + return @"storage_error_could_not_decode_class"; +} + ++ (NSString *)storageErrorCouldNotLoadDatabase +{ + return @"storage_error_could_not_load_database"; +} + ++ (NSString *)storageErrorCouldNotLoadDatabaseSecondAttempt +{ + return @"storage_error_could_not_load_database_second_attempt"; +} + ++ (NSString *)storageErrorCouldNotStoreKeychainValue +{ + return @"storage_error_could_not_store_keychain_value"; +} + ++ (NSString *)storageErrorDeserialization +{ + return @"storage_error_deserialization"; +} + ++ (NSString *)storageErrorFileProtection +{ + return @"storage_error_file_protection"; +} + +#pragma mark - Code Generation Marker + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAttachmentDownloads.h b/SignalUtilitiesKit/OWSAttachmentDownloads.h new file mode 100644 index 000000000..4840d3ae2 --- /dev/null +++ b/SignalUtilitiesKit/OWSAttachmentDownloads.h @@ -0,0 +1,52 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kAttachmentDownloadProgressNotification; +extern NSString *const kAttachmentDownloadProgressKey; +extern NSString *const kAttachmentDownloadAttachmentIDKey; + +@class SSKProtoAttachmentPointer; +@class TSAttachment; +@class TSAttachmentPointer; +@class TSAttachmentStream; +@class TSMessage; +@class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; + +#pragma mark - + +/** + * Given incoming attachment protos, determines which we support. + * It can download those that we support and notifies threads when it receives unsupported attachments. + */ +@interface OWSAttachmentDownloads : NSObject + +- (nullable NSNumber *)downloadProgressForAttachmentId:(NSString *)attachmentId; + +// This will try to download all un-downloaded attachments for a given message. +// Any attachments for the message which are already downloaded are skipped BUT +// they are included in the success callback. +// +// success/failure are always called on a worker queue. +- (void)downloadAttachmentsForMessage:(TSMessage *)message + transaction:(YapDatabaseReadTransaction *)transaction + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure; + +// This will try to download a single attachment. +// +// success/failure are always called on a worker queue. +- (void)downloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure; + +- (void)continueDownloadIfPossible; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSAttachmentDownloads.m b/SignalUtilitiesKit/OWSAttachmentDownloads.m new file mode 100644 index 000000000..c3b5454c1 --- /dev/null +++ b/SignalUtilitiesKit/OWSAttachmentDownloads.m @@ -0,0 +1,556 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSAttachmentDownloads.h" +#import "AppContext.h" +#import "MIMETypeUtil.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSBackgroundTask.h" +#import "OWSDispatch.h" +#import "OWSError.h" +#import "OWSFileSystem.h" +#import "OWSPrimaryStorage.h" +#import "OWSRequestFactory.h" +#import "SSKEnvironment.h" +#import "TSAttachmentPointer.h" +#import "TSAttachmentStream.h" +#import "TSGroupModel.h" +#import "TSGroupThread.h" +#import "TSInfoMessage.h" +#import "TSMessage.h" +#import "TSNetworkManager.h" +#import "TSThread.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kAttachmentDownloadProgressNotification = @"kAttachmentDownloadProgressNotification"; +NSString *const kAttachmentDownloadProgressKey = @"kAttachmentDownloadProgressKey"; +NSString *const kAttachmentDownloadAttachmentIDKey = @"kAttachmentDownloadAttachmentIDKey"; + +// Use a slightly non-zero value to ensure that the progress +// indicator shows up as quickly as possible. +static const CGFloat kAttachmentDownloadProgressTheta = 0.001f; + +typedef void (^AttachmentDownloadSuccess)(TSAttachmentStream *attachmentStream); +typedef void (^AttachmentDownloadFailure)(NSError *error); + +@interface OWSAttachmentDownloadJob : NSObject + +@property (nonatomic, readonly) TSAttachmentPointer *attachmentPointer; +@property (nonatomic, readonly, nullable) TSMessage *message; +@property (nonatomic, readonly) AttachmentDownloadSuccess success; +@property (nonatomic, readonly) AttachmentDownloadFailure failure; +@property (atomic) CGFloat progress; + +@end + +#pragma mark - + +@implementation OWSAttachmentDownloadJob + +- (instancetype)initWithAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + message:(nullable TSMessage *)message + success:(AttachmentDownloadSuccess)success + failure:(AttachmentDownloadFailure)failure +{ + self = [super init]; + if (!self) { + return self; + } + + _attachmentPointer = attachmentPointer; + _message = message; + _success = success; + _failure = failure; + + return self; +} + +@end + +#pragma mark - + +@interface OWSAttachmentDownloads () + +// This property should only be accessed while synchronized on this class. +@property (nonatomic, readonly) NSMutableDictionary *downloadingJobMap; +// This property should only be accessed while synchronized on this class. +@property (nonatomic, readonly) NSMutableArray *attachmentDownloadJobQueue; + +@end + +#pragma mark - + +@implementation OWSAttachmentDownloads + +#pragma mark - Dependencies + +- (OWSPrimaryStorage *)primaryStorage +{ + return SSKEnvironment.shared.primaryStorage; +} + +- (TSNetworkManager *)networkManager +{ + return SSKEnvironment.shared.networkManager; +} + + +#pragma mark - + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + _downloadingJobMap = [NSMutableDictionary new]; + _attachmentDownloadJobQueue = [NSMutableArray new]; + + return self; +} + +#pragma mark - + +- (nullable NSNumber *)downloadProgressForAttachmentId:(NSString *)attachmentId +{ + + @synchronized(self) { + OWSAttachmentDownloadJob *_Nullable job = self.downloadingJobMap[attachmentId]; + if (!job) { + return nil; + } + return @(job.progress); + } +} + +- (void)downloadAttachmentsForMessage:(TSMessage *)message + transaction:(YapDatabaseReadTransaction *)transaction + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(transaction); + OWSAssertDebug(message); + + NSMutableArray *attachmentStreams = [NSMutableArray array]; + NSMutableArray *attachmentPointers = [NSMutableArray new]; + + for (TSAttachment *attachment in [message attachmentsWithTransaction:transaction]) { + if ([attachment isKindOfClass:[TSAttachmentStream class]]) { + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + [attachmentStreams addObject:attachmentStream]; + } else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) { + TSAttachmentPointer *attachmentPointer = (TSAttachmentPointer *)attachment; + if (attachmentPointer.pointerType != TSAttachmentPointerTypeIncoming) { + OWSLogInfo(@"Ignoring restoring attachment."); + continue; + } + [attachmentPointers addObject:attachmentPointer]; + } else { + OWSFailDebug(@"Unexpected attachment type: %@", attachment.class); + } + } + + [self enqueueJobsForAttachmentStreams:attachmentStreams + attachmentPointers:attachmentPointers + message:message + success:success + failure:failure]; +} + +- (void)downloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + success:(void (^)(NSArray *attachmentStreams))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(attachmentPointer); + + [self enqueueJobsForAttachmentStreams:@[] + attachmentPointers:@[ + attachmentPointer, + ] + message:nil + success:success + failure:failure]; +} + +- (void)enqueueJobsForAttachmentStreams:(NSArray *)attachmentStreamsParam + attachmentPointers:(NSArray *)attachmentPointers + message:(nullable TSMessage *)message + success:(void (^)(NSArray *attachmentStreams))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(attachmentStreamsParam); + + // To avoid deadlocks, synchronize on self outside of the transaction. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (attachmentPointers.count < 1) { + OWSAssertDebug(attachmentStreamsParam.count > 0); + successHandler(attachmentStreamsParam); + return; + } + + NSMutableArray *attachmentStreams = [attachmentStreamsParam mutableCopy]; + NSMutableArray *promises = [NSMutableArray array]; + for (TSAttachmentPointer *attachmentPointer in attachmentPointers) { + AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [self enqueueJobForAttachmentPointer:attachmentPointer + message:message + success:^(TSAttachmentStream *attachmentStream) { + @synchronized(attachmentStreams) { + [attachmentStreams addObject:attachmentStream]; + } + + resolve(@(1)); + } + failure:^(NSError *error) { + resolve(error); + }]; + }]; + [promises addObject:promise]; + } + + // We use PMKJoin(), not PMKWhen(), because we don't want the + // completion promise to execute until _all_ promises + // have either succeeded or failed. PMKWhen() executes as + // soon as any of its input promises fail. + AnyPromise *completionPromise + = PMKJoin(promises) + .then(^(id value) { + NSArray *attachmentStreamsCopy; + @synchronized(attachmentStreams) { + attachmentStreamsCopy = [attachmentStreams copy]; + } + OWSLogInfo(@"Attachment downloads succeeded: %lu.", (unsigned long)attachmentStreamsCopy.count); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + successHandler(attachmentStreamsCopy); + }); + }) + .catch(^(NSError *error) { + OWSLogError(@"Attachment downloads failed."); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + failureHandler(error); + }); + }); + [completionPromise retainUntilComplete]; + }); +} + +- (void)enqueueJobForAttachmentPointer:(TSAttachmentPointer *)attachmentPointer + message:(nullable TSMessage *)message + success:(void (^)(TSAttachmentStream *attachmentStream))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(attachmentPointer); + + OWSAttachmentDownloadJob *job = [[OWSAttachmentDownloadJob alloc] initWithAttachmentPointer:attachmentPointer + message:message + success:success + failure:failure]; + + @synchronized(self) { + [self.attachmentDownloadJobQueue addObject:job]; + } + + [self startDownloadIfPossible]; +} + +- (void)startDownloadIfPossible +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + OWSAttachmentDownloadJob *_Nullable job; + + @synchronized(self) { + const NSUInteger kMaxSimultaneousDownloads = 4; + if (self.downloadingJobMap.count >= kMaxSimultaneousDownloads) { + return; + } + job = self.attachmentDownloadJobQueue.firstObject; + if (!job) { + return; + } + if (self.downloadingJobMap[job.attachmentPointer.uniqueId] != nil) { + // Ensure we only have one download in flight at a time for a given attachment. + OWSLogWarn(@"Ignoring duplicate download."); + return; + } + [self.attachmentDownloadJobQueue removeObjectAtIndex:0]; + self.downloadingJobMap[job.attachmentPointer.uniqueId] = job; + } + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + job.attachmentPointer.state = TSAttachmentPointerStateDownloading; + [job.attachmentPointer saveWithTransaction:transaction]; + + if (job.message) { + [job.message touchWithTransaction:transaction]; + } + }]; + + [self retrieveAttachmentForJob:job + success:^(TSAttachmentStream *attachmentStream) { + OWSLogVerbose(@"Attachment download succeeded."); + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [attachmentStream saveWithTransaction:transaction]; + + if (job.message) { + [job.message touchWithTransaction:transaction]; + } + }]; + + job.success(attachmentStream); + + @synchronized(self) { + [self.downloadingJobMap removeObjectForKey:job.attachmentPointer.uniqueId]; + } + + [self startDownloadIfPossible]; + } + failure:^(NSError *error) { + OWSLogError(@"Attachment download failed with error: %@", error); + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + job.attachmentPointer.mostRecentFailureLocalizedText = error.localizedDescription; + job.attachmentPointer.state = TSAttachmentPointerStateFailed; + [job.attachmentPointer saveWithTransaction:transaction]; + + if (job.message) { + [job.message touchWithTransaction:transaction]; + } + }]; + + @synchronized(self) { + [self.downloadingJobMap removeObjectForKey:job.attachmentPointer.uniqueId]; + } + + job.failure(error); + + [self startDownloadIfPossible]; + }]; + }); +} + +#pragma mark - + +- (void)continueDownloadIfPossible +{ + if (self.attachmentDownloadJobQueue.count > 0) { + [LKLogger print:@"[Loki] Continuing unfinished attachment download tasks."]; + [self startDownloadIfPossible]; + } +} + +#pragma mark - + +- (void)retrieveAttachmentForJob:(OWSAttachmentDownloadJob *)job + success:(void (^)(TSAttachmentStream *attachmentStream))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(job); + TSAttachmentPointer *attachmentPointer = job.attachmentPointer; + + __block OWSBackgroundTask *_Nullable backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + + void (^markAndHandleFailure)(NSError *) = ^(NSError *error) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + failureHandler(error); + + OWSAssertDebug(backgroundTask); + backgroundTask = nil; + }); + }; + + void (^markAndHandleSuccess)(TSAttachmentStream *attachmentStream) = ^(TSAttachmentStream *attachmentStream) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + successHandler(attachmentStream); + + OWSAssertDebug(backgroundTask); + backgroundTask = nil; + }); + }; + + __block NSUInteger retryCount = 0; + NSUInteger maxRetryCount = 4; + __block void (^attempt)(); + attempt = ^() { + dispatch_async([OWSDispatch attachmentsQueue], ^{ + [self downloadFromLocation:attachmentPointer.downloadURL + job:job + success:^(NSString *encryptedDataFilePath) { + [self decryptAttachmentPath:encryptedDataFilePath + attachmentPointer:attachmentPointer + success:markAndHandleSuccess + failure:markAndHandleFailure]; + } + failure:^(NSURLSessionTask *task, NSError *error) { + if (retryCount == maxRetryCount) { + markAndHandleFailure(error); + } else { + retryCount += 1; + attempt(); + } + }]; + }); + }; + attempt(); +} + +- (void)decryptAttachmentPath:(NSString *)encryptedDataFilePath + attachmentPointer:(TSAttachmentPointer *)attachmentPointer + success:(void (^)(TSAttachmentStream *attachmentStream))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(encryptedDataFilePath.length > 0); + OWSAssertDebug(attachmentPointer); + + // Use attachmentDecryptSerialQueue to ensure that we only load into memory + // & decrypt a single attachment at a time. + dispatch_async(self.attachmentDecryptSerialQueue, ^{ + @autoreleasepool { + NSData *_Nullable encryptedData = [NSData dataWithContentsOfFile:encryptedDataFilePath]; + if (!encryptedData) { + OWSLogError(@"Could not load encrypted data."); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + return failure(error); + } + + [self decryptAttachmentData:encryptedData + attachmentPointer:attachmentPointer + success:success + failure:failure]; + + if (![OWSFileSystem deleteFile:encryptedDataFilePath]) { + OWSLogError(@"Could not delete temporary file."); + } + } + }); +} + +- (void)decryptAttachmentData:(NSData *)cipherText + attachmentPointer:(TSAttachmentPointer *)attachmentPointer + success:(void (^)(TSAttachmentStream *attachmentStream))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(attachmentPointer); + + NSError *decryptError; + NSData *_Nullable plaintext; + if (attachmentPointer.encryptionKey != nil) { + plaintext = [Cryptography decryptAttachment:cipherText + withKey:attachmentPointer.encryptionKey + digest:attachmentPointer.digest + unpaddedSize:attachmentPointer.byteCount + error:&decryptError]; + } else { + plaintext = cipherText; // Loki: Public chat attachments are unencrypted + } + + if (decryptError) { + OWSLogError(@"failed to decrypt with error: %@", decryptError); + failureHandler(decryptError); + return; + } + + if (!plaintext) { + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeFailedToDecryptMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + failureHandler(error); + return; + } + + TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithPointer:attachmentPointer]; + + NSError *writeError; + [stream writeData:plaintext error:&writeError]; + if (writeError) { + OWSLogError(@"Failed writing attachment stream with error: %@", writeError); + failureHandler(writeError); + return; + } + + successHandler(stream); +} + +- (dispatch_queue_t)attachmentDecryptSerialQueue +{ + static dispatch_queue_t _serialQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _serialQueue = dispatch_queue_create("org.whispersystems.attachment.decrypt", DISPATCH_QUEUE_SERIAL); + }); + + return _serialQueue; +} + +- (void)downloadFromLocation:(NSString *)location + job:(OWSAttachmentDownloadJob *)job + success:(void (^)(NSString *encryptedDataPath))successHandler + failure:(void (^)(NSURLSessionTask *_Nullable task, NSError *error))failureHandlerParam +{ + OWSAssertDebug(job); + TSAttachmentPointer *attachmentPointer = job.attachmentPointer; + + // We want to avoid large downloads from a compromised or buggy service. + const long kMaxDownloadSize = 10 * 1024 * 1024; + __block BOOL hasCheckedContentLength = NO; + + NSString *tempFilePath = + [OWSTemporaryDirectoryAccessibleAfterFirstAuth() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; + NSURL *tempFileURL = [NSURL fileURLWithPath:tempFilePath]; + __block NSURLSessionDownloadTask *task; + void (^failureHandler)(NSError *) = ^(NSError *error) { + OWSLogError(@"Failed to download attachment with error: %@", error.description); + + if (![OWSFileSystem deleteFileIfExists:tempFilePath]) { + OWSLogError(@"Could not delete temporary file #1."); + } + + failureHandlerParam(task, error); + }; + + [[SNFileServerAPI downloadAttachmentFrom:location].then(^(NSData *data) { + BOOL success = [data writeToFile:tempFilePath atomically:YES]; + if (success) { + successHandler(tempFilePath); + } + + NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:tempFilePath]; + if (!fileSize) { + OWSLogError(@"Could not determine attachment file size."); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + return failureHandler(error); + } + if (fileSize.unsignedIntegerValue > kMaxDownloadSize) { + OWSLogError(@"Attachment download length exceeds max size."); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + return failureHandler(error); + } + }) retainUntilComplete]; +} + +- (void)fireProgressNotification:(CGFloat)progress attachmentId:(NSString *)attachmentId +{ + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter postNotificationNameAsync:kAttachmentDownloadProgressNotification + object:nil + userInfo:@{ + kAttachmentDownloadProgressKey : @(progress), + kAttachmentDownloadAttachmentIDKey : attachmentId + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBackgroundTask.h b/SignalUtilitiesKit/OWSBackgroundTask.h new file mode 100644 index 000000000..70a9fbdd0 --- /dev/null +++ b/SignalUtilitiesKit/OWSBackgroundTask.h @@ -0,0 +1,62 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, BackgroundTaskState) { + BackgroundTaskState_Success, + BackgroundTaskState_CouldNotStart, + BackgroundTaskState_Expired, +}; + +typedef void (^BackgroundTaskCompletionBlock)(BackgroundTaskState backgroundTaskState); + +// This class can be safely accessed and used from any thread. +@interface OWSBackgroundTaskManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)sharedManager; + +- (void)observeNotifications; + +@end + +#pragma mark - + +// This class makes it easier and safer to use background tasks. +// +// * Uses RAII (Resource Acquisition Is Initialization) pattern. +// * Ensures completion block is called exactly once and on main thread, +// to facilitate handling "background task timed out" case, for example. +// * Ensures we properly handle the "background task could not be created" +// case. +// +// Usage: +// +// * Use factory method to start a background task. +// * Retain a strong reference to the OWSBackgroundTask during the "work". +// * Clear all references to the OWSBackgroundTask when the work is done, +// if possible. +@interface OWSBackgroundTask : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (OWSBackgroundTask *)backgroundTaskWithLabelStr:(const char *)labelStr; + +// completionBlock will be called exactly once on the main thread. ++ (OWSBackgroundTask *)backgroundTaskWithLabelStr:(const char *)labelStr + completionBlock:(BackgroundTaskCompletionBlock)completionBlock; + ++ (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label; + +// completionBlock will be called exactly once on the main thread. ++ (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label + completionBlock:(BackgroundTaskCompletionBlock)completionBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBackgroundTask.m b/SignalUtilitiesKit/OWSBackgroundTask.m new file mode 100644 index 000000000..7384c063c --- /dev/null +++ b/SignalUtilitiesKit/OWSBackgroundTask.m @@ -0,0 +1,438 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackgroundTask.h" +#import "AppContext.h" +#import "NSTimer+OWS.h" +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^BackgroundTaskExpirationBlock)(void); +typedef NSNumber *OWSTaskId; + +// This class can be safely accessed and used from any thread. +@interface OWSBackgroundTaskManager () + +// This property should only be accessed while synchronized on this instance. +@property (nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; + +// This property should only be accessed while synchronized on this instance. +@property (nonatomic) NSMutableDictionary *expirationMap; + +// This property should only be accessed while synchronized on this instance. +@property (nonatomic) unsigned long long idCounter; + +// Note that this flag is set a little early in "will resign active". +// +// This property should only be accessed while synchronized on this instance. +@property (nonatomic) BOOL isAppActive; + +// We use this timer to provide continuity and reduce churn, +// so that if one OWSBackgroundTask ends right before another +// begins, we use a single uninterrupted background that +// spans their lifetimes. +// +// This property should only be accessed while synchronized on this instance. +@property (nonatomic, nullable) NSTimer *continuityTimer; + +@end + +#pragma mark - + +@implementation OWSBackgroundTaskManager + ++ (instancetype)sharedManager +{ + static OWSBackgroundTaskManager *sharedMyManager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedMyManager = [[self alloc] initDefault]; + }); + return sharedMyManager; +} + +- (instancetype)initDefault +{ + OWSAssertIsOnMainThread(); + + self = [super init]; + + if (!self) { + return self; + } + + self.backgroundTaskId = UIBackgroundTaskInvalid; + self.expirationMap = [NSMutableDictionary new]; + self.idCounter = 0; + self.isAppActive = CurrentAppContext().isMainAppAndActive; + + OWSSingletonAssert(); + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)observeNotifications +{ + if (!CurrentAppContext().isMainApp) { + return; + } + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:OWSApplicationDidBecomeActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResignActive:) + name:OWSApplicationWillResignActiveNotification + object:nil]; +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + OWSAssertIsOnMainThread(); + + @synchronized(self) + { + self.isAppActive = YES; + + [self ensureBackgroundTaskState]; + } +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + OWSAssertIsOnMainThread(); + + @synchronized(self) + { + self.isAppActive = NO; + + [self ensureBackgroundTaskState]; + } +} + +// This method registers a new task with this manager. We only bother +// requesting a background task from iOS if the app is inactive (or about +// to become inactive), so this will often not start a background task. +// +// Returns nil if adding this task _should have_ started a +// background task, but the background task couldn't be begun. +// In that case expirationBlock will not be called. +- (nullable OWSTaskId)addTaskWithExpirationBlock:(BackgroundTaskExpirationBlock)expirationBlock +{ + OWSAssertDebug(expirationBlock); + + OWSTaskId _Nullable taskId; + + @synchronized(self) + { + self.idCounter = self.idCounter + 1; + taskId = @(self.idCounter); + self.expirationMap[taskId] = expirationBlock; + + if (![self ensureBackgroundTaskState]) { + [self.expirationMap removeObjectForKey:taskId]; + return nil; + } + + [self.continuityTimer invalidate]; + self.continuityTimer = nil; + + return taskId; + } +} + +- (void)removeTask:(OWSTaskId)taskId +{ + OWSAssertDebug(taskId); + + @synchronized(self) + { + OWSAssertDebug(self.expirationMap[taskId] != nil); + + [self.expirationMap removeObjectForKey:taskId]; + + // This timer will ensure that we keep the background task active (if necessary) + // for an extra fraction of a second to provide continuity between tasks. + // This makes it easier and safer to use background tasks, since most code + // should be able to ensure background tasks by "narrowly" wrapping + // their core logic with a OWSBackgroundTask and not worrying about "hand off" + // between OWSBackgroundTasks. + [self.continuityTimer invalidate]; + self.continuityTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.25f + target:self + selector:@selector(timerDidFire) + userInfo:nil + repeats:NO]; + + [self ensureBackgroundTaskState]; + } +} + +// Begins or end a background task if necessary. +- (BOOL)ensureBackgroundTaskState +{ + if (!CurrentAppContext().isMainApp) { + // We can't create background tasks in the SAE, but pretend that we succeeded. + return YES; + } + + @synchronized(self) + { + // We only want to have a background task if we are: + // a) "not active" AND + // b1) there is one or more active instance of OWSBackgroundTask OR... + // b2) ...there _was_ an active instance recently. + BOOL shouldHaveBackgroundTask = (!self.isAppActive && (self.expirationMap.count > 0 || self.continuityTimer)); + BOOL hasBackgroundTask = self.backgroundTaskId != UIBackgroundTaskInvalid; + + if (shouldHaveBackgroundTask == hasBackgroundTask) { + // Current state is correct. + return YES; + } else if (shouldHaveBackgroundTask) { + OWSLogInfo(@"Starting background task."); + return [self startBackgroundTask]; + } else { + // Need to end background task. + OWSLogInfo(@"Ending background task."); + UIBackgroundTaskIdentifier backgroundTaskId = self.backgroundTaskId; + self.backgroundTaskId = UIBackgroundTaskInvalid; + [CurrentAppContext() endBackgroundTask:backgroundTaskId]; + return YES; + } + } +} + +// Returns NO if the background task cannot be begun. +- (BOOL)startBackgroundTask +{ + OWSAssertDebug(CurrentAppContext().isMainApp); + + @synchronized(self) + { + OWSAssertDebug(self.backgroundTaskId == UIBackgroundTaskInvalid); + + self.backgroundTaskId = [CurrentAppContext() beginBackgroundTaskWithExpirationHandler:^{ + // Supposedly [UIApplication beginBackgroundTaskWithExpirationHandler]'s handler + // will always be called on the main thread, but in practice we've observed + // otherwise. + // + // See: + // https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio) + OWSAssertDebug([NSThread isMainThread]); + + [self backgroundTaskExpired]; + }]; + + // If the background task could not begin, return NO to indicate that. + if (self.backgroundTaskId == UIBackgroundTaskInvalid) { + OWSLogError(@"background task could not be started."); + + return NO; + } + return YES; + } +} + +- (void)backgroundTaskExpired +{ + UIBackgroundTaskIdentifier backgroundTaskId; + NSDictionary *expirationMap; + + @synchronized(self) + { + backgroundTaskId = self.backgroundTaskId; + self.backgroundTaskId = UIBackgroundTaskInvalid; + + expirationMap = [self.expirationMap copy]; + [self.expirationMap removeAllObjects]; + } + + // Supposedly [UIApplication beginBackgroundTaskWithExpirationHandler]'s handler + // will always be called on the main thread, but in practice we've observed + // otherwise. OWSBackgroundTask's API guarantees that completionBlock will + // always be called on the main thread, so we use DispatchSyncMainThreadSafe() + // to ensure that. We thereby ensure that we don't end the background task + // until all of the completion blocks have completed. + DispatchSyncMainThreadSafe(^{ + for (BackgroundTaskExpirationBlock expirationBlock in expirationMap.allValues) { + expirationBlock(); + } + if (backgroundTaskId != UIBackgroundTaskInvalid) { + // Apparently we need to "end" even expired background tasks. + [CurrentAppContext() endBackgroundTask:backgroundTaskId]; + } + }); +} + +- (void)timerDidFire +{ + @synchronized(self) + { + [self.continuityTimer invalidate]; + self.continuityTimer = nil; + + [self ensureBackgroundTaskState]; + } +} + +@end + +#pragma mark - + +@interface OWSBackgroundTask () + +@property (nonatomic, readonly) NSString *label; + +// This property should only be accessed while synchronized on this instance. +@property (nonatomic, nullable) OWSTaskId taskId; + +// This property should only be accessed while synchronized on this instance. +@property (nonatomic, nullable) BackgroundTaskCompletionBlock completionBlock; + +@end + +#pragma mark - + +@implementation OWSBackgroundTask + ++ (OWSBackgroundTask *)backgroundTaskWithLabelStr:(const char *)labelStr +{ + OWSAssertDebug(labelStr); + + NSString *label = [NSString stringWithFormat:@"%s", labelStr]; + return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:nil]; +} + ++ (OWSBackgroundTask *)backgroundTaskWithLabelStr:(const char *)labelStr + completionBlock:(BackgroundTaskCompletionBlock)completionBlock +{ + + OWSAssertDebug(labelStr); + + NSString *label = [NSString stringWithFormat:@"%s", labelStr]; + return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:completionBlock]; +} + ++ (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label +{ + return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:nil]; +} + ++ (OWSBackgroundTask *)backgroundTaskWithLabel:(NSString *)label + completionBlock:(BackgroundTaskCompletionBlock)completionBlock +{ + return [[OWSBackgroundTask alloc] initWithLabel:label completionBlock:completionBlock]; +} + +- (instancetype)initWithLabel:(NSString *)label completionBlock:(BackgroundTaskCompletionBlock _Nullable)completionBlock +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSAssertDebug(label.length > 0); + + _label = label; + self.completionBlock = completionBlock; + + [self startBackgroundTask]; + + return self; +} + +- (void)dealloc +{ + [self endBackgroundTask]; +} + +- (void)startBackgroundTask +{ + __weak typeof(self) weakSelf = self; + self.taskId = [OWSBackgroundTaskManager.sharedManager addTaskWithExpirationBlock:^{ + DispatchMainThreadSafe(^{ + OWSBackgroundTask *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSLogVerbose(@"task expired"); + + // Make a local copy of completionBlock to ensure that it is called + // exactly once. + BackgroundTaskCompletionBlock _Nullable completionBlock = nil; + + @synchronized(strongSelf) + { + if (!strongSelf.taskId) { + return; + } + OWSLogInfo(@"%@ background task expired.", strongSelf.label); + strongSelf.taskId = nil; + + completionBlock = strongSelf.completionBlock; + strongSelf.completionBlock = nil; + } + + if (completionBlock) { + completionBlock(BackgroundTaskState_Expired); + } + }); + }]; + + // If a background task could not be begun, call the completion block. + if (!self.taskId) { + OWSLogError(@"%@ background task could not be started.", self.label); + + // Make a local copy of completionBlock to ensure that it is called + // exactly once. + BackgroundTaskCompletionBlock _Nullable completionBlock; + @synchronized(self) + { + completionBlock = self.completionBlock; + self.completionBlock = nil; + } + if (completionBlock) { + DispatchMainThreadSafe(^{ + completionBlock(BackgroundTaskState_CouldNotStart); + }); + } + } +} + +- (void)endBackgroundTask +{ + // Make a local copy of this state, since this method is called by `dealloc`. + BackgroundTaskCompletionBlock _Nullable completionBlock; + + @synchronized(self) + { + if (!self.taskId) { + return; + } + [OWSBackgroundTaskManager.sharedManager removeTask:self.taskId]; + self.taskId = nil; + + completionBlock = self.completionBlock; + self.completionBlock = nil; + } + + // endBackgroundTask must be called on the main thread. + DispatchMainThreadSafe(^{ + if (completionBlock) { + completionBlock(BackgroundTaskState_Success); + } + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBackupFragment.h b/SignalUtilitiesKit/OWSBackupFragment.h new file mode 100644 index 000000000..3acddd799 --- /dev/null +++ b/SignalUtilitiesKit/OWSBackupFragment.h @@ -0,0 +1,44 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +// We store metadata for known backup fragments (i.e. CloudKit record) in +// the database. We might learn about them from: +// +// * Past backup exports. +// * An import downloading and parsing the manifest of the last complete backup. +// +// Storing this data in the database provides continuity. +// +// * Backup exports can reuse fragments from previous Backup exports even if they +// don't complete (i.e. backup export resume). +// * Backup exports can reuse fragments from the backup import, if any. +@interface OWSBackupFragment : TSYapDatabaseObject + +@property (nonatomic) NSString *recordName; + +@property (nonatomic) NSData *encryptionKey; + +// This property is only set for certain types of manifest item, +// namely attachments where we need to know where the attachment's +// file should reside relative to the attachments folder. +@property (nonatomic, nullable) NSString *relativeFilePath; + +// This property is only set for attachments. +@property (nonatomic, nullable) NSString *attachmentId; + +// This property is only set if the manifest item is downloaded. +@property (nonatomic, nullable) NSString *downloadFilePath; + +// This property is only set if the manifest item is compressed. +@property (nonatomic, nullable) NSNumber *uncompressedDataLength; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBackupFragment.m b/SignalUtilitiesKit/OWSBackupFragment.m new file mode 100644 index 000000000..87627f26f --- /dev/null +++ b/SignalUtilitiesKit/OWSBackupFragment.m @@ -0,0 +1,13 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackupFragment.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSBackupFragment + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBatchMessageProcessor.h b/SignalUtilitiesKit/OWSBatchMessageProcessor.h new file mode 100644 index 000000000..ae364752f --- /dev/null +++ b/SignalUtilitiesKit/OWSBatchMessageProcessor.h @@ -0,0 +1,40 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class OWSStorage; +@class SSKProtoEnvelope; +@class YapDatabaseReadWriteTransaction; + +@interface OWSMessageContentQueue : NSObject + +- (dispatch_queue_t)serialQueue; + +@end + +// This class is used to write incoming (decrypted, unprocessed) +// messages to a durable queue and then process them in batches, +// in the order in which they were received. +@interface OWSBatchMessageProcessor : NSObject + +@property (nonatomic, readonly) OWSMessageContentQueue *processingQueue; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + ++ (NSString *)databaseExtensionName; ++ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage; + +- (void)enqueueEnvelopeData:(NSData *)envelopeData + plaintextData:(NSData *_Nullable)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBatchMessageProcessor.m b/SignalUtilitiesKit/OWSBatchMessageProcessor.m new file mode 100644 index 000000000..19ecd4c2b --- /dev/null +++ b/SignalUtilitiesKit/OWSBatchMessageProcessor.m @@ -0,0 +1,546 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBatchMessageProcessor.h" +#import "AppContext.h" +#import "AppReadiness.h" +#import "NSArray+OWS.h" +#import "NotificationsProtocol.h" +#import "OWSBackgroundTask.h" +#import "OWSMessageManager.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSPrimaryStorage.h" +#import "OWSQueues.h" +#import "OWSStorage.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSDatabaseView.h" +#import "TSErrorMessage.h" +#import "TSYapDatabaseObject.h" +#import +#import +#import +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Persisted data model + +@interface OWSMessageContentJob : TSYapDatabaseObject + +@property (nonatomic, readonly) NSDate *createdAt; +@property (nonatomic, readonly) NSData *envelopeData; +@property (nonatomic, readonly, nullable) NSData *plaintextData; +@property (nonatomic, readonly) BOOL wasReceivedByUD; + +- (instancetype)initWithEnvelopeData:(NSData *)envelopeData + plaintextData:(NSData *_Nullable)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId NS_UNAVAILABLE; + +@property (nonatomic, readonly, nullable) SSKProtoEnvelope *envelope; + +@end + +#pragma mark - + +@implementation OWSMessageContentJob + ++ (NSString *)collection +{ + return @"OWSBatchMessageProcessingJob"; +} + +- (instancetype)initWithEnvelopeData:(NSData *)envelopeData + plaintextData:(NSData *_Nullable)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD +{ + OWSAssertDebug(envelopeData); + + self = [super initWithUniqueId:[NSUUID new].UUIDString]; + + if (!self) { + return self; + } + + _envelopeData = envelopeData; + _plaintextData = plaintextData; + _wasReceivedByUD = wasReceivedByUD; + _createdAt = [NSDate new]; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable SSKProtoEnvelope *)envelope +{ + NSError *error; + SSKProtoEnvelope *_Nullable result = [SSKProtoEnvelope parseData:self.envelopeData error:&error]; + + if (error) { + OWSFailDebug(@"paring SSKProtoEnvelope failed with error: %@", error); + return nil; + } + + return result; +} + +@end + +#pragma mark - Finder + +NSString *const OWSMessageContentJobFinderExtensionName = @"OWSMessageContentJobFinderExtensionName2"; +NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJobFinderExtensionGroup2"; + +@interface OWSMessageContentJobFinder : NSObject + +@end + +#pragma mark - + +@interface OWSMessageContentJobFinder () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@end + +#pragma mark - + +@implementation OWSMessageContentJobFinder + +- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection +{ + OWSSingletonAssert(); + + self = [super init]; + if (!self) { + return self; + } + + _dbConnection = dbConnection; + + return self; +} + +- (NSArray *)nextJobsForBatchSize:(NSUInteger)maxBatchSize +{ + NSMutableArray *jobs = [NSMutableArray new]; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + YapDatabaseViewTransaction *viewTransaction = [transaction ext:OWSMessageContentJobFinderExtensionName]; + OWSAssertDebug(viewTransaction != nil); + [viewTransaction enumerateKeysAndObjectsInGroup:OWSMessageContentJobFinderExtensionGroup + usingBlock:^(NSString *_Nonnull collection, + NSString *_Nonnull key, + id _Nonnull object, + NSUInteger index, + BOOL *_Nonnull stop) { + OWSMessageContentJob *job = object; + [jobs addObject:job]; + if (jobs.count >= maxBatchSize) { + *stop = YES; + } + }]; + }]; + + return [jobs copy]; +} + +- (void)addJobWithEnvelopeData:(NSData *)envelopeData + plaintextData:(NSData *_Nullable)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(envelopeData); + OWSAssertDebug(transaction); + + OWSMessageContentJob *job = [[OWSMessageContentJob alloc] initWithEnvelopeData:envelopeData + plaintextData:plaintextData + wasReceivedByUD:wasReceivedByUD]; + [job saveWithTransaction:transaction]; +} + +- (void)removeJobsWithIds:(NSArray *)uniqueIds +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [transaction removeObjectsForKeys:uniqueIds inCollection:[OWSMessageContentJob 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:[OWSMessageContentJob class]]) { + OWSFailDebug(@"Unexpected object: %@ in collection: %@", [object1 class], collection1); + return NSOrderedSame; + } + OWSMessageContentJob *job1 = (OWSMessageContentJob *)object1; + + if (![object2 isKindOfClass:[OWSMessageContentJob class]]) { + OWSFailDebug(@"Unexpected object: %@ in collection: %@", [object2 class], collection2); + return NSOrderedSame; + } + OWSMessageContentJob *job2 = (OWSMessageContentJob *)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:[OWSMessageContentJob 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 OWSMessageContentJobFinderExtensionGroup; + }]; + + YapDatabaseViewOptions *options = [YapDatabaseViewOptions new]; + options.allowedCollections = + [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[OWSMessageContentJob collection]]]; + + return [[YapDatabaseAutoView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"1" options:options]; +} + + ++ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage +{ + YapDatabaseView *existingView = [storage registeredExtension:OWSMessageContentJobFinderExtensionName]; + if (existingView) { + OWSFailDebug(@"%@ was already initialized.", OWSMessageContentJobFinderExtensionName); + // already initialized + return; + } + [storage asyncRegisterExtension:[self databaseExtension] withName:OWSMessageContentJobFinderExtensionName]; +} + +@end + +#pragma mark - Queue Processing + +@interface OWSMessageContentQueue () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; +@property (nonatomic, readonly) OWSMessageContentJobFinder *finder; +@property (nonatomic) BOOL isDrainingQueue; +@property (atomic) BOOL isAppInBackground; + +- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection + finder:(OWSMessageContentJobFinder *)finder NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + +#pragma mark - + +@implementation OWSMessageContentQueue + +- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection finder:(OWSMessageContentJobFinder *)finder +{ + OWSSingletonAssert(); + + self = [super init]; + + if (!self) { + return self; + } + + _dbConnection = dbConnection; + _finder = finder; + _isDrainingQueue = NO; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:OWSApplicationWillEnterForegroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:OWSApplicationDidEnterBackgroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(registrationStateDidChange:) + name:RegistrationStateDidChangeNotification + object:nil]; + + // Start processing. + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + if (CurrentAppContext().isMainApp) { + [self drainQueue]; + } + }]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Singletons + +- (OWSMessageManager *)messageManager +{ + OWSAssertDebug(SSKEnvironment.shared.messageManager); + + return SSKEnvironment.shared.messageManager; +} + +- (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +#pragma mark - Notifications + +- (void)applicationWillEnterForeground:(NSNotification *)notification +{ + self.isAppInBackground = NO; +} + +- (void)applicationDidEnterBackground:(NSNotification *)notification +{ + self.isAppInBackground = YES; +} + +- (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.process", DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + +- (void)enqueueEnvelopeData:(NSData *)envelopeData + plaintextData:(NSData *_Nullable)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(envelopeData); + OWSAssertDebug(transaction); + + // We need to persist the decrypted envelope data ASAP to prevent data loss. + [self.finder addJobWithEnvelopeData:envelopeData + plaintextData:plaintextData + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; +} + +- (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); + + // We want a value that is just high enough to yield performance benefits + const NSUInteger kIncomingMessageBatchSize = 32; + + NSArray *batchJobs = [self.finder nextJobsForBatchSize:kIncomingMessageBatchSize]; + OWSAssertDebug(batchJobs); + if (batchJobs.count < 1) { + self.isDrainingQueue = NO; + OWSLogVerbose(@"Queue is drained"); + return; + } + + OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + + NSArray *processedJobs = [self processJobs:batchJobs]; + + [self.finder removeJobsWithIds:processedJobs.uniqueIds]; + + OWSAssertDebug(backgroundTask); + backgroundTask = nil; + + OWSLogVerbose(@"completed %lu/%lu jobs. %lu jobs left.", + (unsigned long)processedJobs.count, + (unsigned long)batchJobs.count, + (unsigned long)[OWSMessageContentJob numberOfKeysInCollection]); + + // Wait a bit in hopes of increasing the batch size. + // This delay won't affect the first message to arrive when this queue is idle, + // so by definition we're receiving more than one message and can benefit from + // batching. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), self.serialQueue, ^{ + [self drainQueueWorkStep]; + }); +} + +- (NSArray *)processJobs:(NSArray *)jobs +{ + AssertOnDispatchQueue(self.serialQueue); + + NSMutableArray *processedJobs = [NSMutableArray new]; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (OWSMessageContentJob *job in jobs) { + + void (^reportFailure)(YapDatabaseReadWriteTransaction *transaction) = ^( + YapDatabaseReadWriteTransaction *transaction) { + TSErrorMessage *errorMessage = [TSErrorMessage corruptedMessageInUnknownThread]; + [SSKEnvironment.shared.notificationsManager notifyUserForThreadlessErrorMessage:errorMessage transaction:transaction]; + }; + + @try { + SSKProtoEnvelope *_Nullable envelope = job.envelope; + if (!envelope) { + reportFailure(transaction); + } else { + [self.messageManager throws_processEnvelope:envelope + plaintextData:job.plaintextData + wasReceivedByUD:job.wasReceivedByUD + transaction:transaction + serverID:0]; + } + } @catch (NSException *exception) { + reportFailure(transaction); + } + + [processedJobs addObject:job]; + + if (self.isAppInBackground) { + // If the app is in the background, stop processing this batch. + // + // Since this check is done after processing jobs, we'll continue + // to process jobs in batches of 1. This reduces the cost of + // being interrupted and rolled back if app is suspended. + break; + } + } + }]; + + return processedJobs; +} + +@end + +#pragma mark - OWSBatchMessageProcessor + +@interface OWSBatchMessageProcessor () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@end + +#pragma mark - + +@implementation OWSBatchMessageProcessor + +- (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]; + OWSMessageContentJobFinder *finder = [[OWSMessageContentJobFinder alloc] initWithDBConnection:dbConnection]; + OWSMessageContentQueue *processingQueue = + [[OWSMessageContentQueue alloc] initWithDBConnection:dbConnection finder:finder]; + + _processingQueue = processingQueue; + + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + if (CurrentAppContext().isMainApp) { + [self.processingQueue drainQueue]; + } + }]; + + return self; +} + +#pragma mark - class methods + ++ (NSString *)databaseExtensionName +{ + return OWSMessageContentJobFinderExtensionName; +} + ++ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage +{ + [OWSMessageContentJobFinder asyncRegisterDatabaseExtension:storage]; +} + +#pragma mark - instance methods + +- (void)enqueueEnvelopeData:(NSData *)envelopeData + plaintextData:(NSData *_Nullable)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (envelopeData.length < 1) { + OWSFailDebug(@"Received an empty envelope."); + return; + } + OWSAssert(transaction); + + // We need to persist the decrypted envelope data ASAP to prevent data loss. + [self.processingQueue enqueueEnvelopeData:envelopeData + plaintextData:plaintextData + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; + + // The new envelope won't be visible to the finder until this transaction commits, + // so drainQueue in the transaction completion. + [transaction addCompletionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + completionBlock:^{ + [self.processingQueue drainQueue]; + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBlockedPhoneNumbersMessage.h b/SignalUtilitiesKit/OWSBlockedPhoneNumbersMessage.h new file mode 100644 index 000000000..aefd4f831 --- /dev/null +++ b/SignalUtilitiesKit/OWSBlockedPhoneNumbersMessage.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSBlockedPhoneNumbersMessage : OWSOutgoingSyncMessage + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithPhoneNumbers:(NSArray *)phoneNumbers + groupIds:(NSArray *)groupIds NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBlockedPhoneNumbersMessage.m b/SignalUtilitiesKit/OWSBlockedPhoneNumbersMessage.m new file mode 100644 index 000000000..be4eb3f0f --- /dev/null +++ b/SignalUtilitiesKit/OWSBlockedPhoneNumbersMessage.m @@ -0,0 +1,57 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBlockedPhoneNumbersMessage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSBlockedPhoneNumbersMessage () + +@property (nonatomic, readonly) NSArray *phoneNumbers; +@property (nonatomic, readonly) NSArray *groupIds; + +@end + +@implementation OWSBlockedPhoneNumbersMessage + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (instancetype)initWithPhoneNumbers:(NSArray *)phoneNumbers groupIds:(NSArray *)groupIds +{ + self = [super init]; + if (!self) { + return self; + } + + _phoneNumbers = [phoneNumbers copy]; + _groupIds = [groupIds copy]; + + return self; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + SSKProtoSyncMessageBlockedBuilder *blockedBuilder = [SSKProtoSyncMessageBlocked builder]; + [blockedBuilder setNumbers:_phoneNumbers]; + [blockedBuilder setGroupIds:_groupIds]; + + NSError *error; + SSKProtoSyncMessageBlocked *_Nullable blockedProto = [blockedBuilder buildAndReturnError:&error]; + if (error || !blockedProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + + SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder]; + [syncMessageBuilder setBlocked:blockedProto]; + return syncMessageBuilder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBlockingManager.h b/SignalUtilitiesKit/OWSBlockingManager.h new file mode 100644 index 000000000..95ef4913a --- /dev/null +++ b/SignalUtilitiesKit/OWSBlockingManager.h @@ -0,0 +1,51 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class TSGroupModel; +@class TSThread; + +extern NSString *const kNSNotificationName_BlockListDidChange; + +extern NSString *const kOWSBlockingManager_BlockListCollection; + +// This class can be safely accessed and used from any thread. +@interface OWSBlockingManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + ++ (instancetype)sharedManager; + +- (void)addBlockedPhoneNumber:(NSString *)phoneNumber; + +- (void)removeBlockedPhoneNumber:(NSString *)phoneNumber; + +// When updating the block list from a sync message, we don't +// want to fire a sync message. +- (void)setBlockedPhoneNumbers:(NSArray *)blockedPhoneNumbers sendSyncMessage:(BOOL)sendSyncMessage; + +// TODO convert to property +- (NSArray *)blockedPhoneNumbers; + +@property (readonly) NSArray *blockedGroupIds; +@property (readonly) NSArray *blockedGroups; + +- (void)addBlockedGroup:(TSGroupModel *)group; +- (void)removeBlockedGroupId:(NSData *)groupId; +- (nullable TSGroupModel *)cachedGroupDetailsWithGroupId:(NSData *)groupId; + +- (BOOL)isRecipientIdBlocked:(NSString *)recipientId; +- (BOOL)isGroupIdBlocked:(NSData *)groupId; +- (BOOL)isThreadBlocked:(TSThread *)thread; + +- (void)syncBlockList; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSBlockingManager.m b/SignalUtilitiesKit/OWSBlockingManager.m new file mode 100644 index 000000000..3bb675757 --- /dev/null +++ b/SignalUtilitiesKit/OWSBlockingManager.m @@ -0,0 +1,443 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBlockingManager.h" +#import "AppContext.h" +#import "AppReadiness.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSBlockedPhoneNumbersMessage.h" +#import "OWSMessageSender.h" +#import "OWSPrimaryStorage.h" +#import "SSKEnvironment.h" +#import "TSContactThread.h" +#import "TSGroupThread.h" +#import "YapDatabaseConnection+OWS.h" +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kNSNotificationName_BlockListDidChange = @"kNSNotificationName_BlockListDidChange"; + +NSString *const kOWSBlockingManager_BlockListCollection = @"kOWSBlockingManager_BlockedPhoneNumbersCollection"; + +// These keys are used to persist the current local "block list" state. +NSString *const kOWSBlockingManager_BlockedPhoneNumbersKey = @"kOWSBlockingManager_BlockedPhoneNumbersKey"; +NSString *const kOWSBlockingManager_BlockedGroupMapKey = @"kOWSBlockingManager_BlockedGroupMapKey"; + +// These keys are used to persist the most recently synced remote "block list" state. +NSString *const kOWSBlockingManager_SyncedBlockedPhoneNumbersKey = @"kOWSBlockingManager_SyncedBlockedPhoneNumbersKey"; +NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingManager_SyncedBlockedGroupIdsKey"; + +@interface OWSBlockingManager () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +// We don't store the phone numbers as instances of PhoneNumber to avoid +// consistency issues between clients, but these should all be valid e164 +// phone numbers. +@property (atomic, readonly) NSMutableSet *blockedPhoneNumberSet; +@property (atomic, readonly) NSMutableDictionary *blockedGroupMap; + +@end + +#pragma mark - + +@implementation OWSBlockingManager + ++ (instancetype)sharedManager +{ + OWSAssertDebug(SSKEnvironment.shared.blockingManager); + + return SSKEnvironment.shared.blockingManager; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSAssertDebug(primaryStorage); + + _dbConnection = primaryStorage.newDatabaseConnection; + + OWSSingletonAssert(); + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:OWSApplicationDidBecomeActiveNotification + object:nil]; +} + +- (OWSMessageSender *)messageSender +{ + OWSAssertDebug(SSKEnvironment.shared.messageSender); + + return SSKEnvironment.shared.messageSender; +} + +#pragma mark - + +- (BOOL)isThreadBlocked:(TSThread *)thread +{ + if ([thread isKindOfClass:[TSContactThread class]]) { + TSContactThread *contactThread = (TSContactThread *)thread; + return [self isRecipientIdBlocked:contactThread.contactIdentifier]; + } else if ([thread isKindOfClass:[TSGroupThread class]]) { + TSGroupThread *groupThread = (TSGroupThread *)thread; + return [self isGroupIdBlocked:groupThread.groupModel.groupId]; + } else { + OWSFailDebug(@"%@ failure unexpected thread type", self.logTag); + return NO; + } +} + +#pragma mark - Contact Blocking + +- (void)addBlockedPhoneNumber:(NSString *)phoneNumber +{ + OWSAssertDebug(phoneNumber.length > 0); + + OWSLogInfo(@"addBlockedPhoneNumber: %@", phoneNumber); + + @synchronized(self) + { + [self ensureLazyInitialization]; + + if ([_blockedPhoneNumberSet containsObject:phoneNumber]) { + // Ignore redundant changes. + return; + } + + [_blockedPhoneNumberSet addObject:phoneNumber]; + } + + [self handleUpdate]; +} + +- (void)removeBlockedPhoneNumber:(NSString *)phoneNumber +{ + OWSAssertDebug(phoneNumber.length > 0); + + OWSLogInfo(@"removeBlockedPhoneNumber: %@", phoneNumber); + + @synchronized(self) + { + [self ensureLazyInitialization]; + + if (![_blockedPhoneNumberSet containsObject:phoneNumber]) { + // Ignore redundant changes. + return; + } + + [_blockedPhoneNumberSet removeObject:phoneNumber]; + } + + [self handleUpdate]; +} + +- (void)setBlockedPhoneNumbers:(NSArray *)blockedPhoneNumbers sendSyncMessage:(BOOL)sendSyncMessage +{ + OWSAssertDebug(blockedPhoneNumbers != nil); + + OWSLogInfo(@"setBlockedPhoneNumbers: %d", (int)blockedPhoneNumbers.count); + + @synchronized(self) + { + [self ensureLazyInitialization]; + + NSSet *newSet = [NSSet setWithArray:blockedPhoneNumbers]; + if ([_blockedPhoneNumberSet isEqualToSet:newSet]) { + return; + } + + _blockedPhoneNumberSet = [newSet mutableCopy]; + } + + [self handleUpdate:sendSyncMessage]; +} + +- (NSArray *)blockedPhoneNumbers +{ + @synchronized(self) + { + [self ensureLazyInitialization]; + + return [_blockedPhoneNumberSet.allObjects sortedArrayUsingSelector:@selector(compare:)]; + } +} + +- (BOOL)isRecipientIdBlocked:(NSString *)recipientId +{ + __block NSString *masterPublicKey; + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) { + masterPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:recipientId in:transaction] ?: recipientId; + }]; + return [self.blockedPhoneNumbers containsObject:masterPublicKey]; +} + +#pragma mark - Group Blocking + +- (NSArray *)blockedGroupIds +{ + @synchronized(self) { + [self ensureLazyInitialization]; + return self.blockedGroupMap.allKeys; + } +} + +- (NSArray *)blockedGroups +{ + @synchronized(self) { + [self ensureLazyInitialization]; + return self.blockedGroupMap.allValues; + } +} + +- (BOOL)isGroupIdBlocked:(NSData *)groupId +{ + return self.blockedGroupMap[groupId] != nil; +} + +- (nullable TSGroupModel *)cachedGroupDetailsWithGroupId:(NSData *)groupId +{ + @synchronized(self) { + return self.blockedGroupMap[groupId]; + } +} + +- (void)addBlockedGroup:(TSGroupModel *)groupModel +{ + NSData *groupId = groupModel.groupId; + OWSAssertDebug(groupId.length > 0); + + OWSLogInfo(@"groupId: %@", groupId); + + @synchronized(self) { + [self ensureLazyInitialization]; + + if ([self isGroupIdBlocked:groupId]) { + // Ignore redundant changes. + return; + } + self.blockedGroupMap[groupId] = groupModel; + } + + [self handleUpdate]; +} + +- (void)removeBlockedGroupId:(NSData *)groupId +{ + OWSAssertDebug(groupId.length > 0); + + OWSLogInfo(@"groupId: %@", groupId); + + @synchronized(self) { + [self ensureLazyInitialization]; + + if (![self isGroupIdBlocked:groupId]) { + // Ignore redundant changes. + return; + } + + [self.blockedGroupMap removeObjectForKey:groupId]; + } + + [self handleUpdate]; +} + + +#pragma mark - Updates + +// This should be called every time the block list changes. + +- (void)handleUpdate +{ + // By default, always send a sync message when the block list changes. + [self handleUpdate:YES]; +} + +// TODO label the `sendSyncMessage` param +- (void)handleUpdate:(BOOL)sendSyncMessage +{ + NSArray *blockedPhoneNumbers = [self blockedPhoneNumbers]; + + [self.dbConnection setObject:blockedPhoneNumbers + forKey:kOWSBlockingManager_BlockedPhoneNumbersKey + inCollection:kOWSBlockingManager_BlockListCollection]; + + NSDictionary *blockedGroupMap; + @synchronized(self) { + blockedGroupMap = [self.blockedGroupMap copy]; + } + NSArray *blockedGroupIds = blockedGroupMap.allKeys; + + [self.dbConnection setObject:blockedGroupMap + forKey:kOWSBlockingManager_BlockedGroupMapKey + inCollection:kOWSBlockingManager_BlockListCollection]; + + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (sendSyncMessage) { + [self sendBlockListSyncMessageWithPhoneNumbers:blockedPhoneNumbers groupIds:blockedGroupIds]; + } else { + // If this update came from an incoming block list sync message, + // update the "synced blocked list" state immediately, + // since we're now in sync. + // + // There could be data loss if both clients modify the block list + // at the same time, but: + // + // a) Block list changes will be rare. + // b) Conflicting block list changes will be even rarer. + // c) It's unlikely a user will make conflicting changes on two + // devices around the same time. + // d) There isn't a good way to avoid this. + [self saveSyncedBlockListWithPhoneNumbers:blockedPhoneNumbers groupIds:blockedGroupIds]; + } + + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_BlockListDidChange + object:nil + userInfo:nil]; + }); +} + +// This method should only be called from within a synchronized block. +- (void)ensureLazyInitialization +{ + if (_blockedPhoneNumberSet) { + OWSAssertDebug(_blockedGroupMap); + + // already loaded + return; + } + + NSArray *blockedPhoneNumbers = + [self.dbConnection objectForKey:kOWSBlockingManager_BlockedPhoneNumbersKey + inCollection:kOWSBlockingManager_BlockListCollection]; + _blockedPhoneNumberSet = [[NSMutableSet alloc] initWithArray:(blockedPhoneNumbers ?: [NSArray new])]; + + NSDictionary *storedBlockedGroupMap = + [self.dbConnection objectForKey:kOWSBlockingManager_BlockedGroupMapKey + inCollection:kOWSBlockingManager_BlockListCollection]; + if ([storedBlockedGroupMap isKindOfClass:[NSDictionary class]]) { + _blockedGroupMap = [storedBlockedGroupMap mutableCopy]; + } else { + _blockedGroupMap = [NSMutableDictionary new]; + } + + [self syncBlockListIfNecessary]; + [self observeNotifications]; +} + +- (void)syncBlockList +{ + OWSAssertDebug(_blockedPhoneNumberSet); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self sendBlockListSyncMessageWithPhoneNumbers:self.blockedPhoneNumbers groupIds:self.blockedGroupIds]; + }); +} + +// This method should only be called from within a synchronized block. +- (void)syncBlockListIfNecessary +{ + /* + OWSAssertDebug(_blockedPhoneNumberSet); + + // If we haven't yet successfully synced the current "block list" changes, + // try again to sync now. + NSArray *syncedBlockedPhoneNumbers = + [self.dbConnection objectForKey:kOWSBlockingManager_SyncedBlockedPhoneNumbersKey + inCollection:kOWSBlockingManager_BlockListCollection]; + NSSet *syncedBlockedPhoneNumberSet = + [[NSSet alloc] initWithArray:(syncedBlockedPhoneNumbers ?: [NSArray new])]; + + NSArray *syncedBlockedGroupIds = + [self.dbConnection objectForKey:kOWSBlockingManager_SyncedBlockedGroupIdsKey + inCollection:kOWSBlockingManager_BlockListCollection]; + NSSet *syncedBlockedGroupIdSet = [[NSSet alloc] initWithArray:(syncedBlockedGroupIds ?: [NSArray new])]; + + NSArray *localBlockedGroupIds = self.blockedGroupIds; + NSSet *localBlockedGroupIdSet = [[NSSet alloc] initWithArray:localBlockedGroupIds]; + + if ([self.blockedPhoneNumberSet isEqualToSet:syncedBlockedPhoneNumberSet] && + [localBlockedGroupIdSet isEqualToSet:syncedBlockedGroupIdSet]) { + OWSLogVerbose(@"Ignoring redundant block list sync"); + return; + } + + OWSLogInfo(@"retrying sync of block list"); + [self sendBlockListSyncMessageWithPhoneNumbers:self.blockedPhoneNumbers groupIds:localBlockedGroupIds]; + */ +} + +- (void)sendBlockListSyncMessageWithPhoneNumbers:(NSArray *)blockedPhoneNumbers + groupIds:(NSArray *)blockedGroupIds +{ + OWSAssertDebug(blockedPhoneNumbers); + OWSAssertDebug(blockedGroupIds); + + OWSBlockedPhoneNumbersMessage *message = + [[OWSBlockedPhoneNumbersMessage alloc] initWithPhoneNumbers:blockedPhoneNumbers groupIds:blockedGroupIds]; + + [self.messageSender sendMessage:message + success:^{ + OWSLogInfo(@"Successfully sent blocked phone numbers sync message"); + + // DURABLE CLEANUP - we could replace the custom durability logic in this class + // with a durable JobQueue. + [self saveSyncedBlockListWithPhoneNumbers:blockedPhoneNumbers groupIds:blockedGroupIds]; + } + failure:^(NSError *error) { + OWSLogError(@"Failed to send blocked phone numbers sync message with error: %@", error); + }]; +} + +/// Records the last block list which we successfully synced. +- (void)saveSyncedBlockListWithPhoneNumbers:(NSArray *)blockedPhoneNumbers + groupIds:(NSArray *)blockedGroupIds +{ + OWSAssertDebug(blockedPhoneNumbers); + OWSAssertDebug(blockedGroupIds); + + [self.dbConnection setObject:blockedPhoneNumbers + forKey:kOWSBlockingManager_SyncedBlockedPhoneNumbersKey + inCollection:kOWSBlockingManager_BlockListCollection]; + + [self.dbConnection setObject:blockedGroupIds + forKey:kOWSBlockingManager_SyncedBlockedGroupIdsKey + inCollection:kOWSBlockingManager_BlockListCollection]; +} + +#pragma mark - Notifications + +- (void)applicationDidBecomeActive:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + @synchronized(self) + { + [self syncBlockListIfNecessary]; + } + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSCallMessageHandler.h b/SignalUtilitiesKit/OWSCallMessageHandler.h new file mode 100644 index 000000000..13b471857 --- /dev/null +++ b/SignalUtilitiesKit/OWSCallMessageHandler.h @@ -0,0 +1,30 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoCallMessageAnswer; +@class SSKProtoCallMessageBusy; +@class SSKProtoCallMessageHangup; +@class SSKProtoCallMessageIceUpdate; +@class SSKProtoCallMessageOffer; + +@protocol OWSCallMessageHandler + +- (void)receivedOffer:(SSKProtoCallMessageOffer *)offer + fromCallerId:(NSString *)callerId NS_SWIFT_NAME(receivedOffer(_:from:)); +- (void)receivedAnswer:(SSKProtoCallMessageAnswer *)answer + fromCallerId:(NSString *)callerId NS_SWIFT_NAME(receivedAnswer(_:from:)); +- (void)receivedIceUpdate:(SSKProtoCallMessageIceUpdate *)iceUpdate + fromCallerId:(NSString *)callerId NS_SWIFT_NAME(receivedIceUpdate(_:from:)); +- (void)receivedHangup:(SSKProtoCallMessageHangup *)hangup + fromCallerId:(NSString *)callerId NS_SWIFT_NAME(receivedHangup(_:from:)); +- (void)receivedBusy:(SSKProtoCallMessageBusy *)busy + fromCallerId:(NSString *)callerId NS_SWIFT_NAME(receivedBusy(_:from:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSCensorshipConfiguration.h b/SignalUtilitiesKit/OWSCensorshipConfiguration.h new file mode 100644 index 000000000..4d1aac625 --- /dev/null +++ b/SignalUtilitiesKit/OWSCensorshipConfiguration.h @@ -0,0 +1,35 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class AFSecurityPolicy; + +extern NSString *const OWSFrontingHost_GoogleEgypt; +extern NSString *const OWSFrontingHost_GoogleUAE; +extern NSString *const OWSFrontingHost_GoogleOman; +extern NSString *const OWSFrontingHost_GoogleQatar; + +@interface OWSCensorshipConfiguration : NSObject + +// returns nil if phone number is not known to be censored ++ (nullable instancetype)censorshipConfigurationWithPhoneNumber:(NSString *)e164PhoneNumber; + +// returns best censorship configuration for country code. Will return a default if one hasn't +// been specifically configured. ++ (instancetype)censorshipConfigurationWithCountryCode:(NSString *)countryCode; ++ (instancetype)defaultConfiguration; + ++ (BOOL)isCensoredPhoneNumber:(NSString *)e164PhoneNumber; + +@property (nonatomic, readonly) NSString *signalServiceReflectorHost; +@property (nonatomic, readonly) NSString *CDNReflectorHost; +@property (nonatomic, readonly) NSURL *domainFrontBaseURL; +@property (nonatomic, readonly) AFSecurityPolicy *domainFrontSecurityPolicy; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSCensorshipConfiguration.m b/SignalUtilitiesKit/OWSCensorshipConfiguration.m new file mode 100644 index 000000000..6a59e715f --- /dev/null +++ b/SignalUtilitiesKit/OWSCensorshipConfiguration.m @@ -0,0 +1,247 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSCensorshipConfiguration.h" +#import "OWSCountryMetadata.h" +#import "OWSError.h" +#import "OWSPrimaryStorage.h" +#import "TSConstants.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSFrontingHost_GoogleEgypt = @"www.google.com.eg"; +NSString *const OWSFrontingHost_GoogleUAE = @"www.google.ae"; +NSString *const OWSFrontingHost_GoogleOman = @"www.google.com.om"; +NSString *const OWSFrontingHost_GoogleQatar = @"www.google.com.qa"; +NSString *const OWSFrontingHost_Default = @"www.google.com"; + +@implementation OWSCensorshipConfiguration + +// returns nil if phone number is not known to be censored ++ (nullable instancetype)censorshipConfigurationWithPhoneNumber:(NSString *)e164PhoneNumber +{ + NSString *countryCode = [self censoredCountryCodeWithPhoneNumber:e164PhoneNumber]; + if (countryCode.length == 0) { + return nil; + } + + return [self censorshipConfigurationWithCountryCode:countryCode]; +} + +// returns best censorship configuration for country code. Will return a default if one hasn't +// been specifically configured. ++ (instancetype)censorshipConfigurationWithCountryCode:(NSString *)countryCode +{ + OWSCountryMetadata *countryMetadadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode]; + OWSAssertDebug(countryMetadadata); + + NSString *_Nullable specifiedDomain = countryMetadadata.frontingDomain; + if (specifiedDomain.length == 0) { + return self.defaultConfiguration; + } + + NSString *frontingURLString = [NSString stringWithFormat:@"https://%@", specifiedDomain]; + NSURL *_Nullable baseURL = [NSURL URLWithString:frontingURLString]; + if (baseURL == nil) { + OWSFailDebug(@"baseURL was unexpectedly nil with specifiedDomain: %@", specifiedDomain); + return self.defaultConfiguration; + } + AFSecurityPolicy *securityPolicy = [self securityPolicyForDomain:specifiedDomain]; + OWSAssertDebug(securityPolicy); + + return [[OWSCensorshipConfiguration alloc] initWithDomainFrontBaseURL:baseURL securityPolicy:securityPolicy]; +} + ++ (instancetype)defaultConfiguration +{ + NSString *frontingURLString = [NSString stringWithFormat:@"https://%@", OWSFrontingHost_Default]; + NSURL *baseURL = [NSURL URLWithString:frontingURLString]; + AFSecurityPolicy *securityPolicy = [self securityPolicyForDomain:OWSFrontingHost_Default]; + + return [[OWSCensorshipConfiguration alloc] initWithDomainFrontBaseURL:baseURL securityPolicy:securityPolicy]; +} + +- (instancetype)initWithDomainFrontBaseURL:(NSURL *)domainFrontBaseURL securityPolicy:(AFSecurityPolicy *)securityPolicy +{ + OWSAssertDebug(domainFrontBaseURL); + OWSAssertDebug(securityPolicy); + + self = [super init]; + if (!self) { + return self; + } + + _domainFrontBaseURL = domainFrontBaseURL; + _domainFrontSecurityPolicy = securityPolicy; + + return self; +} + +// MARK: Public Getters + +- (NSString *)signalServiceReflectorHost +{ + return textSecureServiceReflectorHost; +} + +- (NSString *)CDNReflectorHost +{ + return textSecureCDNReflectorHost; +} + +// MARK: Util + ++ (NSDictionary *)censoredCountryCodes +{ + // The set of countries for which domain fronting should be automatically enabled. + // + // If you want to use a domain front other than the default, specify the domain front + // in OWSCountryMetadata, and ensure we have a Security Policy for that domain in + // `securityPolicyForDomain:` + return @{ + // Egypt + @"+20" : @"EG", + // Oman + @"+968" : @"OM", + // Qatar + @"+974" : @"QA", + // UAE + @"+971" : @"AE", + }; +} + ++ (BOOL)isCensoredPhoneNumber:(NSString *)e164PhoneNumber; +{ + return [self censoredCountryCodeWithPhoneNumber:e164PhoneNumber].length > 0; +} + +// Returns nil if the phone number is not known to be censored ++ (nullable NSString *)censoredCountryCodeWithPhoneNumber:(NSString *)e164PhoneNumber +{ + NSDictionary *censoredCountryCodes = self.censoredCountryCodes; + + for (NSString *callingCode in censoredCountryCodes) { + if ([e164PhoneNumber hasPrefix:callingCode]) { + return censoredCountryCodes[callingCode]; + } + } + + return nil; +} + +#pragma mark - Reflector Pinning Policy + +// When using censorship circumvention, we pin to the fronted domain host. +// Adding a new domain front entails adding a corresponding AFSecurityPolicy +// and pinning to it's CA. +// If the security policy requires new certificates, include them in the SSK bundle ++ (AFSecurityPolicy *)securityPolicyForDomain:(NSString *)domain +{ + if ([domain isEqualToString:OWSFrontingHost_GoogleEgypt]) { + return self.googlePinningPolicy; + } else if ([domain isEqualToString:OWSFrontingHost_GoogleQatar]) { + return self.googlePinningPolicy; + } else if ([domain isEqualToString:OWSFrontingHost_GoogleOman]) { + return self.googlePinningPolicy; + } else if ([domain isEqualToString:OWSFrontingHost_GoogleUAE]) { + return self.googlePinningPolicy; + } else { + OWSFailDebug(@"unknown pinning domain."); + return self.googlePinningPolicy; + } +} + ++ (AFSecurityPolicy *)pinningPolicyWithCertNames:(NSArray *)certNames +{ + NSMutableSet *certificates = [NSMutableSet new]; + for (NSString *certName in certNames) { + NSError *error; + NSData *certData = [self certificateDataWithName:certName error:&error]; + if (error) { + OWSFail(@"reading data for certificate: %@ failed with error: %@", certName, error); + } + + if (!certData) { + OWSFail(@"reading data for certificate: %@ failed with error: %@", certName, error); + } + [certificates addObject:certData]; + } + + return [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:certificates]; +} + ++ (nullable NSData *)certificateDataWithName:(NSString *)name error:(NSError **)error +{ + if (!name.length) { + NSString *failureDescription = [NSString stringWithFormat:@"%@ expected name with length > 0", self.logTag]; + *error = OWSErrorMakeAssertionError(failureDescription); + return nil; + } + + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + NSString *path = [bundle pathForResource:name ofType:@"crt"]; + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSString *failureDescription = + [NSString stringWithFormat:@"%@ Missing certificate for name: %@", self.logTag, name]; + *error = OWSErrorMakeAssertionError(failureDescription); + return nil; + } + + NSData *_Nullable certData = [NSData dataWithContentsOfFile:path options:0 error:error]; + + if (*error != nil) { + OWSFailDebug(@"Failed to read cert file with path: %@", path); + return nil; + } + + if (certData.length == 0) { + OWSFailDebug(@"empty certData for name: %@", name); + return nil; + } + + OWSLogVerbose(@"read cert data with name: %@ length: %lu", name, (unsigned long)certData.length); + return certData; +} + ++ (AFSecurityPolicy *)yahooViewPinningPolicy_deprecated +{ + static AFSecurityPolicy *securityPolicy = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // DigiCertGlobalRootG2 - view.yahoo.com + NSArray *certNames = @[ @"DigiCertSHA2HighAssuranceServerCA" ]; + securityPolicy = [self pinningPolicyWithCertNames:certNames]; + }); + return securityPolicy; +} + ++ (AFSecurityPolicy *)souqPinningPolicy_deprecated +{ + static AFSecurityPolicy *securityPolicy = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // SFSRootCAG2 - cms.souqcdn.com + NSArray *certNames = @[ @"SFSRootCAG2" ]; + securityPolicy = [self pinningPolicyWithCertNames:certNames]; + }); + return securityPolicy; +} + ++ (AFSecurityPolicy *)googlePinningPolicy +{ + static AFSecurityPolicy *securityPolicy = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // GIAG2 cert plus root certs from pki.goog + NSArray *certNames = @[ @"GIAG2", @"GSR2", @"GSR4", @"GTSR1", @"GTSR2", @"GTSR3", @"GTSR4" ]; + securityPolicy = [self pinningPolicyWithCertNames:certNames]; + }); + return securityPolicy; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSChunkedOutputStream.h b/SignalUtilitiesKit/OWSChunkedOutputStream.h new file mode 100644 index 000000000..9e82dadcf --- /dev/null +++ b/SignalUtilitiesKit/OWSChunkedOutputStream.h @@ -0,0 +1,25 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSChunkedOutputStream : NSObject + +// Indicates whether any write failed. +@property (nonatomic, readonly) BOOL hasError; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithOutputStream:(NSOutputStream *)outputStream; + +// Returns NO on error. +- (BOOL)writeData:(NSData *)data; +- (BOOL)writeUInt32:(UInt32)value; +- (BOOL)writeVariableLengthUInt32:(UInt32)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSChunkedOutputStream.m b/SignalUtilitiesKit/OWSChunkedOutputStream.m new file mode 100644 index 000000000..5c4f92ddc --- /dev/null +++ b/SignalUtilitiesKit/OWSChunkedOutputStream.m @@ -0,0 +1,97 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSChunkedOutputStream.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSChunkedOutputStream () + +@property (nonatomic, readonly) NSOutputStream *outputStream; +@property (nonatomic) BOOL hasError; + +@end + +#pragma mark - + +@implementation OWSChunkedOutputStream + +- (instancetype)initWithOutputStream:(NSOutputStream *)outputStream +{ + if (self = [super init]) { + OWSAssertDebug(outputStream); + _outputStream = outputStream; + } + + return self; +} + +- (BOOL)writeByte:(uint8_t)value +{ + NSInteger written = [self.outputStream write:&value maxLength:sizeof(value)]; + if (written != sizeof(value)) { + OWSFailDebug(@"could not write to output stream."); + self.hasError = YES; + return NO; + } + return YES; +} + +- (BOOL)writeData:(NSData *)data +{ + OWSAssertDebug(data); + + if (data.length < 1) { + return YES; + } + + while (YES) { + NSInteger written = [self.outputStream write:data.bytes maxLength:data.length]; + if (written < 1) { + OWSFailDebug(@"could not write to output stream."); + self.hasError = YES; + return NO; + } + if (written < data.length) { + data = [data subdataWithRange:NSMakeRange(written, data.length - written)]; + } else { + return YES; + } + } + return YES; +} + +- (BOOL)writeUInt32:(UInt32)value { + NSData *data = [[NSData alloc] initWithBytes:&value length:sizeof(value)]; + // Both Android and desktop seem to like this better + const char *bytes = data.bytes; + char *reversedBytes = malloc(sizeof(char) * data.length); + int i = data.length - 1; + for (int j = 0; j < data.length; j++) { + reversedBytes[i] = bytes[j]; + i = i - 1; + } + NSData *reversedData = [NSData dataWithBytes:reversedBytes length:data.length]; + return [self writeData:reversedData]; +} + +- (BOOL)writeVariableLengthUInt32:(UInt32)value +{ + while (YES) { + if (value <= 0x7F) { + return [self writeByte:value]; + } else { + if (![self writeByte:((value & 0x7F) | 0x80)]) { + return NO; + } + value >>= 7; + } + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSContact+Private.h b/SignalUtilitiesKit/OWSContact+Private.h new file mode 100644 index 000000000..8ccdbdd8b --- /dev/null +++ b/SignalUtilitiesKit/OWSContact+Private.h @@ -0,0 +1,60 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSContact.h" + +NS_ASSUME_NONNULL_BEGIN + +// These private interfaces expose setter accessors to facilitate +// construction of fake messages, etc. +@interface OWSContactPhoneNumber (Private) + +@property (nonatomic) OWSContactPhoneType phoneType; +@property (nonatomic, nullable) NSString *label; + +@property (nonatomic) NSString *phoneNumber; + +@end + +#pragma mark - + +@interface OWSContactEmail (Private) + +@property (nonatomic) OWSContactEmailType emailType; +@property (nonatomic, nullable) NSString *label; + +@property (nonatomic) NSString *email; + +@end + +#pragma mark - + +@interface OWSContactAddress (Private) + +@property (nonatomic) OWSContactAddressType addressType; +@property (nonatomic, nullable) NSString *label; + +@property (nonatomic, nullable) NSString *street; +@property (nonatomic, nullable) NSString *pobox; +@property (nonatomic, nullable) NSString *neighborhood; +@property (nonatomic, nullable) NSString *city; +@property (nonatomic, nullable) NSString *region; +@property (nonatomic, nullable) NSString *postcode; +@property (nonatomic, nullable) NSString *country; + +@end + +#pragma mark - + +@interface OWSContact (Private) + +@property (nonatomic) NSArray *phoneNumbers; +@property (nonatomic) NSArray *emails; +@property (nonatomic) NSArray *addresses; + +@property (nonatomic) BOOL isProfileAvatar; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSContact.h b/SignalUtilitiesKit/OWSContact.h new file mode 100644 index 000000000..a5c624d15 --- /dev/null +++ b/SignalUtilitiesKit/OWSContact.h @@ -0,0 +1,183 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import +#import "ContactsManagerProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@class CNContact; +@class OWSAttachmentInfo; +@class SSKProtoDataMessage; +@class SSKProtoDataMessageContact; +@class TSAttachment; +@class TSAttachmentStream; +@class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; + +extern BOOL kIsSendingContactSharesEnabled; + +typedef NS_ENUM(NSUInteger, OWSContactPhoneType) { + OWSContactPhoneType_Home = 1, + OWSContactPhoneType_Mobile, + OWSContactPhoneType_Work, + OWSContactPhoneType_Custom, +}; + +NSString *NSStringForContactPhoneType(OWSContactPhoneType value); + +@protocol OWSContactField + +- (BOOL)ows_isValid; + +- (NSString *)localizedLabel; + +@end + +#pragma mark - + +@interface OWSContactPhoneNumber : MTLModel + +@property (nonatomic, readonly) OWSContactPhoneType phoneType; +// Applies in the OWSContactPhoneType_Custom case. +@property (nonatomic, readonly, nullable) NSString *label; + +@property (nonatomic, readonly) NSString *phoneNumber; + +- (nullable NSString *)tryToConvertToE164; + +@end + +#pragma mark - + +typedef NS_ENUM(NSUInteger, OWSContactEmailType) { + OWSContactEmailType_Home = 1, + OWSContactEmailType_Mobile, + OWSContactEmailType_Work, + OWSContactEmailType_Custom, +}; + +NSString *NSStringForContactEmailType(OWSContactEmailType value); + +@interface OWSContactEmail : MTLModel + +@property (nonatomic, readonly) OWSContactEmailType emailType; +// Applies in the OWSContactEmailType_Custom case. +@property (nonatomic, readonly, nullable) NSString *label; + +@property (nonatomic, readonly) NSString *email; + +@end + +#pragma mark - + +typedef NS_ENUM(NSUInteger, OWSContactAddressType) { + OWSContactAddressType_Home = 1, + OWSContactAddressType_Work, + OWSContactAddressType_Custom, +}; + +NSString *NSStringForContactAddressType(OWSContactAddressType value); + +@interface OWSContactAddress : MTLModel + +@property (nonatomic, readonly) OWSContactAddressType addressType; +// Applies in the OWSContactAddressType_Custom case. +@property (nonatomic, readonly, nullable) NSString *label; + +@property (nonatomic, readonly, nullable) NSString *street; +@property (nonatomic, readonly, nullable) NSString *pobox; +@property (nonatomic, readonly, nullable) NSString *neighborhood; +@property (nonatomic, readonly, nullable) NSString *city; +@property (nonatomic, readonly, nullable) NSString *region; +@property (nonatomic, readonly, nullable) NSString *postcode; +@property (nonatomic, readonly, nullable) NSString *country; + +@end + +#pragma mark - + +@interface OWSContactName : MTLModel + +// The "name parts". +@property (nonatomic, nullable) NSString *givenName; +@property (nonatomic, nullable) NSString *familyName; +@property (nonatomic, nullable) NSString *nameSuffix; +@property (nonatomic, nullable) NSString *namePrefix; +@property (nonatomic, nullable) NSString *middleName; + +@property (nonatomic, nullable) NSString *organizationName; + +@property (nonatomic) NSString *displayName; + +// Returns true if any of the name parts (which doesn't include +// organization name) is non-empty. +- (BOOL)hasAnyNamePart; + +@end + +#pragma mark - + +@interface OWSContact : MTLModel + +@property (nonatomic) OWSContactName *name; + +@property (nonatomic, readonly) NSArray *phoneNumbers; +@property (nonatomic, readonly) NSArray *emails; +@property (nonatomic, readonly) NSArray *addresses; + +@property (nonatomic, readonly, nullable) NSString *avatarAttachmentId; +- (nullable TSAttachment *)avatarAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (void)removeAvatarAttachmentWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)saveAvatarImage:(UIImage *)image transaction:(YapDatabaseReadWriteTransaction *)transaction; +// "Profile" avatars should _not_ be saved to device contacts. +@property (nonatomic, readonly) BOOL isProfileAvatar; + +- (instancetype)init NS_UNAVAILABLE; + +- (void)normalize; + +- (BOOL)ows_isValid; + +- (NSString *)debugDescription; + +#pragma mark - Creation and Derivation + +- (OWSContact *)newContactWithName:(OWSContactName *)name; + +- (OWSContact *)copyContactWithName:(OWSContactName *)name; + +#pragma mark - Phone Numbers and Recipient IDs + +- (NSArray *)systemContactsWithSignalAccountPhoneNumbers:(id)contactsManager + NS_SWIFT_NAME(systemContactsWithSignalAccountPhoneNumbers(_:)); +- (NSArray *)systemContactPhoneNumbers:(id)contactsManager + NS_SWIFT_NAME(systemContactPhoneNumbers(_:)); +- (NSArray *)e164PhoneNumbers NS_SWIFT_NAME(e164PhoneNumbers()); + +@end + +#pragma mark - + +// TODO: Move to separate source file, rename to OWSContactConversion. +@interface OWSContacts : NSObject + +#pragma mark - System Contact Conversion + +// `contactForSystemContact` does *not* handle avatars. That must be delt with by the caller ++ (nullable OWSContact *)contactForSystemContact:(CNContact *)systemContact; + ++ (nullable CNContact *)systemContactForContact:(OWSContact *)contact imageData:(nullable NSData *)imageData; + +#pragma mark - Proto Serialization + ++ (nullable SSKProtoDataMessageContact *)protoForContact:(OWSContact *)contact; + ++ (nullable OWSContact *)contactForDataMessage:(SSKProtoDataMessage *)dataMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSContact.m b/SignalUtilitiesKit/OWSContact.m new file mode 100644 index 000000000..9c5f9479a --- /dev/null +++ b/SignalUtilitiesKit/OWSContact.m @@ -0,0 +1,1126 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSContact.h" +#import "Contact.h" +#import "MimeTypeUtil.h" +#import "NSString+SSK.h" +#import "OWSContact+Private.h" +#import "PhoneNumber.h" +#import "TSAttachment.h" +#import "TSAttachmentPointer.h" +#import "TSAttachmentStream.h" +#import +#import + +@import Contacts; + +NS_ASSUME_NONNULL_BEGIN + +// NOTE: When changing the value of this feature flag, you also need +// to update the filtering in the SAE's info.plist. +BOOL kIsSendingContactSharesEnabled = YES; + +NSString *NSStringForContactPhoneType(OWSContactPhoneType value) +{ + switch (value) { + case OWSContactPhoneType_Home: + return @"Home"; + case OWSContactPhoneType_Mobile: + return @"Mobile"; + case OWSContactPhoneType_Work: + return @"Work"; + case OWSContactPhoneType_Custom: + return @"Custom"; + } +} + +@interface OWSContactPhoneNumber () + +@property (nonatomic) OWSContactPhoneType phoneType; +@property (nonatomic, nullable) NSString *label; + +@property (nonatomic) NSString *phoneNumber; + +@end + +#pragma mark - + +@implementation OWSContactPhoneNumber + +- (BOOL)ows_isValid +{ + if (self.phoneNumber.ows_stripped.length < 1) { + OWSLogWarn(@"invalid phone number: %@.", self.phoneNumber); + return NO; + } + return YES; +} + +- (NSString *)localizedLabel +{ + switch (self.phoneType) { + case OWSContactPhoneType_Home: + return [CNLabeledValue localizedStringForLabel:CNLabelHome]; + case OWSContactPhoneType_Mobile: + return [CNLabeledValue localizedStringForLabel:CNLabelPhoneNumberMobile]; + case OWSContactPhoneType_Work: + return [CNLabeledValue localizedStringForLabel:CNLabelWork]; + default: + if (self.label.ows_stripped.length < 1) { + return NSLocalizedString(@"CONTACT_PHONE", @"Label for a contact's phone number."); + } + return self.label.ows_stripped; + } +} + +- (NSString *)debugDescription +{ + NSMutableString *result = [NSMutableString new]; + [result appendFormat:@"[Phone Number: %@, ", NSStringForContactPhoneType(self.phoneType)]; + + if (self.label.length > 0) { + [result appendFormat:@"label: %@, ", self.label]; + } + if (self.phoneNumber.length > 0) { + [result appendFormat:@"phoneNumber: %@, ", self.phoneNumber]; + } + + [result appendString:@"]"]; + return result; +} + +- (nullable NSString *)tryToConvertToE164 +{ + PhoneNumber *_Nullable parsedPhoneNumber; + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromE164:self.phoneNumber]; + if (!parsedPhoneNumber) { + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:self.phoneNumber]; + } + if (parsedPhoneNumber) { + return parsedPhoneNumber.toE164; + } + return nil; +} + +@end + +#pragma mark - + +NSString *NSStringForContactEmailType(OWSContactEmailType value) +{ + switch (value) { + case OWSContactEmailType_Home: + return @"Home"; + case OWSContactEmailType_Mobile: + return @"Mobile"; + case OWSContactEmailType_Work: + return @"Work"; + case OWSContactEmailType_Custom: + return @"Custom"; + } +} + +@interface OWSContactEmail () + +@property (nonatomic) OWSContactEmailType emailType; +@property (nonatomic, nullable) NSString *label; + +@property (nonatomic) NSString *email; + +@end + +#pragma mark - + +@implementation OWSContactEmail + +- (BOOL)ows_isValid +{ + if (self.email.ows_stripped.length < 1) { + OWSLogWarn(@"invalid email: %@.", self.email); + return NO; + } + return YES; +} + +- (NSString *)localizedLabel +{ + switch (self.emailType) { + case OWSContactEmailType_Home: + return [CNLabeledValue localizedStringForLabel:CNLabelHome]; + case OWSContactEmailType_Mobile: + return [CNLabeledValue localizedStringForLabel:CNLabelPhoneNumberMobile]; + case OWSContactEmailType_Work: + return [CNLabeledValue localizedStringForLabel:CNLabelWork]; + default: + if (self.label.ows_stripped.length < 1) { + return NSLocalizedString(@"CONTACT_EMAIL", @"Label for a contact's email address."); + } + return self.label.ows_stripped; + } +} + +- (NSString *)debugDescription +{ + NSMutableString *result = [NSMutableString new]; + [result appendFormat:@"[Email: %@, ", NSStringForContactEmailType(self.emailType)]; + + if (self.label.length > 0) { + [result appendFormat:@"label: %@, ", self.label]; + } + if (self.email.length > 0) { + [result appendFormat:@"email: %@, ", self.email]; + } + + [result appendString:@"]"]; + return result; +} + +@end + +#pragma mark - + +NSString *NSStringForContactAddressType(OWSContactAddressType value) +{ + switch (value) { + case OWSContactAddressType_Home: + return @"Home"; + case OWSContactAddressType_Work: + return @"Work"; + case OWSContactAddressType_Custom: + return @"Custom"; + } +} +@interface OWSContactAddress () + +@property (nonatomic) OWSContactAddressType addressType; +@property (nonatomic, nullable) NSString *label; + +@property (nonatomic, nullable) NSString *street; +@property (nonatomic, nullable) NSString *pobox; +@property (nonatomic, nullable) NSString *neighborhood; +@property (nonatomic, nullable) NSString *city; +@property (nonatomic, nullable) NSString *region; +@property (nonatomic, nullable) NSString *postcode; +@property (nonatomic, nullable) NSString *country; + +@end + +#pragma mark - + +@implementation OWSContactAddress + +- (BOOL)ows_isValid +{ + if (self.street.ows_stripped.length < 1 && self.pobox.ows_stripped.length < 1 + && self.neighborhood.ows_stripped.length < 1 && self.city.ows_stripped.length < 1 + && self.region.ows_stripped.length < 1 && self.postcode.ows_stripped.length < 1 + && self.country.ows_stripped.length < 1) { + OWSLogWarn(@"invalid address; empty."); + return NO; + } + return YES; +} + +- (NSString *)localizedLabel +{ + switch (self.addressType) { + case OWSContactAddressType_Home: + return [CNLabeledValue localizedStringForLabel:CNLabelHome]; + case OWSContactAddressType_Work: + return [CNLabeledValue localizedStringForLabel:CNLabelWork]; + default: + if (self.label.ows_stripped.length < 1) { + return NSLocalizedString(@"CONTACT_ADDRESS", @"Label for a contact's postal address."); + } + return self.label.ows_stripped; + } +} + +- (NSString *)debugDescription +{ + NSMutableString *result = [NSMutableString new]; + [result appendFormat:@"[Address: %@, ", NSStringForContactAddressType(self.addressType)]; + + if (self.label.length > 0) { + [result appendFormat:@"label: %@, ", self.label]; + } + if (self.street.length > 0) { + [result appendFormat:@"street: %@, ", self.street]; + } + if (self.pobox.length > 0) { + [result appendFormat:@"pobox: %@, ", self.pobox]; + } + if (self.neighborhood.length > 0) { + [result appendFormat:@"neighborhood: %@, ", self.neighborhood]; + } + if (self.city.length > 0) { + [result appendFormat:@"city: %@, ", self.city]; + } + if (self.region.length > 0) { + [result appendFormat:@"region: %@, ", self.region]; + } + if (self.postcode.length > 0) { + [result appendFormat:@"postcode: %@, ", self.postcode]; + } + if (self.country.length > 0) { + [result appendFormat:@"country: %@, ", self.country]; + } + + [result appendString:@"]"]; + return result; +} + +@end + +#pragma mark - + +@implementation OWSContactName + +- (NSString *)logDescription +{ + NSMutableString *result = [NSMutableString new]; + [result appendString:@"["]; + + if (self.givenName.length > 0) { + [result appendFormat:@"givenName: %@, ", self.givenName]; + } + if (self.familyName.length > 0) { + [result appendFormat:@"familyName: %@, ", self.familyName]; + } + if (self.middleName.length > 0) { + [result appendFormat:@"middleName: %@, ", self.middleName]; + } + if (self.namePrefix.length > 0) { + [result appendFormat:@"namePrefix: %@, ", self.namePrefix]; + } + if (self.nameSuffix.length > 0) { + [result appendFormat:@"nameSuffix: %@, ", self.nameSuffix]; + } + if (self.displayName.length > 0) { + [result appendFormat:@"displayName: %@, ", self.displayName]; + } + + [result appendString:@"]"]; + return result; +} + +- (NSString *)displayName +{ + [self ensureDisplayName]; + + if (_displayName.length < 1) { + OWSFailDebug(@"could not derive a valid display name."); + return NSLocalizedString(@"CONTACT_WITHOUT_NAME", @"Indicates that a contact has no name."); + } + return _displayName; +} + +- (void)ensureDisplayName +{ + if (_displayName.length < 1) { + CNContact *_Nullable cnContact = [self systemContactForName]; + _displayName = [CNContactFormatter stringFromContact:cnContact style:CNContactFormatterStyleFullName]; + } + if (_displayName.length < 1) { + // Fall back to using the organization name. + _displayName = self.organizationName; + } +} + +- (void)updateDisplayName +{ + _displayName = nil; + + [self ensureDisplayName]; +} + +- (nullable CNContact *)systemContactForName +{ + CNMutableContact *systemContact = [CNMutableContact new]; + systemContact.givenName = self.givenName.ows_stripped; + systemContact.middleName = self.middleName.ows_stripped; + systemContact.familyName = self.familyName.ows_stripped; + systemContact.namePrefix = self.namePrefix.ows_stripped; + systemContact.nameSuffix = self.nameSuffix.ows_stripped; + // We don't need to set display name, it's implicit for system contacts. + systemContact.organizationName = self.organizationName.ows_stripped; + return systemContact; +} + +- (BOOL)hasAnyNamePart +{ + return (self.givenName.ows_stripped.length > 0 || self.middleName.ows_stripped.length > 0 + || self.familyName.ows_stripped.length > 0 || self.namePrefix.ows_stripped.length > 0 + || self.nameSuffix.ows_stripped.length > 0); +} + +@end + +#pragma mark - + +@interface OWSContact () + +@property (nonatomic) NSArray *phoneNumbers; +@property (nonatomic) NSArray *emails; +@property (nonatomic) NSArray *addresses; + +@property (nonatomic, nullable) NSString *avatarAttachmentId; +@property (nonatomic) BOOL isProfileAvatar; + +@property (nonatomic, nullable) NSArray *e164PhoneNumbersCached; + +@end + +#pragma mark - + +@implementation OWSContact + +- (instancetype)init +{ + if (self = [super init]) { + _name = [OWSContactName new]; + _phoneNumbers = @[]; + _emails = @[]; + _addresses = @[]; + } + + return self; +} + +- (void)normalize +{ + self.phoneNumbers = [self.phoneNumbers + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(OWSContactPhoneNumber *value, + NSDictionary *_Nullable bindings) { + return value.ows_isValid; + }]]; + self.emails = [self.emails filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(OWSContactEmail *value, + NSDictionary *_Nullable bindings) { + return value.ows_isValid; + }]]; + self.addresses = + [self.addresses filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(OWSContactAddress *value, + NSDictionary *_Nullable bindings) { + return value.ows_isValid; + }]]; +} + +- (BOOL)ows_isValid +{ + if (self.name.displayName.ows_stripped.length < 1) { + OWSLogWarn(@"invalid contact; no display name."); + return NO; + } + BOOL hasValue = NO; + for (OWSContactPhoneNumber *phoneNumber in self.phoneNumbers) { + if (!phoneNumber.ows_isValid) { + return NO; + } + hasValue = YES; + } + for (OWSContactEmail *email in self.emails) { + if (!email.ows_isValid) { + return NO; + } + hasValue = YES; + } + for (OWSContactAddress *address in self.addresses) { + if (!address.ows_isValid) { + return NO; + } + hasValue = YES; + } + return hasValue; +} + +- (NSString *)debugDescription +{ + NSMutableString *result = [NSMutableString new]; + [result appendString:@"["]; + + [result appendFormat:@"%@, ", self.name.logDescription]; + + for (OWSContactPhoneNumber *phoneNumber in self.phoneNumbers) { + [result appendFormat:@"%@, ", phoneNumber.debugDescription]; + } + for (OWSContactEmail *email in self.emails) { + [result appendFormat:@"%@, ", email.debugDescription]; + } + for (OWSContactAddress *address in self.addresses) { + [result appendFormat:@"%@, ", address.debugDescription]; + } + + [result appendString:@"]"]; + return result; +} + +- (OWSContact *)newContactWithName:(OWSContactName *)name +{ + OWSAssertDebug(name); + + OWSContact *newContact = [OWSContact new]; + + newContact.name = name; + + [name updateDisplayName]; + + return newContact; +} + +- (OWSContact *)copyContactWithName:(OWSContactName *)name +{ + OWSAssertDebug(name); + + OWSContact *contactCopy = [self copy]; + + contactCopy.name = name; + + [name updateDisplayName]; + + return contactCopy; +} + +#pragma mark - Avatar + +- (nullable TSAttachment *)avatarAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [TSAttachment fetchObjectWithUniqueID:self.avatarAttachmentId transaction:transaction]; +} + +- (void)saveAvatarImage:(UIImage *)image transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + NSData *imageData = UIImageJPEGRepresentation(image, (CGFloat)0.9); + + TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:OWSMimeTypeImageJpeg + byteCount:(UInt32)imageData.length + sourceFilename:nil + caption:nil + albumMessageId:nil]; + + NSError *error; + BOOL success = [attachmentStream writeData:imageData error:&error]; + OWSAssertDebug(success && !error); + + [attachmentStream saveWithTransaction:transaction]; + self.avatarAttachmentId = attachmentStream.uniqueId; +} + +- (void)removeAvatarAttachmentWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + TSAttachment *_Nullable attachment = + [TSAttachment fetchObjectWithUniqueID:self.avatarAttachmentId transaction:transaction]; + [attachment removeWithTransaction:transaction]; +} + +#pragma mark - Phone Numbers and Recipient IDs + +- (NSArray *)systemContactsWithSignalAccountPhoneNumbers:(id)contactsManager +{ + OWSAssertDebug(contactsManager); + + return [self.e164PhoneNumbers + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable recipientId, + NSDictionary *_Nullable bindings) { + return [contactsManager isSystemContactWithSignalAccount:recipientId]; + }]]; +} + +- (NSArray *)systemContactPhoneNumbers:(id)contactsManager +{ + OWSAssertDebug(contactsManager); + + return [self.e164PhoneNumbers + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable recipientId, + NSDictionary *_Nullable bindings) { + return [contactsManager isSystemContact:recipientId]; + }]]; +} + +- (NSArray *)e164PhoneNumbers +{ + if (self.e164PhoneNumbersCached) { + return self.e164PhoneNumbersCached; + } + NSMutableArray *e164PhoneNumbers = [NSMutableArray new]; + for (OWSContactPhoneNumber *phoneNumber in self.phoneNumbers) { + PhoneNumber *_Nullable parsedPhoneNumber; + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromE164:phoneNumber.phoneNumber]; + if (!parsedPhoneNumber) { + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:phoneNumber.phoneNumber]; + } + if (parsedPhoneNumber) { + [e164PhoneNumbers addObject:parsedPhoneNumber.toE164]; + } + } + self.e164PhoneNumbersCached = e164PhoneNumbers; + return e164PhoneNumbers; +} + +@end + +#pragma mark - + +@implementation OWSContacts + +#pragma mark - System Contact Conversion + +// `contactForSystemContact` does *not* handle avatars. That must be delt with by the caller ++ (nullable OWSContact *)contactForSystemContact:(CNContact *)systemContact +{ + if (!systemContact) { + OWSFailDebug(@"Missing contact."); + return nil; + } + + OWSContact *contact = [OWSContact new]; + + OWSContactName *contactName = [OWSContactName new]; + contactName.givenName = systemContact.givenName.ows_stripped; + contactName.middleName = systemContact.middleName.ows_stripped; + contactName.familyName = systemContact.familyName.ows_stripped; + contactName.namePrefix = systemContact.namePrefix.ows_stripped; + contactName.nameSuffix = systemContact.nameSuffix.ows_stripped; + contactName.organizationName = systemContact.organizationName.ows_stripped; + [contactName ensureDisplayName]; + contact.name = contactName; + + NSMutableArray *phoneNumbers = [NSMutableArray new]; + for (CNLabeledValue *phoneNumberField in systemContact.phoneNumbers) { + OWSContactPhoneNumber *phoneNumber = [OWSContactPhoneNumber new]; + + // Make a best effort to parse the phone number to e164. + NSString *unparsedPhoneNumber = phoneNumberField.value.stringValue; + PhoneNumber *_Nullable parsedPhoneNumber; + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromE164:unparsedPhoneNumber]; + if (!parsedPhoneNumber) { + parsedPhoneNumber = [PhoneNumber tryParsePhoneNumberFromUserSpecifiedText:unparsedPhoneNumber]; + } + if (parsedPhoneNumber) { + phoneNumber.phoneNumber = parsedPhoneNumber.toE164; + } else { + phoneNumber.phoneNumber = unparsedPhoneNumber; + } + + if ([phoneNumberField.label isEqualToString:CNLabelHome]) { + phoneNumber.phoneType = OWSContactPhoneType_Home; + } else if ([phoneNumberField.label isEqualToString:CNLabelWork]) { + phoneNumber.phoneType = OWSContactPhoneType_Work; + } else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberMobile]) { + phoneNumber.phoneType = OWSContactPhoneType_Mobile; + } else { + phoneNumber.phoneType = OWSContactPhoneType_Custom; + phoneNumber.label = [Contact localizedStringForCNLabel:phoneNumberField.label]; + } + [phoneNumbers addObject:phoneNumber]; + } + contact.phoneNumbers = phoneNumbers; + + NSMutableArray *emails = [NSMutableArray new]; + for (CNLabeledValue *emailField in systemContact.emailAddresses) { + OWSContactEmail *email = [OWSContactEmail new]; + email.email = emailField.value; + if ([emailField.label isEqualToString:CNLabelHome]) { + email.emailType = OWSContactEmailType_Home; + } else if ([emailField.label isEqualToString:CNLabelWork]) { + email.emailType = OWSContactEmailType_Work; + } else { + email.emailType = OWSContactEmailType_Custom; + email.label = [Contact localizedStringForCNLabel:emailField.label]; + } + [emails addObject:email]; + } + contact.emails = emails; + + NSMutableArray *addresses = [NSMutableArray new]; + for (CNLabeledValue *addressField in systemContact.postalAddresses) { + OWSContactAddress *address = [OWSContactAddress new]; + address.street = addressField.value.street; + // TODO: Is this the correct mapping? + // address.neighborhood = addressField.value.subLocality; + address.city = addressField.value.city; + // TODO: Is this the correct mapping? + // address.region = addressField.value.subAdministrativeArea; + address.region = addressField.value.state; + address.postcode = addressField.value.postalCode; + // TODO: Should we be using 2-letter codes, 3-letter codes or names? + address.country = addressField.value.ISOCountryCode; + + if ([addressField.label isEqualToString:CNLabelHome]) { + address.addressType = OWSContactAddressType_Home; + } else if ([addressField.label isEqualToString:CNLabelWork]) { + address.addressType = OWSContactAddressType_Work; + } else { + address.addressType = OWSContactAddressType_Custom; + address.label = [Contact localizedStringForCNLabel:addressField.label]; + } + [addresses addObject:address]; + } + contact.addresses = addresses; + + return contact; +} + ++ (nullable CNContact *)systemContactForContact:(OWSContact *)contact imageData:(nullable NSData *)imageData +{ + if (!contact) { + OWSFailDebug(@"Missing contact."); + return nil; + } + + CNMutableContact *systemContact = [CNMutableContact new]; + + systemContact.givenName = contact.name.givenName; + systemContact.middleName = contact.name.middleName; + systemContact.familyName = contact.name.familyName; + systemContact.namePrefix = contact.name.namePrefix; + systemContact.nameSuffix = contact.name.nameSuffix; + // We don't need to set display name, it's implicit for system contacts. + systemContact.organizationName = contact.name.organizationName; + + NSMutableArray *> *systemPhoneNumbers = [NSMutableArray new]; + for (OWSContactPhoneNumber *phoneNumber in contact.phoneNumbers) { + switch (phoneNumber.phoneType) { + case OWSContactPhoneType_Home: + [systemPhoneNumbers + addObject:[CNLabeledValue + labeledValueWithLabel:CNLabelHome + value:[CNPhoneNumber + phoneNumberWithStringValue:phoneNumber.phoneNumber]]]; + break; + case OWSContactPhoneType_Mobile: + [systemPhoneNumbers + addObject:[CNLabeledValue + labeledValueWithLabel:CNLabelPhoneNumberMobile + value:[CNPhoneNumber + phoneNumberWithStringValue:phoneNumber.phoneNumber]]]; + break; + case OWSContactPhoneType_Work: + [systemPhoneNumbers + addObject:[CNLabeledValue + labeledValueWithLabel:CNLabelWork + value:[CNPhoneNumber + phoneNumberWithStringValue:phoneNumber.phoneNumber]]]; + break; + case OWSContactPhoneType_Custom: + [systemPhoneNumbers + addObject:[CNLabeledValue + labeledValueWithLabel:phoneNumber.label + value:[CNPhoneNumber + phoneNumberWithStringValue:phoneNumber.phoneNumber]]]; + break; + } + } + systemContact.phoneNumbers = systemPhoneNumbers; + + NSMutableArray *> *systemEmails = [NSMutableArray new]; + for (OWSContactEmail *email in contact.emails) { + switch (email.emailType) { + case OWSContactEmailType_Home: + [systemEmails addObject:[CNLabeledValue labeledValueWithLabel:CNLabelHome value:email.email]]; + break; + case OWSContactEmailType_Mobile: + [systemEmails addObject:[CNLabeledValue labeledValueWithLabel:@"Mobile" value:email.email]]; + break; + case OWSContactEmailType_Work: + [systemEmails addObject:[CNLabeledValue labeledValueWithLabel:CNLabelWork value:email.email]]; + break; + case OWSContactEmailType_Custom: + [systemEmails addObject:[CNLabeledValue labeledValueWithLabel:email.label value:email.email]]; + break; + } + } + systemContact.emailAddresses = systemEmails; + + NSMutableArray *> *systemAddresses = [NSMutableArray new]; + for (OWSContactAddress *address in contact.addresses) { + CNMutablePostalAddress *systemAddress = [CNMutablePostalAddress new]; + systemAddress.street = address.street; + // TODO: Is this the correct mapping? + // systemAddress.subLocality = address.neighborhood; + systemAddress.city = address.city; + // TODO: Is this the correct mapping? + // systemAddress.subAdministrativeArea = address.region; + systemAddress.state = address.region; + systemAddress.postalCode = address.postcode; + // TODO: Should we be using 2-letter codes, 3-letter codes or names? + systemAddress.ISOCountryCode = address.country; + + switch (address.addressType) { + case OWSContactAddressType_Home: + [systemAddresses addObject:[CNLabeledValue labeledValueWithLabel:CNLabelHome value:systemAddress]]; + break; + case OWSContactAddressType_Work: + [systemAddresses addObject:[CNLabeledValue labeledValueWithLabel:CNLabelWork value:systemAddress]]; + break; + case OWSContactAddressType_Custom: + [systemAddresses addObject:[CNLabeledValue labeledValueWithLabel:address.label value:systemAddress]]; + break; + } + } + systemContact.postalAddresses = systemAddresses; + systemContact.imageData = imageData; + + return systemContact; +} + +#pragma mark - Proto Serialization + ++ (nullable SSKProtoDataMessageContact *)protoForContact:(OWSContact *)contact +{ + OWSAssertDebug(contact); + + SSKProtoDataMessageContactBuilder *contactBuilder = [SSKProtoDataMessageContact builder]; + + SSKProtoDataMessageContactNameBuilder *nameBuilder = [SSKProtoDataMessageContactName builder]; + + OWSContactName *contactName = contact.name; + if (contactName.givenName.ows_stripped.length > 0) { + nameBuilder.givenName = contactName.givenName.ows_stripped; + } + if (contactName.familyName.ows_stripped.length > 0) { + nameBuilder.familyName = contactName.familyName.ows_stripped; + } + if (contactName.middleName.ows_stripped.length > 0) { + nameBuilder.middleName = contactName.middleName.ows_stripped; + } + if (contactName.namePrefix.ows_stripped.length > 0) { + nameBuilder.prefix = contactName.namePrefix.ows_stripped; + } + if (contactName.nameSuffix.ows_stripped.length > 0) { + nameBuilder.suffix = contactName.nameSuffix.ows_stripped; + } + if (contactName.organizationName.ows_stripped.length > 0) { + contactBuilder.organization = contactName.organizationName.ows_stripped; + } + nameBuilder.displayName = contactName.displayName; + + NSError *error; + SSKProtoDataMessageContactName *_Nullable nameProto = [nameBuilder buildAndReturnError:&error]; + if (error || !nameProto) { + OWSLogError(@"could not build protobuf: %@", error); + } else { + [contactBuilder setName:nameProto]; + } + + for (OWSContactPhoneNumber *phoneNumber in contact.phoneNumbers) { + SSKProtoDataMessageContactPhoneBuilder *phoneBuilder = [SSKProtoDataMessageContactPhone builder]; + phoneBuilder.value = phoneNumber.phoneNumber; + if (phoneNumber.label.ows_stripped.length > 0) { + phoneBuilder.label = phoneNumber.label.ows_stripped; + } + switch (phoneNumber.phoneType) { + case OWSContactPhoneType_Home: + phoneBuilder.type = SSKProtoDataMessageContactPhoneTypeHome; + break; + case OWSContactPhoneType_Mobile: + phoneBuilder.type = SSKProtoDataMessageContactPhoneTypeMobile; + break; + case OWSContactPhoneType_Work: + phoneBuilder.type = SSKProtoDataMessageContactPhoneTypeWork; + break; + case OWSContactPhoneType_Custom: + phoneBuilder.type = SSKProtoDataMessageContactPhoneTypeCustom; + break; + } + SSKProtoDataMessageContactPhone *_Nullable numberProto = [phoneBuilder buildAndReturnError:&error]; + if (error || !numberProto) { + OWSLogError(@"could not build protobuf: %@", error); + } else { + [contactBuilder addNumber:numberProto]; + } + } + + for (OWSContactEmail *email in contact.emails) { + SSKProtoDataMessageContactEmailBuilder *emailBuilder = [SSKProtoDataMessageContactEmail builder]; + emailBuilder.value = email.email; + if (email.label.ows_stripped.length > 0) { + emailBuilder.label = email.label.ows_stripped; + } + switch (email.emailType) { + case OWSContactEmailType_Home: + emailBuilder.type = SSKProtoDataMessageContactEmailTypeHome; + break; + case OWSContactEmailType_Mobile: + emailBuilder.type = SSKProtoDataMessageContactEmailTypeMobile; + break; + case OWSContactEmailType_Work: + emailBuilder.type = SSKProtoDataMessageContactEmailTypeWork; + break; + case OWSContactEmailType_Custom: + emailBuilder.type = SSKProtoDataMessageContactEmailTypeCustom; + break; + } + SSKProtoDataMessageContactEmail *_Nullable emailProto = [emailBuilder buildAndReturnError:&error]; + if (error || !emailProto) { + OWSLogError(@"could not build protobuf: %@", error); + } else { + [contactBuilder addEmail:emailProto]; + } + } + + for (OWSContactAddress *address in contact.addresses) { + SSKProtoDataMessageContactPostalAddressBuilder *addressBuilder = + [SSKProtoDataMessageContactPostalAddress builder]; + if (address.label.ows_stripped.length > 0) { + addressBuilder.label = address.label.ows_stripped; + } + if (address.street.ows_stripped.length > 0) { + addressBuilder.street = address.street.ows_stripped; + } + if (address.pobox.ows_stripped.length > 0) { + addressBuilder.pobox = address.pobox.ows_stripped; + } + if (address.neighborhood.ows_stripped.length > 0) { + addressBuilder.neighborhood = address.neighborhood.ows_stripped; + } + if (address.city.ows_stripped.length > 0) { + addressBuilder.city = address.city.ows_stripped; + } + if (address.region.ows_stripped.length > 0) { + addressBuilder.region = address.region.ows_stripped; + } + if (address.postcode.ows_stripped.length > 0) { + addressBuilder.postcode = address.postcode.ows_stripped; + } + if (address.country.ows_stripped.length > 0) { + addressBuilder.country = address.country.ows_stripped; + } + SSKProtoDataMessageContactPostalAddress *_Nullable addressProto = [addressBuilder buildAndReturnError:&error]; + if (error || !addressProto) { + OWSLogError(@"could not build protobuf: %@", error); + } else { + [contactBuilder addAddress:addressProto]; + } + } + + if (contact.avatarAttachmentId) { + SSKProtoAttachmentPointer *_Nullable attachmentProto = + [TSAttachmentStream buildProtoForAttachmentId:contact.avatarAttachmentId]; + if (!attachmentProto) { + OWSLogError(@"could not build protobuf: %@", error); + } else { + SSKProtoDataMessageContactAvatarBuilder *avatarBuilder = [SSKProtoDataMessageContactAvatar builder]; + avatarBuilder.avatar = attachmentProto; + SSKProtoDataMessageContactAvatar *_Nullable avatarProto = [avatarBuilder buildAndReturnError:&error]; + if (error || !avatarProto) { + OWSLogError(@"could not build protobuf: %@", error); + } else { + contactBuilder.avatar = avatarProto; + } + } + } + + SSKProtoDataMessageContact *_Nullable contactProto = [contactBuilder buildAndReturnError:&error]; + if (error || !contactProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + if (contactProto.number.count < 1 && contactProto.email.count < 1 && contactProto.address.count < 1) { + OWSFailDebug(@"contact has neither phone, email or address."); + return nil; + } + return contactProto; +} + ++ (nullable OWSContact *)contactForDataMessage:(SSKProtoDataMessage *)dataMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(dataMessage); + + if (dataMessage.contact.count < 1) { + return nil; + } + OWSAssertDebug(dataMessage.contact.count == 1); + SSKProtoDataMessageContact *contactProto = dataMessage.contact.firstObject; + + OWSContact *contact = [OWSContact new]; + + OWSContactName *contactName = [OWSContactName new]; + if (contactProto.name) { + SSKProtoDataMessageContactName *nameProto = contactProto.name; + + if (nameProto.givenName) { + contactName.givenName = nameProto.givenName.ows_stripped; + } + if (nameProto.familyName) { + contactName.familyName = nameProto.familyName.ows_stripped; + } + if (nameProto.prefix) { + contactName.namePrefix = nameProto.prefix.ows_stripped; + } + if (nameProto.suffix) { + contactName.nameSuffix = nameProto.suffix.ows_stripped; + } + if (nameProto.middleName) { + contactName.middleName = nameProto.middleName.ows_stripped; + } + if (nameProto.displayName) { + contactName.displayName = nameProto.displayName.ows_stripped; + } + } + if (contactProto.organization) { + contactName.organizationName = contactProto.organization.ows_stripped; + } + [contactName ensureDisplayName]; + contact.name = contactName; + + NSMutableArray *phoneNumbers = [NSMutableArray new]; + for (SSKProtoDataMessageContactPhone *phoneNumberProto in contactProto.number) { + OWSContactPhoneNumber *_Nullable phoneNumber = [self phoneNumberForProto:phoneNumberProto]; + if (phoneNumber) { + [phoneNumbers addObject:phoneNumber]; + } + } + contact.phoneNumbers = [phoneNumbers copy]; + + NSMutableArray *emails = [NSMutableArray new]; + for (SSKProtoDataMessageContactEmail *emailProto in contactProto.email) { + OWSContactEmail *_Nullable email = [self emailForProto:emailProto]; + if (email) { + [emails addObject:email]; + } + } + contact.emails = [emails copy]; + + NSMutableArray *addresses = [NSMutableArray new]; + for (SSKProtoDataMessageContactPostalAddress *addressProto in contactProto.address) { + OWSContactAddress *_Nullable address = [self addressForProto:addressProto]; + if (address) { + [addresses addObject:address]; + } + } + contact.addresses = [addresses copy]; + + if (contactProto.avatar) { + SSKProtoDataMessageContactAvatar *avatarInfo = contactProto.avatar; + + if (avatarInfo.avatar) { + SSKProtoAttachmentPointer *avatarAttachment = avatarInfo.avatar; + + TSAttachmentPointer *_Nullable attachmentPointer = + [TSAttachmentPointer attachmentPointerFromProto:avatarAttachment albumMessage:nil]; + if (attachmentPointer) { + [attachmentPointer saveWithTransaction:transaction]; + contact.avatarAttachmentId = attachmentPointer.uniqueId; + contact.isProfileAvatar = avatarInfo.isProfile; + } else { + OWSFailDebug(@"Invalid avatar attachment."); + } + } else { + OWSFailDebug(@"avatarInfo.hasAvatar was unexpectedly false"); + } + } + + + return contact; +} + ++ (nullable OWSContactPhoneNumber *)phoneNumberForProto: + (SSKProtoDataMessageContactPhone *)phoneNumberProto +{ + OWSContactPhoneNumber *result = [OWSContactPhoneNumber new]; + result.phoneType = OWSContactPhoneType_Custom; + if (phoneNumberProto.hasType) { + switch (phoneNumberProto.type) { + case SSKProtoDataMessageContactPhoneTypeHome: + result.phoneType = OWSContactPhoneType_Home; + break; + case SSKProtoDataMessageContactPhoneTypeMobile: + result.phoneType = OWSContactPhoneType_Mobile; + break; + case SSKProtoDataMessageContactPhoneTypeWork: + result.phoneType = OWSContactPhoneType_Work; + break; + default: + break; + } + } + if (phoneNumberProto.hasLabel) { + result.label = phoneNumberProto.label.ows_stripped; + } + if (phoneNumberProto.hasValue) { + result.phoneNumber = phoneNumberProto.value.ows_stripped; + } else { + return nil; + } + return result; +} + ++ (nullable OWSContactEmail *)emailForProto:(SSKProtoDataMessageContactEmail *)emailProto +{ + OWSContactEmail *result = [OWSContactEmail new]; + result.emailType = OWSContactEmailType_Custom; + if (emailProto.hasType) { + switch (emailProto.type) { + case SSKProtoDataMessageContactEmailTypeHome: + result.emailType = OWSContactEmailType_Home; + break; + case SSKProtoDataMessageContactEmailTypeMobile: + result.emailType = OWSContactEmailType_Mobile; + break; + case SSKProtoDataMessageContactEmailTypeWork: + result.emailType = OWSContactEmailType_Work; + break; + default: + break; + } + } + if (emailProto.hasLabel) { + result.label = emailProto.label.ows_stripped; + } + if (emailProto.hasValue) { + result.email = emailProto.value.ows_stripped; + } else { + return nil; + } + return result; +} + ++ (nullable OWSContactAddress *)addressForProto:(SSKProtoDataMessageContactPostalAddress *)addressProto +{ + OWSContactAddress *result = [OWSContactAddress new]; + result.addressType = OWSContactAddressType_Custom; + if (addressProto.hasType) { + switch (addressProto.type) { + case SSKProtoDataMessageContactPostalAddressTypeHome: + result.addressType = OWSContactAddressType_Home; + break; + case SSKProtoDataMessageContactPostalAddressTypeWork: + result.addressType = OWSContactAddressType_Work; + break; + default: + break; + } + } + if (addressProto.hasLabel) { + result.label = addressProto.label.ows_stripped; + } + if (addressProto.hasStreet) { + result.street = addressProto.street.ows_stripped; + } + if (addressProto.hasPobox) { + result.pobox = addressProto.pobox.ows_stripped; + } + if (addressProto.hasNeighborhood) { + result.neighborhood = addressProto.neighborhood.ows_stripped; + } + if (addressProto.hasCity) { + result.city = addressProto.city.ows_stripped; + } + if (addressProto.hasRegion) { + result.region = addressProto.region.ows_stripped; + } + if (addressProto.hasPostcode) { + result.postcode = addressProto.postcode.ows_stripped; + } + if (addressProto.hasCountry) { + result.country = addressProto.country.ows_stripped; + } + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSContactDiscoveryOperation.swift b/SignalUtilitiesKit/OWSContactDiscoveryOperation.swift new file mode 100644 index 000000000..cbefc9e8e --- /dev/null +++ b/SignalUtilitiesKit/OWSContactDiscoveryOperation.swift @@ -0,0 +1,535 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc(OWSLegacyContactDiscoveryOperation) +public class LegacyContactDiscoveryBatchOperation: OWSOperation { + + @objc + public var registeredRecipientIds: Set + + private let recipientIdsToLookup: [String] + private var networkManager: TSNetworkManager { + return TSNetworkManager.shared() + } + + // MARK: Initializers + + @objc + public required init(recipientIdsToLookup: [String]) { + self.recipientIdsToLookup = recipientIdsToLookup + self.registeredRecipientIds = Set() + + super.init() + + Logger.debug("with recipientIdsToLookup: \(recipientIdsToLookup.count)") + } + + // MARK: OWSOperation Overrides + + // Called every retry, this is where the bulk of the operation's work should go. + override public func run() { + Logger.debug("") + + guard !isCancelled else { + Logger.info("no work to do, since we were canceled") + self.reportCancelled() + return + } + + var phoneNumbersByHashes: [String: String] = [:] + + for recipientId in recipientIdsToLookup { + guard let hash = Cryptography.truncatedSHA1Base64EncodedWithoutPadding(recipientId) else { + owsFailDebug("could not hash recipient id: \(recipientId)") + continue + } + assert(phoneNumbersByHashes[hash] == nil) + phoneNumbersByHashes[hash] = recipientId + } + + let hashes: [String] = Array(phoneNumbersByHashes.keys) + + let request = OWSRequestFactory.contactsIntersectionRequest(withHashesArray: hashes) + + self.networkManager.makeRequest(request, + success: { (task, responseDict) in + do { + self.registeredRecipientIds = try self.parse(response: responseDict, phoneNumbersByHashes: phoneNumbersByHashes) + self.reportSuccess() + } catch { + self.reportError(error) + } + }, + failure: { (task, error) in + guard let response = task.response as? HTTPURLResponse else { + let responseError: NSError = OWSErrorMakeUnableToProcessServerResponseError() as NSError + responseError.isRetryable = true + self.reportError(responseError) + return + } + + guard response.statusCode != 413 else { + let rateLimitError = OWSErrorWithCodeDescription(OWSErrorCode.contactsUpdaterRateLimit, "Contacts Intersection Rate Limit") + self.reportError(rateLimitError) + return + } + + self.reportError(error) + }) + } + + // Called at most one time. + override public func didSucceed() { + // Compare against new CDS service + let modernCDSOperation = CDSOperation(recipientIdsToLookup: self.recipientIdsToLookup) + let cdsFeedbackOperation = CDSFeedbackOperation(legacyRegisteredRecipientIds: self.registeredRecipientIds) + cdsFeedbackOperation.addDependency(modernCDSOperation) + + let operations = modernCDSOperation.dependencies + [modernCDSOperation, cdsFeedbackOperation] + CDSOperation.operationQueue.addOperations(operations, waitUntilFinished: false) + } + + // MARK: Private Helpers + + private func parse(response: Any?, phoneNumbersByHashes: [String: String]) throws -> Set { + + guard let responseDict = response as? [String: AnyObject] else { + let responseError: NSError = OWSErrorMakeUnableToProcessServerResponseError() as NSError + responseError.isRetryable = true + + throw responseError + } + + guard let contactDicts = responseDict["contacts"] as? [[String: AnyObject]] else { + let responseError: NSError = OWSErrorMakeUnableToProcessServerResponseError() as NSError + responseError.isRetryable = true + + throw responseError + } + + var registeredRecipientIds: Set = Set() + + for contactDict in contactDicts { + guard let hash = contactDict["token"] as? String, hash.count > 0 else { + owsFailDebug("hash was unexpectedly nil") + continue + } + + guard let recipientId = phoneNumbersByHashes[hash], recipientId.count > 0 else { + owsFailDebug("recipientId was unexpectedly nil") + continue + } + + guard recipientIdsToLookup.contains(recipientId) else { + owsFailDebug("unexpected recipientId") + continue + } + + registeredRecipientIds.insert(recipientId) + } + + return registeredRecipientIds + } + +} + +enum ContactDiscoveryError: Error { + case parseError(description: String) + case assertionError(description: String) + case clientError(underlyingError: Error) + case serverError(underlyingError: Error) +} + +@objc(OWSCDSOperation) +class CDSOperation: OWSOperation { + + let batchSize = 2048 + static let operationQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 5 + queue.name = CDSOperation.logTag() + return queue + }() + + let recipientIdsToLookup: [String] + + @objc + var registeredRecipientIds: Set + + @objc + required init(recipientIdsToLookup: [String]) { + self.recipientIdsToLookup = recipientIdsToLookup + self.registeredRecipientIds = Set() + + super.init() + + Logger.debug("with recipientIdsToLookup: \(recipientIdsToLookup.count)") + for batchIds in recipientIdsToLookup.chunked(by: batchSize) { + let batchOperation = CDSBatchOperation(recipientIdsToLookup: batchIds) + self.addDependency(batchOperation) + } + } + + // MARK: Mandatory overrides + + // Called every retry, this is where the bulk of the operation's work should go. + override func run() { + Logger.debug("") + + for dependency in self.dependencies { + guard let batchOperation = dependency as? CDSBatchOperation else { + owsFailDebug("unexpected dependency: \(dependency)") + continue + } + + self.registeredRecipientIds.formUnion(batchOperation.registeredRecipientIds) + } + + self.reportSuccess() + } + +} + +public +class CDSBatchOperation: OWSOperation { + + private let recipientIdsToLookup: [String] + private(set) var registeredRecipientIds: Set + + private var networkManager: TSNetworkManager { + return TSNetworkManager.shared() + } + + private var contactDiscoveryService: ContactDiscoveryService { + return ContactDiscoveryService.shared() + } + + // MARK: Initializers + + public required init(recipientIdsToLookup: [String]) { + self.recipientIdsToLookup = Set(recipientIdsToLookup).map { $0 } + self.registeredRecipientIds = Set() + + super.init() + + Logger.debug("with recipientIdsToLookup: \(recipientIdsToLookup.count)") + } + + // MARK: OWSOperationOverrides + + // Called every retry, this is where the bulk of the operation's work should go. + override public func run() { + Logger.debug("") + + guard !isCancelled else { + Logger.info("no work to do, since we were canceled") + self.reportCancelled() + return + } + + contactDiscoveryService.performRemoteAttestation(success: { (remoteAttestation: RemoteAttestation) in + self.makeContactDiscoveryRequest(remoteAttestation: remoteAttestation) + }, + failure: self.reportError) + } + + private func makeContactDiscoveryRequest(remoteAttestation: RemoteAttestation) { + return // Loki: Do nothing + + guard !isCancelled else { + Logger.info("no work to do, since we were canceled") + self.reportCancelled() + return + } + + let encryptionResult: AES25GCMEncryptionResult + do { + encryptionResult = try encryptAddresses(recipientIds: recipientIdsToLookup, remoteAttestation: remoteAttestation) + } catch { + reportError(error) + return + } + + let request = OWSRequestFactory.enclaveContactDiscoveryRequest(withId: remoteAttestation.requestId, + addressCount: UInt(recipientIdsToLookup.count), + encryptedAddressData: encryptionResult.ciphertext, + cryptIv: encryptionResult.initializationVector, + cryptMac: encryptionResult.authTag, + enclaveId: remoteAttestation.enclaveId, + authUsername: remoteAttestation.auth.username, + authPassword: remoteAttestation.auth.password, + cookies: remoteAttestation.cookies) + + self.networkManager.makeRequest(request, + success: { (task, responseDict) in + do { + self.registeredRecipientIds = try self.handle(response: responseDict, remoteAttestation: remoteAttestation) + self.reportSuccess() + } catch { + self.reportError(error) + } + }, + failure: { (task, error) in + guard let response = task.response as? HTTPURLResponse else { + let responseError = OWSErrorMakeUnableToProcessServerResponseError() as NSError + responseError.isRetryable = true + self.reportError(responseError) + return + } + + guard response.statusCode != 413 else { + let rateLimitError: NSError = OWSErrorWithCodeDescription(OWSErrorCode.contactsUpdaterRateLimit, "Contacts Intersection Rate Limit") as NSError + + // TODO CDS ratelimiting, handle Retry-After header if available + rateLimitError.isRetryable = false + self.reportError(rateLimitError) + return + } + + guard response.statusCode / 100 != 4 else { + let clientError: NSError = ContactDiscoveryError.clientError(underlyingError: error) as NSError + clientError.isRetryable = (error as NSError).isRetryable + self.reportError(clientError) + return + } + + guard response.statusCode / 100 != 5 else { + let serverError = ContactDiscoveryError.serverError(underlyingError: error) as NSError + serverError.isRetryable = (error as NSError).isRetryable + + // TODO CDS ratelimiting, handle Retry-After header if available + self.reportError(serverError) + return + } + + self.reportError(error) + }) + } + + func encryptAddresses(recipientIds: [String], remoteAttestation: RemoteAttestation) throws -> AES25GCMEncryptionResult { + + let addressPlainTextData = try type(of: self).encodePhoneNumbers(recipientIds: recipientIds) + + guard let encryptionResult = Cryptography.encryptAESGCM(plainTextData: addressPlainTextData, + additionalAuthenticatedData: remoteAttestation.requestId, + key: remoteAttestation.keys.clientKey) else { + + throw ContactDiscoveryError.assertionError(description: "Encryption failure") + } + + return encryptionResult + } + + class func encodePhoneNumbers(recipientIds: [String]) throws -> Data { + var output = Data() + + for recipientId in recipientIds { + guard recipientId.prefix(1) == "+" else { + throw ContactDiscoveryError.assertionError(description: "unexpected id format") + } + + let numericPortionIndex = recipientId.index(after: recipientId.startIndex) + let numericPortion = recipientId.suffix(from: numericPortionIndex) + + guard let numericIdentifier = UInt64(numericPortion), numericIdentifier > 99 else { + throw ContactDiscoveryError.assertionError(description: "unexpectedly short identifier") + } + + var bigEndian: UInt64 = CFSwapInt64HostToBig(numericIdentifier) + let buffer = UnsafeBufferPointer(start: &bigEndian, count: 1) + output.append(buffer) + } + + return output + } + + func handle(response: Any?, remoteAttestation: RemoteAttestation) throws -> Set { + let isIncludedData: Data = try parseAndDecrypt(response: response, remoteAttestation: remoteAttestation) + guard let isIncluded: [Bool] = type(of: self).boolArray(data: isIncludedData) else { + throw ContactDiscoveryError.assertionError(description: "isIncluded was unexpectedly nil") + } + + return try match(recipientIds: self.recipientIdsToLookup, isIncluded: isIncluded) + } + + class func boolArray(data: Data) -> [Bool]? { + var bools: [Bool]? + data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + let buffer = UnsafeBufferPointer(start: bytes, count: data.count) + bools = Array(buffer) + } + + return bools + } + + func match(recipientIds: [String], isIncluded: [Bool]) throws -> Set { + guard recipientIds.count == isIncluded.count else { + throw ContactDiscoveryError.assertionError(description: "length mismatch for isIncluded/recipientIds") + } + + let includedRecipientIds: [String] = (0.. Data { + + guard let params = ParamParser(responseObject: response) else { + throw ContactDiscoveryError.parseError(description: "missing response dict") + } + + let cipherText = try params.requiredBase64EncodedData(key: "data") + let initializationVector = try params.requiredBase64EncodedData(key: "iv") + let authTag = try params.requiredBase64EncodedData(key: "mac") + + guard let plainText = Cryptography.decryptAESGCM(withInitializationVector: initializationVector, + ciphertext: cipherText, + additionalAuthenticatedData: nil, + authTag: authTag, + key: remoteAttestation.keys.serverKey) else { + throw ContactDiscoveryError.parseError(description: "decryption failed") + } + + return plainText + } +} + +class CDSFeedbackOperation: OWSOperation { + + enum FeedbackResult { + case ok + case mismatch + case attestationError(reason: String) + case unexpectedError(reason: String) + } + + private let legacyRegisteredRecipientIds: Set + + var networkManager: TSNetworkManager { + return TSNetworkManager.shared() + } + + // MARK: Initializers + + required init(legacyRegisteredRecipientIds: Set) { + self.legacyRegisteredRecipientIds = legacyRegisteredRecipientIds + + super.init() + + Logger.debug("") + } + + // MARK: OWSOperation Overrides + + override func checkForPreconditionError() -> Error? { + // override super with no-op + // In this rare case, we want to proceed even though our dependency might have an + // error so we can report the details of that error to the feedback service. + return nil + } + + // Called every retry, this is where the bulk of the operation's work should go. + override func run() { + + guard !isCancelled else { + Logger.info("no work to do, since we were canceled") + self.reportCancelled() + return + } + + guard let cdsOperation = dependencies.first as? CDSOperation else { + let error = OWSErrorMakeAssertionError("cdsOperation was unexpectedly nil") + self.reportError(error) + return + } + + if let error = cdsOperation.failingError { + switch error { + case TSNetworkManagerError.failedConnection: + // Don't submit feedback for connectivity errors + self.reportSuccess() + case ContactDiscoveryError.serverError, ContactDiscoveryError.clientError: + // Server already has this information, no need submit feedback + self.reportSuccess() + case let cdsError as ContactDiscoveryServiceError: + let reason = cdsError.reason + switch cdsError.code { + case .assertionError: + self.makeRequest(result: .unexpectedError(reason: "CDS assertionError: \(reason ?? "unknown")")) + case .attestationFailed: + self.makeRequest(result: .attestationError(reason: "CDS attestationFailed: \(reason ?? "unknown")")) + } + case ContactDiscoveryError.assertionError(let assertionDescription): + self.makeRequest(result: .unexpectedError(reason: "assertionError: \(assertionDescription)")) + case ContactDiscoveryError.parseError(description: let parseErrorDescription): + self.makeRequest(result: .unexpectedError(reason: "parseError: \(parseErrorDescription)")) + default: + let nsError = error as NSError + let reason = "unexpectedError code:\(nsError.code)" + self.makeRequest(result: .unexpectedError(reason: reason)) + } + + return + } + + if cdsOperation.registeredRecipientIds == legacyRegisteredRecipientIds { + self.makeRequest(result: .ok) + return + } else { + self.makeRequest(result: .mismatch) + return + } + } + + func makeRequest(result: FeedbackResult) { + let reason: String? + switch result { + case .ok: + reason = nil + case .mismatch: + reason = nil + case .attestationError(let attestationErrorReason): + reason = attestationErrorReason + case .unexpectedError(let unexpectedErrorReason): + reason = unexpectedErrorReason + } + let request = OWSRequestFactory.cdsFeedbackRequest(status: result.statusPath, reason: reason) + self.networkManager.makeRequest(request, + success: { _, _ in self.reportSuccess() }, + failure: { _, error in self.reportError(error) }) + } +} + +extension Array { + func chunked(by chunkSize: Int) -> [[Element]] { + return stride(from: 0, to: self.count, by: chunkSize).map { + Array(self[$0..)contactsManager + conversationColorName:(NSString *)conversationColorName +disappearingMessagesConfiguration:(nullable OWSDisappearingMessagesConfiguration *)disappearingMessagesConfiguration; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSContactsOutputStream.m b/SignalUtilitiesKit/OWSContactsOutputStream.m new file mode 100644 index 000000000..9515a0b29 --- /dev/null +++ b/SignalUtilitiesKit/OWSContactsOutputStream.m @@ -0,0 +1,109 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSContactsOutputStream.h" +#import "Contact.h" +#import "ContactsManagerProtocol.h" +#import "MIMETypeUtil.h" +#import "NSData+keyVersionByte.h" +#import "OWSBlockingManager.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import "OWSRecipientIdentity.h" +#import "SignalAccount.h" +#import "TSContactThread.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSContactsOutputStream + +- (void)writeSignalAccount:(SignalAccount *)signalAccount + recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity + profileKeyData:(nullable NSData *)profileKeyData + contactsManager:(id)contactsManager + conversationColorName:(NSString *)conversationColorName +disappearingMessagesConfiguration:(nullable OWSDisappearingMessagesConfiguration *)disappearingMessagesConfiguration +{ + OWSAssertDebug(signalAccount); + OWSAssertDebug(contactsManager); + + SSKProtoContactDetailsBuilder *contactBuilder = + [SSKProtoContactDetails builderWithNumber:signalAccount.recipientId]; + [contactBuilder setName:[LKUserDisplayNameUtilities getPrivateChatDisplayNameFor:signalAccount.recipientId] ?: signalAccount.recipientId]; + [contactBuilder setColor:conversationColorName]; + + if (recipientIdentity != nil) { + SSKProtoVerified *_Nullable verified = BuildVerifiedProtoWithRecipientId(recipientIdentity.recipientId, + [recipientIdentity.identityKey prependKeyType], + recipientIdentity.verificationState, + 0); + if (!verified) { + OWSLogError(@"could not build protobuf."); + return; + } + contactBuilder.verified = verified; + } + + /* + UIImage *_Nullable rawAvatar = [contactsManager avatarImageForCNContactId:signalAccount.contact.cnContactId]; + NSData *_Nullable avatarPng; + if (rawAvatar) { + avatarPng = UIImagePNGRepresentation(rawAvatar); + if (avatarPng) { + SSKProtoContactDetailsAvatarBuilder *avatarBuilder = [SSKProtoContactDetailsAvatar builder]; + [avatarBuilder setContentType:OWSMimeTypeImagePng]; + [avatarBuilder setLength:(uint32_t)avatarPng.length]; + + NSError *error; + SSKProtoContactDetailsAvatar *_Nullable avatar = [avatarBuilder buildAndReturnError:&error]; + if (error || !avatar) { + OWSLogError(@"could not build protobuf: %@", error); + return; + } + [contactBuilder setAvatar:avatar]; + } + } + */ + + if (profileKeyData) { + OWSAssertDebug(profileKeyData.length == kAES256_KeyByteLength); + [contactBuilder setProfileKey:profileKeyData]; + } + + // Always ensure the "expire timer" property is set so that desktop + // can easily distinguish between a modern client declaring "off" vs a + // legacy client "not specifying". + [contactBuilder setExpireTimer:0]; + + if (disappearingMessagesConfiguration && disappearingMessagesConfiguration.isEnabled) { + [contactBuilder setExpireTimer:disappearingMessagesConfiguration.durationSeconds]; + } + + if ([OWSBlockingManager.sharedManager isRecipientIdBlocked:signalAccount.recipientId]) { + [contactBuilder setBlocked:YES]; + } + + NSError *error; + NSData *_Nullable contactData = [contactBuilder buildSerializedDataAndReturnError:&error]; + if (error || !contactData) { + OWSFailDebug(@"could not serialize protobuf: %@", error); + return; + } + + uint32_t contactDataLength = (uint32_t)contactData.length; + [self writeUInt32:contactDataLength]; + [self writeData:contactData]; + + /* + if (avatarPng) { + [self writeData:avatarPng]; + } + */ +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSCountryMetadata.h b/SignalUtilitiesKit/OWSCountryMetadata.h new file mode 100644 index 000000000..f2ce56d8c --- /dev/null +++ b/SignalUtilitiesKit/OWSCountryMetadata.h @@ -0,0 +1,23 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSCountryMetadata : NSObject + +@property (nonatomic) NSString *name; +@property (nonatomic) NSString *tld; +@property (nonatomic, nullable) NSString *frontingDomain; +@property (nonatomic) NSString *countryCode; +@property (nonatomic) NSString *localizedCountryName; + ++ (OWSCountryMetadata *)countryMetadataForCountryCode:(NSString *)countryCode; + ++ (NSArray *)allCountryMetadatas; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSCountryMetadata.m b/SignalUtilitiesKit/OWSCountryMetadata.m new file mode 100644 index 000000000..0c0987457 --- /dev/null +++ b/SignalUtilitiesKit/OWSCountryMetadata.m @@ -0,0 +1,379 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSCountryMetadata.h" +#import "OWSCensorshipConfiguration.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSCountryMetadata + ++ (OWSCountryMetadata *)countryMetadataWithName:(NSString *)name + tld:(NSString *)tld + frontingDomain:(nullable NSString *)frontingDomain + countryCode:(NSString *)countryCode +{ + OWSAssertDebug(name.length > 0); + OWSAssertDebug(tld.length > 0); + OWSAssertDebug(countryCode.length > 0); + + OWSCountryMetadata *instance = [OWSCountryMetadata new]; + instance.name = name; + instance.tld = tld; + instance.frontingDomain = frontingDomain; + instance.countryCode = countryCode; + + NSString *localizedCountryName = [[NSLocale currentLocale] displayNameForKey:NSLocaleCountryCode value:countryCode]; + if (localizedCountryName.length < 1) { + localizedCountryName = name; + } + instance.localizedCountryName = localizedCountryName; + + return instance; +} + ++ (OWSCountryMetadata *)countryMetadataForCountryCode:(NSString *)countryCode +{ + OWSAssertDebug(countryCode.length > 0); + + return [self countryCodeToCountryMetadataMap][countryCode]; +} + ++ (NSDictionary *)countryCodeToCountryMetadataMap +{ + static NSDictionary *cachedValue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *map = [NSMutableDictionary new]; + for (OWSCountryMetadata *metadata in [self allCountryMetadatas]) { + map[metadata.countryCode] = metadata; + } + cachedValue = map; + }); + return cachedValue; +} + ++ (NSArray *)allCountryMetadatas +{ + static NSArray *cachedValue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cachedValue = @[ + [OWSCountryMetadata countryMetadataWithName:@"Andorra" tld:@".ad" frontingDomain:nil countryCode:@"AD"], + [OWSCountryMetadata countryMetadataWithName:@"United Arab Emirates" + tld:@".ae" + frontingDomain:OWSFrontingHost_GoogleUAE + countryCode:@"AE"], + [OWSCountryMetadata countryMetadataWithName:@"Afghanistan" tld:@".af" frontingDomain:nil countryCode:@"AF"], + [OWSCountryMetadata countryMetadataWithName:@"Antigua and Barbuda" + tld:@".ag" + frontingDomain:nil + countryCode:@"AG"], + [OWSCountryMetadata countryMetadataWithName:@"Anguilla" tld:@".ai" frontingDomain:nil countryCode:@"AI"], + [OWSCountryMetadata countryMetadataWithName:@"Albania" tld:@".al" frontingDomain:nil countryCode:@"AL"], + [OWSCountryMetadata countryMetadataWithName:@"Armenia" tld:@".am" frontingDomain:nil countryCode:@"AM"], + [OWSCountryMetadata countryMetadataWithName:@"Angola" tld:@".ao" frontingDomain:nil countryCode:@"AO"], + [OWSCountryMetadata countryMetadataWithName:@"Argentina" tld:@".ar" frontingDomain:nil countryCode:@"AR"], + [OWSCountryMetadata countryMetadataWithName:@"American Samoa" + tld:@".as" + frontingDomain:nil + countryCode:@"AS"], + [OWSCountryMetadata countryMetadataWithName:@"Austria" tld:@".at" frontingDomain:nil countryCode:@"AT"], + [OWSCountryMetadata countryMetadataWithName:@"Australia" tld:@".au" frontingDomain:nil countryCode:@"AU"], + [OWSCountryMetadata countryMetadataWithName:@"Azerbaijan" tld:@".az" frontingDomain:nil countryCode:@"AZ"], + [OWSCountryMetadata countryMetadataWithName:@"Bosnia and Herzegovina" + tld:@".ba" + frontingDomain:nil + countryCode:@"BA"], + [OWSCountryMetadata countryMetadataWithName:@"Bangladesh" tld:@".bd" frontingDomain:nil countryCode:@"BD"], + [OWSCountryMetadata countryMetadataWithName:@"Belgium" tld:@".be" frontingDomain:nil countryCode:@"BE"], + [OWSCountryMetadata countryMetadataWithName:@"Burkina Faso" + tld:@".bf" + frontingDomain:nil + countryCode:@"BF"], + [OWSCountryMetadata countryMetadataWithName:@"Bulgaria" tld:@".bg" frontingDomain:nil countryCode:@"BG"], + [OWSCountryMetadata countryMetadataWithName:@"Bahrain" tld:@".bh" frontingDomain:nil countryCode:@"BH"], + [OWSCountryMetadata countryMetadataWithName:@"Burundi" tld:@".bi" frontingDomain:nil countryCode:@"BI"], + [OWSCountryMetadata countryMetadataWithName:@"Benin" tld:@".bj" frontingDomain:nil countryCode:@"BJ"], + [OWSCountryMetadata countryMetadataWithName:@"Brunei" tld:@".bn" frontingDomain:nil countryCode:@"BN"], + [OWSCountryMetadata countryMetadataWithName:@"Bolivia" tld:@".bo" frontingDomain:nil countryCode:@"BO"], + [OWSCountryMetadata countryMetadataWithName:@"Brazil" tld:@".br" frontingDomain:nil countryCode:@"BR"], + [OWSCountryMetadata countryMetadataWithName:@"Bahamas" tld:@".bs" frontingDomain:nil countryCode:@"BS"], + [OWSCountryMetadata countryMetadataWithName:@"Bhutan" tld:@".bt" frontingDomain:nil countryCode:@"BT"], + [OWSCountryMetadata countryMetadataWithName:@"Botswana" tld:@".bw" frontingDomain:nil countryCode:@"BW"], + [OWSCountryMetadata countryMetadataWithName:@"Belarus" tld:@".by" frontingDomain:nil countryCode:@"BY"], + [OWSCountryMetadata countryMetadataWithName:@"Belize" tld:@".bz" frontingDomain:nil countryCode:@"BZ"], + [OWSCountryMetadata countryMetadataWithName:@"Canada" tld:@".ca" frontingDomain:nil countryCode:@"CA"], + [OWSCountryMetadata countryMetadataWithName:@"Cambodia" tld:@".kh" frontingDomain:nil countryCode:@"KH"], + [OWSCountryMetadata countryMetadataWithName:@"Cocos (Keeling) Islands" + tld:@".cc" + frontingDomain:nil + countryCode:@"CC"], + [OWSCountryMetadata countryMetadataWithName:@"Democratic Republic of the Congo" + tld:@".cd" + frontingDomain:nil + countryCode:@"CD"], + [OWSCountryMetadata countryMetadataWithName:@"Central African Republic" + tld:@".cf" + frontingDomain:nil + countryCode:@"CF"], + [OWSCountryMetadata countryMetadataWithName:@"Republic of the Congo" + tld:@".cg" + frontingDomain:nil + countryCode:@"CG"], + [OWSCountryMetadata countryMetadataWithName:@"Switzerland" tld:@".ch" frontingDomain:nil countryCode:@"CH"], + [OWSCountryMetadata countryMetadataWithName:@"Ivory Coast" tld:@".ci" frontingDomain:nil countryCode:@"CI"], + [OWSCountryMetadata countryMetadataWithName:@"Cook Islands" + tld:@".ck" + frontingDomain:nil + countryCode:@"CK"], + [OWSCountryMetadata countryMetadataWithName:@"Chile" tld:@".cl" frontingDomain:nil countryCode:@"CL"], + [OWSCountryMetadata countryMetadataWithName:@"Cameroon" tld:@".cm" frontingDomain:nil countryCode:@"CM"], + [OWSCountryMetadata countryMetadataWithName:@"China" tld:@".cn" frontingDomain:nil countryCode:@"CN"], + [OWSCountryMetadata countryMetadataWithName:@"Colombia" tld:@".co" frontingDomain:nil countryCode:@"CO"], + [OWSCountryMetadata countryMetadataWithName:@"Costa Rica" tld:@".cr" frontingDomain:nil countryCode:@"CR"], + [OWSCountryMetadata countryMetadataWithName:@"Cuba" tld:@".cu" frontingDomain:nil countryCode:@"CU"], + [OWSCountryMetadata countryMetadataWithName:@"Cape Verde" tld:@".cv" frontingDomain:nil countryCode:@"CV"], + [OWSCountryMetadata countryMetadataWithName:@"Christmas Island" + tld:@".cx" + frontingDomain:nil + countryCode:@"CX"], + [OWSCountryMetadata countryMetadataWithName:@"Cyprus" tld:@".cy" frontingDomain:nil countryCode:@"CY"], + [OWSCountryMetadata countryMetadataWithName:@"Czech Republic" + tld:@".cz" + frontingDomain:nil + countryCode:@"CZ"], + [OWSCountryMetadata countryMetadataWithName:@"Germany" tld:@".de" frontingDomain:nil countryCode:@"DE"], + [OWSCountryMetadata countryMetadataWithName:@"Djibouti" tld:@".dj" frontingDomain:nil countryCode:@"DJ"], + [OWSCountryMetadata countryMetadataWithName:@"Denmark" tld:@".dk" frontingDomain:nil countryCode:@"DK"], + [OWSCountryMetadata countryMetadataWithName:@"Dominica" tld:@".dm" frontingDomain:nil countryCode:@"DM"], + [OWSCountryMetadata countryMetadataWithName:@"Dominican Republic" + tld:@".do" + frontingDomain:nil + countryCode:@"DO"], + [OWSCountryMetadata countryMetadataWithName:@"Algeria" tld:@".dz" frontingDomain:nil countryCode:@"DZ"], + [OWSCountryMetadata countryMetadataWithName:@"Ecuador" tld:@".ec" frontingDomain:nil countryCode:@"EC"], + [OWSCountryMetadata countryMetadataWithName:@"Estonia" tld:@".ee" frontingDomain:nil countryCode:@"EE"], + [OWSCountryMetadata countryMetadataWithName:@"Egypt" + tld:@".eg" + frontingDomain:OWSFrontingHost_GoogleEgypt + countryCode:@"EG"], + [OWSCountryMetadata countryMetadataWithName:@"Spain" tld:@".es" frontingDomain:nil countryCode:@"ES"], + [OWSCountryMetadata countryMetadataWithName:@"Ethiopia" tld:@".et" frontingDomain:nil countryCode:@"ET"], + [OWSCountryMetadata countryMetadataWithName:@"Finland" tld:@".fi" frontingDomain:nil countryCode:@"FI"], + [OWSCountryMetadata countryMetadataWithName:@"Fiji" tld:@".fj" frontingDomain:nil countryCode:@"FJ"], + [OWSCountryMetadata countryMetadataWithName:@"Federated States of Micronesia" + tld:@".fm" + frontingDomain:nil + countryCode:@"FM"], + [OWSCountryMetadata countryMetadataWithName:@"France" tld:@".fr" frontingDomain:nil countryCode:@"FR"], + [OWSCountryMetadata countryMetadataWithName:@"Gabon" tld:@".ga" frontingDomain:nil countryCode:@"GA"], + [OWSCountryMetadata countryMetadataWithName:@"Georgia" tld:@".ge" frontingDomain:nil countryCode:@"GE"], + [OWSCountryMetadata countryMetadataWithName:@"French Guiana" + tld:@".gf" + frontingDomain:nil + countryCode:@"GF"], + [OWSCountryMetadata countryMetadataWithName:@"Guernsey" tld:@".gg" frontingDomain:nil countryCode:@"GG"], + [OWSCountryMetadata countryMetadataWithName:@"Ghana" tld:@".gh" frontingDomain:nil countryCode:@"GH"], + [OWSCountryMetadata countryMetadataWithName:@"Gibraltar" tld:@".gi" frontingDomain:nil countryCode:@"GI"], + [OWSCountryMetadata countryMetadataWithName:@"Greenland" tld:@".gl" frontingDomain:nil countryCode:@"GL"], + [OWSCountryMetadata countryMetadataWithName:@"Gambia" tld:@".gm" frontingDomain:nil countryCode:@"GM"], + [OWSCountryMetadata countryMetadataWithName:@"Guadeloupe" tld:@".gp" frontingDomain:nil countryCode:@"GP"], + [OWSCountryMetadata countryMetadataWithName:@"Greece" tld:@".gr" frontingDomain:nil countryCode:@"GR"], + [OWSCountryMetadata countryMetadataWithName:@"Guatemala" tld:@".gt" frontingDomain:nil countryCode:@"GT"], + [OWSCountryMetadata countryMetadataWithName:@"Guyana" tld:@".gy" frontingDomain:nil countryCode:@"GY"], + [OWSCountryMetadata countryMetadataWithName:@"Hong Kong" tld:@".hk" frontingDomain:nil countryCode:@"HK"], + [OWSCountryMetadata countryMetadataWithName:@"Honduras" tld:@".hn" frontingDomain:nil countryCode:@"HN"], + [OWSCountryMetadata countryMetadataWithName:@"Croatia" tld:@".hr" frontingDomain:nil countryCode:@"HR"], + [OWSCountryMetadata countryMetadataWithName:@"Haiti" tld:@".ht" frontingDomain:nil countryCode:@"HT"], + [OWSCountryMetadata countryMetadataWithName:@"Hungary" tld:@".hu" frontingDomain:nil countryCode:@"HU"], + [OWSCountryMetadata countryMetadataWithName:@"Indonesia" tld:@".id" frontingDomain:nil countryCode:@"ID"], + [OWSCountryMetadata countryMetadataWithName:@"Iraq" tld:@".iq" frontingDomain:nil countryCode:@"IQ"], + [OWSCountryMetadata countryMetadataWithName:@"Ireland" tld:@".ie" frontingDomain:nil countryCode:@"IE"], + [OWSCountryMetadata countryMetadataWithName:@"Israel" tld:@".il" frontingDomain:nil countryCode:@"IL"], + [OWSCountryMetadata countryMetadataWithName:@"Isle of Man" tld:@".im" frontingDomain:nil countryCode:@"IM"], + [OWSCountryMetadata countryMetadataWithName:@"India" tld:@".in" frontingDomain:nil countryCode:@"IN"], + [OWSCountryMetadata countryMetadataWithName:@"British Indian Ocean Territory" + tld:@".io" + frontingDomain:nil + countryCode:@"IO"], + [OWSCountryMetadata countryMetadataWithName:@"Iceland" tld:@".is" frontingDomain:nil countryCode:@"IS"], + [OWSCountryMetadata countryMetadataWithName:@"Italy" tld:@".it" frontingDomain:nil countryCode:@"IT"], + [OWSCountryMetadata countryMetadataWithName:@"Jersey" tld:@".je" frontingDomain:nil countryCode:@"JE"], + [OWSCountryMetadata countryMetadataWithName:@"Jamaica" tld:@".jm" frontingDomain:nil countryCode:@"JM"], + [OWSCountryMetadata countryMetadataWithName:@"Jordan" tld:@".jo" frontingDomain:nil countryCode:@"JO"], + [OWSCountryMetadata countryMetadataWithName:@"Japan" tld:@".jp" frontingDomain:nil countryCode:@"JP"], + [OWSCountryMetadata countryMetadataWithName:@"Kenya" tld:@".ke" frontingDomain:nil countryCode:@"KE"], + [OWSCountryMetadata countryMetadataWithName:@"Kiribati" tld:@".ki" frontingDomain:nil countryCode:@"KI"], + [OWSCountryMetadata countryMetadataWithName:@"Kyrgyzstan" tld:@".kg" frontingDomain:nil countryCode:@"KG"], + [OWSCountryMetadata countryMetadataWithName:@"South Korea" tld:@".kr" frontingDomain:nil countryCode:@"KR"], + [OWSCountryMetadata countryMetadataWithName:@"Kuwait" tld:@".kw" frontingDomain:nil countryCode:@"KW"], + [OWSCountryMetadata countryMetadataWithName:@"Kazakhstan" tld:@".kz" frontingDomain:nil countryCode:@"KZ"], + [OWSCountryMetadata countryMetadataWithName:@"Laos" tld:@".la" frontingDomain:nil countryCode:@"LA"], + [OWSCountryMetadata countryMetadataWithName:@"Lebanon" tld:@".lb" frontingDomain:nil countryCode:@"LB"], + [OWSCountryMetadata countryMetadataWithName:@"Saint Lucia" tld:@".lc" frontingDomain:nil countryCode:@"LC"], + [OWSCountryMetadata countryMetadataWithName:@"Liechtenstein" + tld:@".li" + frontingDomain:nil + countryCode:@"LI"], + [OWSCountryMetadata countryMetadataWithName:@"Sri Lanka" tld:@".lk" frontingDomain:nil countryCode:@"LK"], + [OWSCountryMetadata countryMetadataWithName:@"Lesotho" tld:@".ls" frontingDomain:nil countryCode:@"LS"], + [OWSCountryMetadata countryMetadataWithName:@"Lithuania" tld:@".lt" frontingDomain:nil countryCode:@"LT"], + [OWSCountryMetadata countryMetadataWithName:@"Luxembourg" tld:@".lu" frontingDomain:nil countryCode:@"LU"], + [OWSCountryMetadata countryMetadataWithName:@"Latvia" tld:@".lv" frontingDomain:nil countryCode:@"LV"], + [OWSCountryMetadata countryMetadataWithName:@"Libya" tld:@".ly" frontingDomain:nil countryCode:@"LY"], + [OWSCountryMetadata countryMetadataWithName:@"Morocco" tld:@".ma" frontingDomain:nil countryCode:@"MA"], + [OWSCountryMetadata countryMetadataWithName:@"Moldova" tld:@".md" frontingDomain:nil countryCode:@"MD"], + [OWSCountryMetadata countryMetadataWithName:@"Montenegro" tld:@".me" frontingDomain:nil countryCode:@"ME"], + [OWSCountryMetadata countryMetadataWithName:@"Madagascar" tld:@".mg" frontingDomain:nil countryCode:@"MG"], + [OWSCountryMetadata countryMetadataWithName:@"Macedonia" tld:@".mk" frontingDomain:nil countryCode:@"MK"], + [OWSCountryMetadata countryMetadataWithName:@"Mali" tld:@".ml" frontingDomain:nil countryCode:@"ML"], + [OWSCountryMetadata countryMetadataWithName:@"Myanmar" tld:@".mm" frontingDomain:nil countryCode:@"MM"], + [OWSCountryMetadata countryMetadataWithName:@"Mongolia" tld:@".mn" frontingDomain:nil countryCode:@"MN"], + [OWSCountryMetadata countryMetadataWithName:@"Montserrat" tld:@".ms" frontingDomain:nil countryCode:@"MS"], + [OWSCountryMetadata countryMetadataWithName:@"Malta" tld:@".mt" frontingDomain:nil countryCode:@"MT"], + [OWSCountryMetadata countryMetadataWithName:@"Mauritius" tld:@".mu" frontingDomain:nil countryCode:@"MU"], + [OWSCountryMetadata countryMetadataWithName:@"Maldives" tld:@".mv" frontingDomain:nil countryCode:@"MV"], + [OWSCountryMetadata countryMetadataWithName:@"Malawi" tld:@".mw" frontingDomain:nil countryCode:@"MW"], + [OWSCountryMetadata countryMetadataWithName:@"Mexico" tld:@".mx" frontingDomain:nil countryCode:@"MX"], + [OWSCountryMetadata countryMetadataWithName:@"Malaysia" tld:@".my" frontingDomain:nil countryCode:@"MY"], + [OWSCountryMetadata countryMetadataWithName:@"Mozambique" tld:@".mz" frontingDomain:nil countryCode:@"MZ"], + [OWSCountryMetadata countryMetadataWithName:@"Namibia" tld:@".na" frontingDomain:nil countryCode:@"NA"], + [OWSCountryMetadata countryMetadataWithName:@"Niger" tld:@".ne" frontingDomain:nil countryCode:@"NE"], + [OWSCountryMetadata countryMetadataWithName:@"Norfolk Island" + tld:@".nf" + frontingDomain:nil + countryCode:@"NF"], + [OWSCountryMetadata countryMetadataWithName:@"Nigeria" tld:@".ng" frontingDomain:nil countryCode:@"NG"], + [OWSCountryMetadata countryMetadataWithName:@"Nicaragua" tld:@".ni" frontingDomain:nil countryCode:@"NI"], + [OWSCountryMetadata countryMetadataWithName:@"Netherlands" tld:@".nl" frontingDomain:nil countryCode:@"NL"], + [OWSCountryMetadata countryMetadataWithName:@"Norway" tld:@".no" frontingDomain:nil countryCode:@"NO"], + [OWSCountryMetadata countryMetadataWithName:@"Nepal" tld:@".np" frontingDomain:nil countryCode:@"NP"], + [OWSCountryMetadata countryMetadataWithName:@"Nauru" tld:@".nr" frontingDomain:nil countryCode:@"NR"], + [OWSCountryMetadata countryMetadataWithName:@"Niue" tld:@".nu" frontingDomain:nil countryCode:@"NU"], + [OWSCountryMetadata countryMetadataWithName:@"New Zealand" tld:@".nz" frontingDomain:nil countryCode:@"NZ"], + [OWSCountryMetadata countryMetadataWithName:@"Oman" + tld:@".om" + frontingDomain:OWSFrontingHost_GoogleOman + countryCode:@"OM"], + [OWSCountryMetadata countryMetadataWithName:@"Pakistan" tld:@".pk" frontingDomain:nil countryCode:@"PK"], + [OWSCountryMetadata countryMetadataWithName:@"Panama" tld:@".pa" frontingDomain:nil countryCode:@"PA"], + [OWSCountryMetadata countryMetadataWithName:@"Peru" tld:@".pe" frontingDomain:nil countryCode:@"PE"], + [OWSCountryMetadata countryMetadataWithName:@"Philippines" tld:@".ph" frontingDomain:nil countryCode:@"PH"], + [OWSCountryMetadata countryMetadataWithName:@"Poland" tld:@".pl" frontingDomain:nil countryCode:@"PL"], + [OWSCountryMetadata countryMetadataWithName:@"Papua New Guinea" + tld:@".pg" + frontingDomain:nil + countryCode:@"PG"], + [OWSCountryMetadata countryMetadataWithName:@"Pitcairn Islands" + tld:@".pn" + frontingDomain:nil + countryCode:@"PN"], + [OWSCountryMetadata countryMetadataWithName:@"Puerto Rico" tld:@".pr" frontingDomain:nil countryCode:@"PR"], + [OWSCountryMetadata countryMetadataWithName:@"Palestine[4]" + tld:@".ps" + frontingDomain:nil + countryCode:@"PS"], + [OWSCountryMetadata countryMetadataWithName:@"Portugal" tld:@".pt" frontingDomain:nil countryCode:@"PT"], + [OWSCountryMetadata countryMetadataWithName:@"Paraguay" tld:@".py" frontingDomain:nil countryCode:@"PY"], + [OWSCountryMetadata countryMetadataWithName:@"Qatar" + tld:@".qa" + frontingDomain:OWSFrontingHost_GoogleQatar + countryCode:@"QA"], + [OWSCountryMetadata countryMetadataWithName:@"Romania" tld:@".ro" frontingDomain:nil countryCode:@"RO"], + [OWSCountryMetadata countryMetadataWithName:@"Serbia" tld:@".rs" frontingDomain:nil countryCode:@"RS"], + [OWSCountryMetadata countryMetadataWithName:@"Russia" tld:@".ru" frontingDomain:nil countryCode:@"RU"], + [OWSCountryMetadata countryMetadataWithName:@"Rwanda" tld:@".rw" frontingDomain:nil countryCode:@"RW"], + [OWSCountryMetadata countryMetadataWithName:@"Saudi Arabia" + tld:@".sa" + frontingDomain:nil + countryCode:@"SA"], + [OWSCountryMetadata countryMetadataWithName:@"Solomon Islands" + tld:@".sb" + frontingDomain:nil + countryCode:@"SB"], + [OWSCountryMetadata countryMetadataWithName:@"Seychelles" tld:@".sc" frontingDomain:nil countryCode:@"SC"], + [OWSCountryMetadata countryMetadataWithName:@"Sweden" tld:@".se" frontingDomain:nil countryCode:@"SE"], + [OWSCountryMetadata countryMetadataWithName:@"Singapore" tld:@".sg" frontingDomain:nil countryCode:@"SG"], + [OWSCountryMetadata countryMetadataWithName:@"Saint Helena, Ascension and Tristan da Cunha" + tld:@".sh" + frontingDomain:nil + countryCode:@"SH"], + [OWSCountryMetadata countryMetadataWithName:@"Slovenia" tld:@".si" frontingDomain:nil countryCode:@"SI"], + [OWSCountryMetadata countryMetadataWithName:@"Slovakia" tld:@".sk" frontingDomain:nil countryCode:@"SK"], + [OWSCountryMetadata countryMetadataWithName:@"Sierra Leone" + tld:@".sl" + frontingDomain:nil + countryCode:@"SL"], + [OWSCountryMetadata countryMetadataWithName:@"Senegal" tld:@".sn" frontingDomain:nil countryCode:@"SN"], + [OWSCountryMetadata countryMetadataWithName:@"San Marino" tld:@".sm" frontingDomain:nil countryCode:@"SM"], + [OWSCountryMetadata countryMetadataWithName:@"Somalia" tld:@".so" frontingDomain:nil countryCode:@"SO"], + [OWSCountryMetadata countryMetadataWithName:@"São Tomé and Príncipe" + tld:@".st" + frontingDomain:nil + countryCode:@"ST"], + [OWSCountryMetadata countryMetadataWithName:@"Suriname" tld:@".sr" frontingDomain:nil countryCode:@"SR"], + [OWSCountryMetadata countryMetadataWithName:@"El Salvador" tld:@".sv" frontingDomain:nil countryCode:@"SV"], + [OWSCountryMetadata countryMetadataWithName:@"Chad" tld:@".td" frontingDomain:nil countryCode:@"TD"], + [OWSCountryMetadata countryMetadataWithName:@"Togo" tld:@".tg" frontingDomain:nil countryCode:@"TG"], + [OWSCountryMetadata countryMetadataWithName:@"Thailand" tld:@".th" frontingDomain:nil countryCode:@"TH"], + [OWSCountryMetadata countryMetadataWithName:@"Tajikistan" tld:@".tj" frontingDomain:nil countryCode:@"TJ"], + [OWSCountryMetadata countryMetadataWithName:@"Tokelau" tld:@".tk" frontingDomain:nil countryCode:@"TK"], + [OWSCountryMetadata countryMetadataWithName:@"Timor-Leste" tld:@".tl" frontingDomain:nil countryCode:@"TL"], + [OWSCountryMetadata countryMetadataWithName:@"Turkmenistan" + tld:@".tm" + frontingDomain:nil + countryCode:@"TM"], + [OWSCountryMetadata countryMetadataWithName:@"Tonga" tld:@".to" frontingDomain:nil countryCode:@"TO"], + [OWSCountryMetadata countryMetadataWithName:@"Tunisia" tld:@".tn" frontingDomain:nil countryCode:@"TN"], + [OWSCountryMetadata countryMetadataWithName:@"Turkey" tld:@".tr" frontingDomain:nil countryCode:@"TR"], + [OWSCountryMetadata countryMetadataWithName:@"Trinidad and Tobago" + tld:@".tt" + frontingDomain:nil + countryCode:@"TT"], + [OWSCountryMetadata countryMetadataWithName:@"Taiwan" tld:@".tw" frontingDomain:nil countryCode:@"TW"], + [OWSCountryMetadata countryMetadataWithName:@"Tanzania" tld:@".tz" frontingDomain:nil countryCode:@"TZ"], + [OWSCountryMetadata countryMetadataWithName:@"Ukraine" tld:@".ua" frontingDomain:nil countryCode:@"UA"], + [OWSCountryMetadata countryMetadataWithName:@"Uganda" tld:@".ug" frontingDomain:nil countryCode:@"UG"], + [OWSCountryMetadata countryMetadataWithName:@"United States" + tld:@".com" + frontingDomain:nil + countryCode:@"US"], + [OWSCountryMetadata countryMetadataWithName:@"Uruguay" tld:@".uy" frontingDomain:nil countryCode:@"UY"], + [OWSCountryMetadata countryMetadataWithName:@"Uzbekistan" tld:@".uz" frontingDomain:nil countryCode:@"UZ"], + [OWSCountryMetadata countryMetadataWithName:@"Saint Vincent and the Grenadines" + tld:@".vc" + frontingDomain:nil + countryCode:@"VC"], + [OWSCountryMetadata countryMetadataWithName:@"Venezuela" tld:@".ve" frontingDomain:nil countryCode:@"VE"], + [OWSCountryMetadata countryMetadataWithName:@"British Virgin Islands" + tld:@".vg" + frontingDomain:nil + countryCode:@"VG"], + [OWSCountryMetadata countryMetadataWithName:@"United States Virgin Islands" + tld:@".vi" + frontingDomain:nil + countryCode:@"VI"], + [OWSCountryMetadata countryMetadataWithName:@"Vietnam" tld:@".vn" frontingDomain:nil countryCode:@"VN"], + [OWSCountryMetadata countryMetadataWithName:@"Vanuatu" tld:@".vu" frontingDomain:nil countryCode:@"VU"], + [OWSCountryMetadata countryMetadataWithName:@"Samoa" tld:@".ws" frontingDomain:nil countryCode:@"WS"], + [OWSCountryMetadata countryMetadataWithName:@"South Africa" + tld:@".za" + frontingDomain:nil + countryCode:@"ZA"], + [OWSCountryMetadata countryMetadataWithName:@"Zambia" tld:@".zm" frontingDomain:nil countryCode:@"ZM"], + [OWSCountryMetadata countryMetadataWithName:@"Zimbabwe" tld:@".zw" frontingDomain:nil countryCode:@"ZW"], + ]; + cachedValue = [cachedValue sortedArrayUsingComparator:^NSComparisonResult( + OWSCountryMetadata *_Nonnull left, OWSCountryMetadata *_Nonnull right) { + return [left.localizedCountryName compare:right.localizedCountryName]; + }]; + }); + return cachedValue; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDevice.h b/SignalUtilitiesKit/OWSDevice.h new file mode 100644 index 000000000..9804c81ce --- /dev/null +++ b/SignalUtilitiesKit/OWSDevice.h @@ -0,0 +1,78 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +extern uint32_t const OWSDevicePrimaryDeviceId; + +@interface OWSDeviceManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)sharedManager; + +- (BOOL)mayHaveLinkedDevices:(YapDatabaseConnection *)dbConnection; +- (void)setMayHaveLinkedDevices; +- (void)clearMayHaveLinkedDevices; + +- (BOOL)hasReceivedSyncMessageInLastSeconds:(NSTimeInterval)intervalSeconds; +- (void)setHasReceivedSyncMessage; + +@end + +#pragma mark - + +@interface OWSDevice : TSYapDatabaseObject + +@property (nonatomic, readonly) NSInteger deviceId; +@property (nonatomic, readonly, nullable) NSString *name; +@property (nonatomic, readonly) NSDate *createdAt; +@property (nonatomic, readonly) NSDate *lastSeenAt; + ++ (nullable instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error; + ++ (NSArray *)currentDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction; + +/** + * Set local database of devices to `devices`. + * + * This will create missing devices, update existing devices, and delete stale devices. + * @param devices Removes any existing devices, replacing them with `devices` + * + * Returns YES if any devices were added or removed. + */ ++ (BOOL)replaceAll:(NSArray *)devices; + +/** + * The id of the device currently running this application + */ ++ (uint32_t)currentDeviceId; + +/** + * + * @param transaction yapTransaction + * @return + * If the user has any linked devices (apart from the device this app is running on). + */ ++ (BOOL)hasSecondaryDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction; + +- (NSString *)displayName; +- (BOOL)isPrimaryDevice; + +/** + * Assign attributes to this device from another. + * + * @param other + * OWSDevice whose attributes to copy to this device + * @return + * YES if any values on self changed, else NO + */ +- (BOOL)updateAttributesWithDevice:(OWSDevice *)other; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDevice.m b/SignalUtilitiesKit/OWSDevice.m new file mode 100644 index 000000000..7de3032ab --- /dev/null +++ b/SignalUtilitiesKit/OWSDevice.m @@ -0,0 +1,353 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDevice.h" +#import "OWSError.h" +#import "OWSPrimaryStorage.h" +#import "ProfileManagerProtocol.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "YapDatabaseConnection+OWS.h" +#import "YapDatabaseConnection.h" +#import "YapDatabaseTransaction.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +uint32_t const OWSDevicePrimaryDeviceId = 1; +NSString *const kOWSPrimaryStorage_OWSDeviceCollection = @"kTSStorageManager_OWSDeviceCollection"; +NSString *const kOWSPrimaryStorage_MayHaveLinkedDevices = @"kTSStorageManager_MayHaveLinkedDevices"; + +@interface OWSDeviceManager () + +@property (atomic) NSDate *lastReceivedSyncMessage; + +@end + +#pragma mark - + +@implementation OWSDeviceManager + ++ (instancetype)sharedManager +{ + static OWSDeviceManager *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] initDefault]; + }); + return instance; +} + +- (instancetype)initDefault +{ + return [super init]; +} + +- (BOOL)mayHaveLinkedDevices:(YapDatabaseConnection *)dbConnection +{ + OWSAssertDebug(dbConnection); + + return [dbConnection boolForKey:kOWSPrimaryStorage_MayHaveLinkedDevices + inCollection:kOWSPrimaryStorage_OWSDeviceCollection + defaultValue:YES]; +} + +// In order to avoid skipping necessary sync messages, the default value +// for mayHaveLinkedDevices is YES. Once we've successfully sent a +// sync message with no device messages (e.g. the service has confirmed +// that we have no linked devices), we can set mayHaveLinkedDevices to NO +// to avoid unnecessary message sends for sync messages until we learn +// of a linked device (e.g. through the device linking UI or by receiving +// a sync message, etc.). +- (void)clearMayHaveLinkedDevices +{ + // Note that we write async to avoid opening transactions within transactions. + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [transaction setObject:@(NO) + forKey:kOWSPrimaryStorage_MayHaveLinkedDevices + inCollection:kOWSPrimaryStorage_OWSDeviceCollection]; + }]; +} + +- (void)setMayHaveLinkedDevices +{ + // Note that we write async to avoid opening transactions within transactions. + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [transaction setObject:@(YES) + forKey:kOWSPrimaryStorage_MayHaveLinkedDevices + inCollection:kOWSPrimaryStorage_OWSDeviceCollection]; + }]; +} + +- (BOOL)hasReceivedSyncMessageInLastSeconds:(NSTimeInterval)intervalSeconds +{ + return (self.lastReceivedSyncMessage && fabs(self.lastReceivedSyncMessage.timeIntervalSinceNow) < intervalSeconds); +} + +- (void)setHasReceivedSyncMessage +{ + self.lastReceivedSyncMessage = [NSDate new]; + + [self setMayHaveLinkedDevices]; +} + +@end + +#pragma mark - + +@interface OWSDevice () + +@property (nonatomic) NSInteger deviceId; +@property (nonatomic, nullable) NSString *name; +@property (nonatomic) NSDate *createdAt; +@property (nonatomic) NSDate *lastSeenAt; + +@end + +#pragma mark - + +@implementation OWSDevice + +#pragma mark - Dependencies + ++ (id)profileManager +{ + return SSKEnvironment.shared.profileManager; +} + ++ (id)udManager +{ + return SSKEnvironment.shared.udManager; +} + ++ (TSAccountManager *)tsAccountManager +{ + return TSAccountManager.sharedInstance; +} + +- (OWSIdentityManager *)identityManager +{ + OWSAssertDebug(SSKEnvironment.shared.identityManager); + + return SSKEnvironment.shared.identityManager; +} + +#pragma mark - + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [super saveWithTransaction:transaction]; +} + ++ (nullable instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error +{ + OWSDevice *device = [MTLJSONAdapter modelOfClass:[self class] fromJSONDictionary:deviceAttributes error:error]; + if (device.deviceId < OWSDevicePrimaryDeviceId) { + OWSFailDebug(@"Invalid device id: %lu", (unsigned long)device.deviceId); + *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Invalid device id."); + return nil; + } + return device; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"createdAt": @"created", + @"lastSeenAt": @"lastSeen", + @"deviceId": @"id", + @"name": @"name" + }; +} + ++ (MTLValueTransformer *)createdAtJSONTransformer +{ + return self.millisecondTimestampToDateTransformer; +} + ++ (MTLValueTransformer *)lastSeenAtJSONTransformer +{ + return self.millisecondTimestampToDateTransformer; +} + ++ (NSArray *)currentDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *result = [NSMutableArray new]; + [transaction enumerateKeysAndObjectsInCollection:OWSDevice.collection + usingBlock:^(NSString *key, OWSDevice *object, BOOL *stop) { + if (![object isKindOfClass:[OWSDevice class]]) { + OWSFailDebug(@"Unexpected object in collection: %@", object.class); + return; + } + [result addObject:object]; + }]; + return result; +} + ++ (BOOL)replaceAll:(NSArray *)currentDevices +{ + BOOL didAddOrRemove = NO; + NSMutableArray *existingDevices = [[self allObjectsInCollection] mutableCopy]; + for (OWSDevice *currentDevice in currentDevices) { + NSUInteger existingDeviceIndex = [existingDevices indexOfObject:currentDevice]; + if (existingDeviceIndex == NSNotFound) { + // New Device + OWSLogInfo(@"Adding device: %@", currentDevice); + [currentDevice save]; + didAddOrRemove = YES; + } else { + OWSDevice *existingDevice = existingDevices[existingDeviceIndex]; + if ([existingDevice updateAttributesWithDevice:currentDevice]) { + [existingDevice save]; + } + [existingDevices removeObjectAtIndex:existingDeviceIndex]; + } + } + + // Since we removed existing devices as we went, only stale devices remain + for (OWSDevice *staleDevice in existingDevices) { + OWSLogVerbose(@"Removing device: %@", staleDevice); + [staleDevice remove]; + didAddOrRemove = YES; + } + + if (didAddOrRemove) { + dispatch_async(dispatch_get_main_queue(), ^{ + // Device changes can affect the UD access mode for a recipient, + // so we need to fetch the profile for this user to update UD access mode. + [self.profileManager fetchLocalUsersProfile]; + }); + return YES; + } else { + return NO; + } +} + ++ (MTLValueTransformer *)millisecondTimestampToDateTransformer +{ + static MTLValueTransformer *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError **error) { + if ([value isKindOfClass:[NSNumber class]]) { + NSNumber *number = (NSNumber *)value; + NSDate *result = [NSDate ows_dateWithMillisecondsSince1970:[number longLongValue]]; + if (result) { + *success = YES; + return result; + } + } + *success = NO; + OWSLogError(@"unable to decode date from %@", value); + *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, @"Unable to decode date from JSON."); + return nil; + } + reverseBlock:^id(id value, BOOL *success, NSError **error) { + if ([value isKindOfClass:[NSDate class]]) { + NSDate *date = (NSDate *)value; + NSNumber *result = [NSNumber numberWithLongLong:[NSDate ows_millisecondsSince1970ForDate:date]]; + if (result) { + *success = YES; + return result; + } + } + OWSLogError(@"unable to encode date from %@", value); + *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToEncodeJson, @"Unable to encode date to JSON."); + *success = NO; + return nil; + }]; + }); + return instance; +} + ++ (uint32_t)currentDeviceId +{ + // Someday it may be possible to have a non-primary iOS device, but for now + // any iOS device must be the primary device. + return OWSDevicePrimaryDeviceId; +} + +- (BOOL)isPrimaryDevice +{ + return self.deviceId == OWSDevicePrimaryDeviceId; +} + +- (NSString *)displayName +{ + if (self.name) { + ECKeyPair *_Nullable identityKeyPair = self.identityManager.identityKeyPair; + OWSAssertDebug(identityKeyPair); + if (identityKeyPair) { + NSError *error; + NSString *_Nullable decryptedName = + [DeviceNames decryptDeviceNameWithBase64String:self.name identityKeyPair:identityKeyPair error:&error]; + if (error) { + // Not necessarily an error; might be a legacy device name. + OWSLogError(@"Could not decrypt device name: %@", error); + } else if (decryptedName) { + return decryptedName; + } + } + + return self.name; + } + + if (self.deviceId == OWSDevicePrimaryDeviceId) { + return @"This Device"; + } + return NSLocalizedString(@"UNNAMED_DEVICE", @"Label text in device manager for a device with no name"); +} + +- (BOOL)updateAttributesWithDevice:(OWSDevice *)other +{ + BOOL changed = NO; + if (![self.lastSeenAt isEqual:other.lastSeenAt]) { + self.lastSeenAt = other.lastSeenAt; + changed = YES; + } + + if (![self.createdAt isEqual:other.createdAt]) { + self.createdAt = other.createdAt; + changed = YES; + } + + if (![self.name isEqual:other.name]) { + self.name = other.name; + changed = YES; + } + + return changed; +} + ++ (BOOL)hasSecondaryDevicesWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self numberOfKeysInCollectionWithTransaction:transaction] > 1; +} + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[OWSDevice class]]) { + return NO; + } + + return [self isEqualToDevice:(OWSDevice *)object]; +} + +- (BOOL)isEqualToDevice:(OWSDevice *)device +{ + return self.deviceId == device.deviceId; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDeviceProvisioner.h b/SignalUtilitiesKit/OWSDeviceProvisioner.h new file mode 100644 index 000000000..f9f93217f --- /dev/null +++ b/SignalUtilitiesKit/OWSDeviceProvisioner.h @@ -0,0 +1,38 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSDeviceProvisioningCodeService; +@class OWSDeviceProvisioningService; + +@interface OWSDeviceProvisioner : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithMyPublicKey:(NSData *)myPublicKey + myPrivateKey:(NSData *)myPrivateKey + theirPublicKey:(NSData *)theirPublicKey + theirEphemeralDeviceId:(NSString *)ephemeralDeviceId + accountIdentifier:(NSString *)accountIdentifier + profileKey:(NSData *)profileKey + readReceiptsEnabled:(BOOL)areReadReceiptsEnabled + provisioningCodeService:(OWSDeviceProvisioningCodeService *)provisioningCodeService + provisioningService:(OWSDeviceProvisioningService *)provisioningService NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithMyPublicKey:(NSData *)myPublicKey + myPrivateKey:(NSData *)myPrivateKey + theirPublicKey:(NSData *)theirPublicKey + theirEphemeralDeviceId:(NSString *)ephemeralDeviceId + accountIdentifier:(NSString *)accountIdentifier + profileKey:(NSData *)profileKey + readReceiptsEnabled:(BOOL)areReadReceiptsEnabled; + +- (void)provisionWithSuccess:(void (^)(void))successCallback failure:(void (^)(NSError *))failureCallback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDeviceProvisioner.m b/SignalUtilitiesKit/OWSDeviceProvisioner.m new file mode 100644 index 000000000..2deefb9b1 --- /dev/null +++ b/SignalUtilitiesKit/OWSDeviceProvisioner.m @@ -0,0 +1,123 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDeviceProvisioner.h" +#import "OWSDeviceProvisioningCodeService.h" +#import "OWSDeviceProvisioningService.h" +#import "OWSError.h" +#import "OWSProvisioningMessage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDeviceProvisioner () + +@property (nonatomic, readonly) NSData *myPublicKey; +@property (nonatomic, readonly) NSData *myPrivateKey; +@property (nonatomic, readonly) NSData *theirPublicKey; +@property (nonatomic, readonly) NSString *accountIdentifier; +@property (nonatomic, readonly) NSData *profileKey; +@property (nonatomic, nullable) NSString *ephemeralDeviceId; +@property (nonatomic, readonly) BOOL areReadReceiptsEnabled; +@property (nonatomic, readonly) OWSDeviceProvisioningCodeService *provisioningCodeService; +@property (nonatomic, readonly) OWSDeviceProvisioningService *provisioningService; + +@end + +@implementation OWSDeviceProvisioner + +- (instancetype)initWithMyPublicKey:(NSData *)myPublicKey + myPrivateKey:(NSData *)myPrivateKey + theirPublicKey:(NSData *)theirPublicKey + theirEphemeralDeviceId:(NSString *)ephemeralDeviceId + accountIdentifier:(NSString *)accountIdentifier + profileKey:(NSData *)profileKey + readReceiptsEnabled:(BOOL)areReadReceiptsEnabled + provisioningCodeService:(OWSDeviceProvisioningCodeService *)provisioningCodeService + provisioningService:(OWSDeviceProvisioningService *)provisioningService +{ + self = [super init]; + if (!self) { + return self; + } + + _myPublicKey = myPublicKey; + _myPrivateKey = myPrivateKey; + _theirPublicKey = theirPublicKey; + _accountIdentifier = accountIdentifier; + _profileKey = profileKey; + _ephemeralDeviceId = ephemeralDeviceId; + _areReadReceiptsEnabled = areReadReceiptsEnabled; + _provisioningCodeService = provisioningCodeService; + _provisioningService = provisioningService; + + return self; +} + +- (instancetype)initWithMyPublicKey:(NSData *)myPublicKey + myPrivateKey:(NSData *)myPrivateKey + theirPublicKey:(NSData *)theirPublicKey + theirEphemeralDeviceId:(NSString *)ephemeralDeviceId + accountIdentifier:(NSString *)accountIdentifier + profileKey:(NSData *)profileKey + readReceiptsEnabled:(BOOL)areReadReceiptsEnabled +{ + return [self initWithMyPublicKey:myPublicKey + myPrivateKey:myPrivateKey + theirPublicKey:theirPublicKey + theirEphemeralDeviceId:ephemeralDeviceId + accountIdentifier:accountIdentifier + profileKey:profileKey + readReceiptsEnabled:areReadReceiptsEnabled + provisioningCodeService:[OWSDeviceProvisioningCodeService new] + provisioningService:[OWSDeviceProvisioningService new]]; +} + +- (void)provisionWithSuccess:(void (^)(void))successCallback failure:(void (^)(NSError *_Nonnull))failureCallback +{ + [self.provisioningCodeService + requestProvisioningCodeWithSuccess:^(NSString *provisioningCode) { + OWSLogInfo(@"Retrieved provisioning code."); + [self provisionWithCode:provisioningCode success:successCallback failure:failureCallback]; + } + failure:^(NSError *error) { + OWSLogError(@"Failed to get provisioning code with error: %@", error); + failureCallback(error); + }]; +} + +- (void)provisionWithCode:(NSString *)provisioningCode + success:(void (^)(void))successCallback + failure:(void (^)(NSError *_Nonnull))failureCallback +{ + OWSProvisioningMessage *message = [[OWSProvisioningMessage alloc] initWithMyPublicKey:self.myPublicKey + myPrivateKey:self.myPrivateKey + theirPublicKey:self.theirPublicKey + accountIdentifier:self.accountIdentifier + profileKey:self.profileKey + readReceiptsEnabled:self.areReadReceiptsEnabled + provisioningCode:provisioningCode]; + + NSData *_Nullable messageBody = [message buildEncryptedMessageBody]; + if (messageBody == nil) { + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToEncryptMessage, @"Failed building provisioning message"); + failureCallback(error); + return; + } + + [self.provisioningService provisionWithMessageBody:messageBody + ephemeralDeviceId:self.ephemeralDeviceId + success:^{ + OWSLogInfo(@"ProvisioningService SUCCEEDED"); + successCallback(); + } + failure:^(NSError *error) { + OWSLogError(@"ProvisioningService FAILED with error:%@", error); + failureCallback(error); + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDeviceProvisioningCodeService.h b/SignalUtilitiesKit/OWSDeviceProvisioningCodeService.h new file mode 100644 index 000000000..bd0792f4b --- /dev/null +++ b/SignalUtilitiesKit/OWSDeviceProvisioningCodeService.h @@ -0,0 +1,18 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSNetworkManager; + +@interface OWSDeviceProvisioningCodeService : NSObject + +- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager NS_DESIGNATED_INITIALIZER; + +- (void)requestProvisioningCodeWithSuccess:(void (^)(NSString *))successCallback + failure:(void (^)(NSError *))failureCallback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDeviceProvisioningCodeService.m b/SignalUtilitiesKit/OWSDeviceProvisioningCodeService.m new file mode 100644 index 000000000..02d967355 --- /dev/null +++ b/SignalUtilitiesKit/OWSDeviceProvisioningCodeService.m @@ -0,0 +1,62 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDeviceProvisioningCodeService.h" +#import "OWSRequestFactory.h" +#import "TSNetworkManager.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSDeviceProvisioningCodeServiceProvisioningCodeKey = @"verificationCode"; + +@interface OWSDeviceProvisioningCodeService () + +@property (readonly) TSNetworkManager *networkManager; + +@end + +@implementation OWSDeviceProvisioningCodeService + +- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager +{ + + self = [super init]; + if (!self) { + return self; + } + + _networkManager = networkManager; + + return self; +} + +- (instancetype)init +{ + return [self initWithNetworkManager:[TSNetworkManager sharedManager]]; +} + +- (void)requestProvisioningCodeWithSuccess:(void (^)(NSString *))successCallback + failure:(void (^)(NSError *))failureCallback +{ + TSRequest *request = [OWSRequestFactory deviceProvisioningCodeRequest]; + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSLogVerbose(@"ProvisioningCode request succeeded"); + if ([(NSObject *)responseObject isKindOfClass:[NSDictionary class]]) { + NSDictionary *responseDict = (NSDictionary *)responseObject; + NSString *provisioningCode = + [responseDict objectForKey:OWSDeviceProvisioningCodeServiceProvisioningCodeKey]; + successCallback(provisioningCode); + } + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSLogVerbose(@"ProvisioningCode request failed with error: %@", error); + failureCallback(error); + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDeviceProvisioningService.h b/SignalUtilitiesKit/OWSDeviceProvisioningService.h new file mode 100644 index 000000000..fa28b686d --- /dev/null +++ b/SignalUtilitiesKit/OWSDeviceProvisioningService.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSNetworkManager; + +@interface OWSDeviceProvisioningService : NSObject + +- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager; + +- (void)provisionWithMessageBody:(NSData *)messageBody + ephemeralDeviceId:(NSString *)deviceId + success:(void (^)(void))successCallback + failure:(void (^)(NSError *))failureCallback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDeviceProvisioningService.m b/SignalUtilitiesKit/OWSDeviceProvisioningService.m new file mode 100644 index 000000000..4f8dca0e7 --- /dev/null +++ b/SignalUtilitiesKit/OWSDeviceProvisioningService.m @@ -0,0 +1,57 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDeviceProvisioningService.h" +#import "OWSRequestFactory.h" +#import "TSNetworkManager.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDeviceProvisioningService () + +@property (nonatomic, readonly) TSNetworkManager *networkManager; + +@end + +@implementation OWSDeviceProvisioningService + +- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager +{ + self = [super init]; + if (!self) { + return self; + } + + _networkManager = networkManager; + + return self; +} + +- (instancetype)init +{ + return [self initWithNetworkManager:[TSNetworkManager sharedManager]]; +} + +- (void)provisionWithMessageBody:(NSData *)messageBody + ephemeralDeviceId:(NSString *)deviceId + success:(void (^)(void))successCallback + failure:(void (^)(NSError *))failureCallback +{ + TSRequest *request = + [OWSRequestFactory deviceProvisioningRequestWithMessageBody:messageBody ephemeralDeviceId:deviceId]; + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSLogVerbose(@"Provisioning request succeeded"); + successCallback(); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSLogVerbose(@"Provisioning request failed with error: %@", error); + failureCallback(error); + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDevicesService.h b/SignalUtilitiesKit/OWSDevicesService.h new file mode 100644 index 000000000..eed731e2a --- /dev/null +++ b/SignalUtilitiesKit/OWSDevicesService.h @@ -0,0 +1,25 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const NSNotificationName_DeviceListUpdateSucceeded; +extern NSString *const NSNotificationName_DeviceListUpdateFailed; +extern NSString *const NSNotificationName_DeviceListUpdateModifiedDeviceList; + +@class OWSDevice; + +@interface OWSDevicesService : NSObject + ++ (void)refreshDevices; + ++ (void)unlinkDevice:(OWSDevice *)device + success:(void (^)(void))successCallback + failure:(void (^)(NSError *))failureCallback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDevicesService.m b/SignalUtilitiesKit/OWSDevicesService.m new file mode 100644 index 000000000..e84163d2c --- /dev/null +++ b/SignalUtilitiesKit/OWSDevicesService.m @@ -0,0 +1,126 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDevicesService.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSDevice.h" +#import "OWSError.h" +#import "OWSRequestFactory.h" +#import "TSNetworkManager.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const NSNotificationName_DeviceListUpdateSucceeded = @"NSNotificationName_DeviceListUpdateSucceeded"; +NSString *const NSNotificationName_DeviceListUpdateFailed = @"NSNotificationName_DeviceListUpdateFailed"; +NSString *const NSNotificationName_DeviceListUpdateModifiedDeviceList + = @"NSNotificationName_DeviceListUpdateModifiedDeviceList"; + +@implementation OWSDevicesService + ++ (void)refreshDevices +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self + getDevicesWithSuccess:^(NSArray *devices) { + // If we have more than one device; we may have a linked device. + if (devices.count > 1) { + // Setting this flag here shouldn't be necessary, but we do so + // because the "cost" is low and it will improve robustness. + [OWSDeviceManager.sharedManager setMayHaveLinkedDevices]; + } + + BOOL didAddOrRemove = [OWSDevice replaceAll:devices]; + + [NSNotificationCenter.defaultCenter + postNotificationNameAsync:NSNotificationName_DeviceListUpdateSucceeded + object:nil]; + + if (didAddOrRemove) { + [NSNotificationCenter.defaultCenter + postNotificationNameAsync:NSNotificationName_DeviceListUpdateModifiedDeviceList + object:nil]; + } + } + failure:^(NSError *error) { + OWSLogError(@"Request device list failed with error: %@", error); + + [NSNotificationCenter.defaultCenter postNotificationNameAsync:NSNotificationName_DeviceListUpdateFailed + object:error]; + }]; + }); +} + ++ (void)getDevicesWithSuccess:(void (^)(NSArray *))successCallback + failure:(void (^)(NSError *))failureCallback +{ + TSRequest *request = [OWSRequestFactory getDevicesRequest]; + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSLogVerbose(@"Get devices request succeeded"); + NSArray *devices = [self parseResponse:responseObject]; + + if (devices) { + successCallback(devices); + } else { + OWSLogError(@"unable to parse devices response:%@", responseObject); + NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + failureCallback(error); + } + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSLogVerbose(@"Get devices request failed with error: %@", error); + failureCallback(error); + }]; +} + ++ (void)unlinkDevice:(OWSDevice *)device + success:(void (^)(void))successCallback + failure:(void (^)(NSError *))failureCallback +{ + TSRequest *request = [OWSRequestFactory deleteDeviceRequestWithDevice:device]; + + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSLogVerbose(@"Delete device request succeeded"); + successCallback(); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSLogVerbose(@"Get devices request failed with error: %@", error); + failureCallback(error); + }]; +} + ++ (NSArray *)parseResponse:(id)responseObject +{ + if (![responseObject isKindOfClass:[NSDictionary class]]) { + OWSLogError(@"Device response was not a dictionary."); + return nil; + } + NSDictionary *response = (NSDictionary *)responseObject; + + NSArray *devicesAttributes = response[@"devices"]; + if (!devicesAttributes) { + OWSLogError(@"Device response had no devices."); + return nil; + } + + NSMutableArray *devices = [NSMutableArray new]; + for (NSDictionary *deviceAttributes in devicesAttributes) { + NSError *error; + OWSDevice *_Nullable device = [OWSDevice deviceFromJSONDictionary:deviceAttributes error:&error]; + if (error || !device) { + OWSLogError(@"Failed to build device from dictionary with error: %@", error); + } else { + [devices addObject:device]; + } + } + + return [devices copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.h b/SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.h new file mode 100644 index 000000000..b530124fa --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInfoMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSDisappearingMessagesConfiguration; +@class TSThread; + +@interface OWSDisappearingConfigurationUpdateInfoMessage : TSInfoMessage + +@property (nonatomic, readonly) BOOL configurationIsEnabled; + +/** + * @param remoteName is nil when created by the local user + */ +// MJK TODO - can we remove sendertimestamp here +- (instancetype)initWithTimestamp:(uint64_t)timestamp + thread:(TSThread *)thread + configuration:(OWSDisappearingMessagesConfiguration *)configuration + createdByRemoteName:(nullable NSString *)remoteName + createdInExistingGroup:(BOOL)createdInExistingGroup; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.m b/SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.m new file mode 100644 index 000000000..27c7add0b --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingConfigurationUpdateInfoMessage.m @@ -0,0 +1,95 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDisappearingConfigurationUpdateInfoMessage.h" +#import "NSString+SSK.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDisappearingConfigurationUpdateInfoMessage () + +@property (nonatomic, readonly, nullable) NSString *createdByRemoteName; +@property (nonatomic, readonly) BOOL createdInExistingGroup; +@property (nonatomic, readonly) uint32_t configurationDurationSeconds; + +@end + +@implementation OWSDisappearingConfigurationUpdateInfoMessage + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + thread:(TSThread *)thread + configuration:(OWSDisappearingMessagesConfiguration *)configuration + createdByRemoteName:(nullable NSString *)remoteName + createdInExistingGroup:(BOOL)createdInExistingGroup +{ + self = [super initWithTimestamp:timestamp inThread:thread messageType:TSInfoMessageTypeDisappearingMessagesUpdate]; + if (!self) { + return self; + } + + _configurationIsEnabled = configuration.isEnabled; + _configurationDurationSeconds = configuration.durationSeconds; + + // At most one should be set + OWSAssertDebug(!remoteName || !createdInExistingGroup); + + _createdByRemoteName = remoteName; + _createdInExistingGroup = createdInExistingGroup; + + return self; +} + +- (BOOL)shouldUseReceiptDateForSorting +{ + // Use the timestamp, not the "received at" timestamp to sort, + // since we're creating these interactions after the fact and back-dating them. + return NO; +} + +-(NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + if (self.createdInExistingGroup) { + OWSAssertDebug(self.configurationIsEnabled && self.configurationDurationSeconds > 0); + NSString *infoFormat = NSLocalizedString(@"DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT", + @"Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} " + @"before messages disappear, see the *_TIME_AMOUNT strings for context."); + + NSString *durationString = [NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO]; + return [NSString stringWithFormat:infoFormat, durationString]; + } else if (self.createdByRemoteName) { + if (self.configurationIsEnabled && self.configurationDurationSeconds > 0) { + NSString *infoFormat = NSLocalizedString(@"OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + @"Info Message when {{other user}} updates message expiration to {{time amount}}, see the " + @"*_TIME_AMOUNT " + @"strings for context."); + + NSString *durationString = + [NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO]; + return [NSString stringWithFormat:infoFormat, self.createdByRemoteName, durationString]; + } else { + NSString *infoFormat = NSLocalizedString(@"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", + @"Info Message when {{other user}} disables or doesn't support disappearing messages"); + return [NSString stringWithFormat:infoFormat, self.createdByRemoteName]; + } + } else { + // Changed by localNumber on this device or via synced transcript + if (self.configurationIsEnabled && self.configurationDurationSeconds > 0) { + NSString *infoFormat = NSLocalizedString(@"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + @"Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context."); + + NSString *durationString = + [NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO]; + return [NSString stringWithFormat:infoFormat, durationString]; + } else { + return NSLocalizedString(@"YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", + @"Info Message when you disable disappearing messages"); + } + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesConfiguration.h b/SignalUtilitiesKit/OWSDisappearingMessagesConfiguration.h new file mode 100644 index 000000000..00d2871a0 --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesConfiguration.h @@ -0,0 +1,34 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +#define OWSDisappearingMessagesConfigurationDefaultExpirationDuration kDayInterval + +@class YapDatabaseReadTransaction; + +@interface OWSDisappearingMessagesConfiguration : TSYapDatabaseObject + +- (instancetype)initDefaultWithThreadId:(NSString *)threadId; + +- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds; + +@property (nonatomic, getter=isEnabled) BOOL enabled; +@property (nonatomic) uint32_t durationSeconds; +@property (nonatomic, readonly) NSUInteger durationIndex; +@property (nonatomic, readonly) NSString *durationString; +@property (nonatomic, readonly) BOOL dictionaryValueDidChange; +@property (readonly, getter=isNewRecord) BOOL newRecord; + ++ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId + transaction:(YapDatabaseReadTransaction *)transaction; + ++ (NSArray *)validDurationsSeconds; ++ (uint32_t)maxDurationSeconds; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesConfiguration.m b/SignalUtilitiesKit/OWSDisappearingMessagesConfiguration.m new file mode 100644 index 000000000..a993ad808 --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesConfiguration.m @@ -0,0 +1,133 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDisappearingMessagesConfiguration.h" +#import "NSString+SSK.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDisappearingMessagesConfiguration () + +// Transient record lifecycle attributes. +@property (atomic) NSDictionary *originalDictionaryValue; +@property (atomic, getter=isNewRecord) BOOL newRecord; + +@end + +@implementation OWSDisappearingMessagesConfiguration + +- (instancetype)initDefaultWithThreadId:(NSString *)threadId +{ + return [self initWithThreadId:threadId + enabled:NO + durationSeconds:OWSDisappearingMessagesConfigurationDefaultExpirationDuration]; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + + _originalDictionaryValue = [self dictionaryValue]; + _newRecord = NO; + + return self; +} + +- (instancetype)initWithThreadId:(NSString *)threadId enabled:(BOOL)isEnabled durationSeconds:(uint32_t)seconds +{ + self = [super initWithUniqueId:threadId]; + if (!self) { + return self; + } + + _enabled = isEnabled; + _durationSeconds = seconds; + _newRecord = YES; + _originalDictionaryValue = self.dictionaryValue; + + return self; +} + ++ (instancetype)fetchOrBuildDefaultWithThreadId:(NSString *)threadId + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSDisappearingMessagesConfiguration *savedConfiguration = + [self fetchObjectWithUniqueID:threadId transaction:transaction]; + if (savedConfiguration) { + return savedConfiguration; + } else { + return [[self alloc] initDefaultWithThreadId:threadId]; + } +} + ++ (NSArray *)validDurationsSeconds +{ + return @[ + @(5 * kSecondInterval), + @(10 * kSecondInterval), + @(30 * kSecondInterval), + @(1 * kMinuteInterval), + @(5 * kMinuteInterval), + @(30 * kMinuteInterval), + @(1 * kHourInterval), + @(6 * kHourInterval), + @(12 * kHourInterval), + @(24 * kHourInterval), + @(1 * kWeekInterval) + ]; +} + ++ (uint32_t)maxDurationSeconds +{ + static uint32_t max; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + max = [[self.validDurationsSeconds valueForKeyPath:@"@max.intValue"] unsignedIntValue]; + + // It's safe to update this assert if we add a larger duration + OWSAssertDebug(max == 1 * kWeekInterval); + }); + + return max; +} + +- (NSUInteger)durationIndex +{ + return [[self.class validDurationsSeconds] indexOfObject:@(self.durationSeconds)]; +} + +- (NSString *)durationString +{ + return [NSString formatDurationSeconds:self.durationSeconds useShortFormat:NO]; +} + +#pragma mark - Dirty Tracking + ++ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey +{ + // Don't persist transient properties + if ([propertyKey isEqualToString:@"originalDictionaryValue"] + ||[propertyKey isEqualToString:@"newRecord"]) { + return MTLPropertyStorageNone; + } else { + return [super storageBehaviorForPropertyWithKey:propertyKey]; + } +} + +- (BOOL)dictionaryValueDidChange +{ + return ![self.originalDictionaryValue isEqual:[self dictionaryValue]]; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [super saveWithTransaction:transaction]; + self.originalDictionaryValue = [self dictionaryValue]; + self.newRecord = NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesConfigurationMessage.h b/SignalUtilitiesKit/OWSDisappearingMessagesConfigurationMessage.h new file mode 100644 index 000000000..7c96e20f2 --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesConfigurationMessage.h @@ -0,0 +1,30 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSDisappearingMessagesConfiguration; + +@interface OWSDisappearingMessagesConfigurationMessage : TSOutgoingMessage + +// MJK TODO - remove senderTimestamp +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithConfiguration:(OWSDisappearingMessagesConfiguration *)configuration thread:(TSThread *)thread; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesConfigurationMessage.m b/SignalUtilitiesKit/OWSDisappearingMessagesConfigurationMessage.m new file mode 100644 index 000000000..e6d539971 --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesConfigurationMessage.m @@ -0,0 +1,72 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSDisappearingMessagesConfigurationMessage.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDisappearingMessagesConfigurationMessage () + +@property (nonatomic, readonly) OWSDisappearingMessagesConfiguration *configuration; + +@end + +#pragma mark - + +@implementation OWSDisappearingMessagesConfigurationMessage + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeDisappearingMessagesConfiguration]; } + +- (instancetype)initWithConfiguration:(OWSDisappearingMessagesConfiguration *)configuration thread:(TSThread *)thread +{ + // MJK TODO - remove sender timestamp + self = [super initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + if (!self) { + return self; + } + + _configuration = configuration; + + return self; +} + + +- (nullable id)dataMessageBuilder +{ + SSKProtoDataMessageBuilder *_Nullable dataMessageBuilder = [super dataMessageBuilder]; + if (!dataMessageBuilder) { + return nil; + } + [dataMessageBuilder setTimestamp:self.timestamp]; + [dataMessageBuilder setFlags:SSKProtoDataMessageFlagsExpirationTimerUpdate]; + if (self.configuration.isEnabled) { + [dataMessageBuilder setExpireTimer:self.configuration.durationSeconds]; + } else { + [dataMessageBuilder setExpireTimer:0]; + } + + return dataMessageBuilder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesFinder.h b/SignalUtilitiesKit/OWSDisappearingMessagesFinder.h new file mode 100644 index 000000000..5ba154856 --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesFinder.h @@ -0,0 +1,47 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class OWSStorage; +@class TSMessage; +@class TSThread; +@class YapDatabaseReadTransaction; + +@interface OWSDisappearingMessagesFinder : NSObject + +- (void)enumerateExpiredMessagesWithBlock:(void (^_Nonnull)(TSMessage *message))block + transaction:(YapDatabaseReadTransaction *)transaction; + +- (void)enumerateUnstartedExpiringMessagesInThread:(TSThread *)thread + block:(void (^_Nonnull)(TSMessage *message))block + transaction:(YapDatabaseReadTransaction *)transaction; + +- (void)enumerateMessagesWhichFailedToStartExpiringWithBlock:(void (^_Nonnull)(TSMessage *message))block + transaction:(YapDatabaseReadTransaction *)transaction; + +/** + * @return + * uint64_t millisecond timestamp wrapped in a number. Retrieve with `unsignedLongLongvalue`. + * or nil if there are no upcoming expired messages + */ +- (nullable NSNumber *)nextExpirationTimestampWithTransaction:(YapDatabaseReadTransaction *_Nonnull)transaction; + ++ (NSString *)databaseExtensionName; + ++ (void)asyncRegisterDatabaseExtensions:(OWSStorage *)storage; + +#ifdef DEBUG +/** + * Only use the sync version for testing, generally we'll want to register extensions async + */ ++ (void)blockingRegisterDatabaseExtensions:(OWSStorage *)storage; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesFinder.m b/SignalUtilitiesKit/OWSDisappearingMessagesFinder.m new file mode 100644 index 000000000..a319141e7 --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesFinder.m @@ -0,0 +1,270 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDisappearingMessagesFinder.h" +#import "OWSPrimaryStorage.h" +#import "TSIncomingMessage.h" +#import "TSMessage.h" +#import "TSOutgoingMessage.h" +#import "TSThread.h" +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const OWSDisappearingMessageFinderThreadIdColumn = @"thread_id"; +static NSString *const OWSDisappearingMessageFinderExpiresAtColumn = @"expires_at"; +static NSString *const OWSDisappearingMessageFinderExpiresAtIndex = @"index_messages_on_expires_at_and_thread_id_v2"; + +@implementation OWSDisappearingMessagesFinder + +- (NSArray *)fetchUnstartedExpiringMessageIdsInThread:(TSThread *)thread + transaction:(YapDatabaseReadTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *messageIds = [NSMutableArray new]; + NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ = 0 AND %@ = \"%@\"", + OWSDisappearingMessageFinderExpiresAtColumn, + OWSDisappearingMessageFinderThreadIdColumn, + thread.uniqueId]; + + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:OWSDisappearingMessageFinderExpiresAtIndex] + enumerateKeysMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, BOOL *stop) { + [messageIds addObject:key]; + }]; + + return [messageIds copy]; +} + +- (NSArray *)fetchMessageIdsWhichFailedToStartExpiring:(YapDatabaseReadTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *messageIds = [NSMutableArray new]; + NSString *formattedString = + [NSString stringWithFormat:@"WHERE %@ = 0", OWSDisappearingMessageFinderExpiresAtColumn]; + + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:OWSDisappearingMessageFinderExpiresAtIndex] + enumerateKeysAndObjectsMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, id object, BOOL *stop) { + if (![object isKindOfClass:[TSMessage class]]) { + OWSFailDebug(@"Object was unexpected class: %@", [object class]); + return; + } + + // We'll need to update if we ever support expiring other message types + OWSAssertDebug([object isKindOfClass:[TSOutgoingMessage class]] || [object isKindOfClass:[TSIncomingMessage class]]); + + TSMessage *message = (TSMessage *)object; + if ([message shouldStartExpireTimerWithTransaction:transaction]) { + if ([message isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message; + if (!incomingMessage.wasRead) { + return; + } + } + [messageIds addObject:key]; + } + }]; + + return [messageIds copy]; +} + +- (NSArray *)fetchExpiredMessageIdsWithTransaction:(YapDatabaseReadTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *messageIds = [NSMutableArray new]; + + uint64_t now = [NSDate ows_millisecondTimeStamp]; + // When (expiresAt == 0) the message SHOULD NOT expire. Careful ;) + NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ > 0 AND %@ <= %lld", + OWSDisappearingMessageFinderExpiresAtColumn, + OWSDisappearingMessageFinderExpiresAtColumn, + now]; + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:OWSDisappearingMessageFinderExpiresAtIndex] + enumerateKeysMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, BOOL *stop) { + [messageIds addObject:key]; + }]; + + return [messageIds copy]; +} + +- (nullable NSNumber *)nextExpirationTimestampWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ > 0 ORDER BY %@ ASC", + OWSDisappearingMessageFinderExpiresAtColumn, + OWSDisappearingMessageFinderExpiresAtColumn]; + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + + __block TSMessage *firstMessage; + [[transaction ext:OWSDisappearingMessageFinderExpiresAtIndex] + enumerateKeysAndObjectsMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, id object, BOOL *stop) { + firstMessage = (TSMessage *)object; + *stop = YES; + }]; + + if (firstMessage && firstMessage.expiresAt > 0) { + return [NSNumber numberWithUnsignedLongLong:firstMessage.expiresAt]; + } + + return nil; +} + +- (void)enumerateUnstartedExpiringMessagesInThread:(TSThread *)thread + block:(void (^_Nonnull)(TSMessage *message))block + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + for (NSString *expiringMessageId in + [self fetchUnstartedExpiringMessageIdsInThread:thread transaction:transaction]) { + TSMessage *_Nullable message = [TSMessage fetchObjectWithUniqueID:expiringMessageId transaction:transaction]; + if ([message isKindOfClass:[TSMessage class]]) { + block(message); + } else { + OWSFailDebug(@"unexpected object: %@", [message class]); + } + } +} + +- (void)enumerateMessagesWhichFailedToStartExpiringWithBlock:(void (^_Nonnull)(TSMessage *message))block + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + for (NSString *expiringMessageId in [self fetchMessageIdsWhichFailedToStartExpiring:transaction]) { + + TSMessage *_Nullable message = [TSMessage fetchObjectWithUniqueID:expiringMessageId transaction:transaction]; + if (![message isKindOfClass:[TSMessage class]]) { + OWSFailDebug(@"unexpected object: %@", [message class]); + continue; + } + + if (![message shouldStartExpireTimerWithTransaction:transaction]) { + OWSFailDebug(@"object: %@ shouldn't expire.", message); + continue; + } + + block(message); + } +} + +/** + * Don't use this in production. Useful for testing. + * We don't want to instantiate potentially many messages at once. + */ +- (NSArray *)fetchUnstartedExpiringMessagesInThread:(TSThread *)thread + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *messages = [NSMutableArray new]; + [self enumerateUnstartedExpiringMessagesInThread:thread + block:^(TSMessage *message) { + [messages addObject:message]; + } + transaction:transaction]; + + return [messages copy]; +} + + +- (void)enumerateExpiredMessagesWithBlock:(void (^_Nonnull)(TSMessage *message))block + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + // Since we can't directly mutate the enumerated expired messages, we store only their ids in hopes of saving a + // little memory and then enumerate the (larger) TSMessage objects one at a time. + for (NSString *expiredMessageId in [self fetchExpiredMessageIdsWithTransaction:transaction]) { + TSMessage *_Nullable message = [TSMessage fetchObjectWithUniqueID:expiredMessageId transaction:transaction]; + if ([message isKindOfClass:[TSMessage class]]) { + block(message); + } else { + OWSLogError(@"unexpected object: %@", message); + } + } +} + +/** + * Don't use this in production. Useful for testing. + * We don't want to instantiate potentially many messages at once. + */ +- (NSArray *)fetchExpiredMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *messages = [NSMutableArray new]; + [self enumerateExpiredMessagesWithBlock:^(TSMessage *message) { + [messages addObject:message]; + } + transaction:transaction]; + + return [messages copy]; +} + +#pragma mark - YapDatabaseExtension + ++ (YapDatabaseSecondaryIndex *)indexDatabaseExtension +{ + YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new]; + [setup addColumn:OWSDisappearingMessageFinderExpiresAtColumn withType:YapDatabaseSecondaryIndexTypeInteger]; + [setup addColumn:OWSDisappearingMessageFinderThreadIdColumn withType:YapDatabaseSecondaryIndexTypeText]; + + YapDatabaseSecondaryIndexHandler *handler = + [YapDatabaseSecondaryIndexHandler withObjectBlock:^(YapDatabaseReadTransaction *transaction, + NSMutableDictionary *dict, + NSString *collection, + NSString *key, + id object) { + if (![object isKindOfClass:[TSMessage class]]) { + return; + } + TSMessage *message = (TSMessage *)object; + + if (![message shouldStartExpireTimerWithTransaction:transaction]) { + return; + } + + dict[OWSDisappearingMessageFinderExpiresAtColumn] = @(message.expiresAt); + dict[OWSDisappearingMessageFinderThreadIdColumn] = message.uniqueThreadId; + }]; + + return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:@"1"]; +} + +#ifdef DEBUG +// Useful for tests, don't use in app startup path because it's slow. ++ (void)blockingRegisterDatabaseExtensions:(OWSStorage *)storage +{ + [storage registerExtension:[self indexDatabaseExtension] withName:OWSDisappearingMessageFinderExpiresAtIndex]; +} +#endif + ++ (NSString *)databaseExtensionName +{ + return OWSDisappearingMessageFinderExpiresAtIndex; +} + ++ (void)asyncRegisterDatabaseExtensions:(OWSStorage *)storage +{ + [storage asyncRegisterExtension:[self indexDatabaseExtension] withName:OWSDisappearingMessageFinderExpiresAtIndex]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesJob.h b/SignalUtilitiesKit/OWSDisappearingMessagesJob.h new file mode 100644 index 000000000..26ad356f6 --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesJob.h @@ -0,0 +1,56 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class TSMessage; +@class TSThread; +@class YapDatabaseReadWriteTransaction; + +@protocol ContactsManagerProtocol; + +@interface OWSDisappearingMessagesJob : NSObject + ++ (instancetype)sharedJob; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + +- (void)startAnyExpirationForMessage:(TSMessage *)message + expirationStartedAt:(uint64_t)expirationStartedAt + transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction; + +/** + * Synchronize our disappearing messages settings with that of the given message. Useful so we can + * become eventually consistent with remote senders. + * + * @param duration + * Can be 0, indicating a non-expiring message, or greater, indicating an expiring message. We match the expiration + * timer of the message, including disabling expiring messages if the message is not an expiring message. + * + * @param remoteRecipientId + * nil for outgoing messages, otherwise the recipientId of the sender + * + * @param createdInExistingGroup + * YES when being added to a group which already has DM enabled, otherwise NO + */ +- (void)becomeConsistentWithDisappearingDuration:(uint32_t)duration + thread:(TSThread *)thread + createdByRemoteRecipientId:(nullable NSString *)remoteRecipientId + createdInExistingGroup:(BOOL)createdInExistingGroup + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// Clean up any messages that expired since last launch immediately +// and continue cleaning in the background. +- (void)startIfNecessary; + +- (void)cleanupMessagesWhichFailedToStartExpiringWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDisappearingMessagesJob.m b/SignalUtilitiesKit/OWSDisappearingMessagesJob.m new file mode 100644 index 000000000..ef29f762e --- /dev/null +++ b/SignalUtilitiesKit/OWSDisappearingMessagesJob.m @@ -0,0 +1,424 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDisappearingMessagesJob.h" +#import "AppContext.h" +#import "AppReadiness.h" +#import "ContactsManagerProtocol.h" +#import "NSTimer+OWS.h" +#import "OWSBackgroundTask.h" +#import "OWSDisappearingConfigurationUpdateInfoMessage.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import "OWSDisappearingMessagesFinder.h" +#import "OWSPrimaryStorage.h" +#import "SSKEnvironment.h" +#import "TSIncomingMessage.h" +#import "TSMessage.h" +#import "TSThread.h" +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +// Can we move to Signal-iOS? +@interface OWSDisappearingMessagesJob () + +@property (nonatomic, readonly) YapDatabaseConnection *databaseConnection; + +@property (nonatomic, readonly) OWSDisappearingMessagesFinder *disappearingMessagesFinder; + ++ (dispatch_queue_t)serialQueue; + +// These three properties should only be accessed on the main thread. +@property (nonatomic) BOOL hasStarted; +@property (nonatomic, nullable) NSTimer *nextDisappearanceTimer; +@property (nonatomic, nullable) NSDate *nextDisappearanceDate; +@property (nonatomic, nullable) NSTimer *fallbackTimer; + +@end + +void AssertIsOnDisappearingMessagesQueue() +{ +#ifdef DEBUG + if (@available(iOS 10.0, *)) { + dispatch_assert_queue(OWSDisappearingMessagesJob.serialQueue); + } +#endif +} + +#pragma mark - + +@implementation OWSDisappearingMessagesJob + ++ (instancetype)sharedJob +{ + OWSAssertDebug(SSKEnvironment.shared.disappearingMessagesJob); + + return SSKEnvironment.shared.disappearingMessagesJob; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _databaseConnection = primaryStorage.newDatabaseConnection; + _disappearingMessagesFinder = [OWSDisappearingMessagesFinder new]; + + // suspenders in case a deletion schedule is missed. + NSTimeInterval kFallBackTimerInterval = 5 * kMinuteInterval; + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + if (CurrentAppContext().isMainApp) { + self.fallbackTimer = [NSTimer weakScheduledTimerWithTimeInterval:kFallBackTimerInterval + target:self + selector:@selector(fallbackTimerDidFire) + userInfo:nil + repeats:YES]; + } + }]; + + OWSSingletonAssert(); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:OWSApplicationDidBecomeActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResignActive:) + name:OWSApplicationWillResignActiveNotification + object:nil]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + ++ (dispatch_queue_t)serialQueue +{ + static dispatch_queue_t queue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("org.whispersystems.disappearing.messages", DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + +#pragma mark - Dependencies + +- (id)contactsManager +{ + return SSKEnvironment.shared.contactsManager; +} + +#pragma mark - + +- (NSUInteger)deleteExpiredMessages +{ + AssertIsOnDisappearingMessagesQueue(); + + uint64_t now = [NSDate ows_millisecondTimeStamp]; + + OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + + __block NSUInteger expirationCount = 0; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self.disappearingMessagesFinder enumerateExpiredMessagesWithBlock:^(TSMessage *message) { + // sanity check + if (message.expiresAt > now) { + OWSFailDebug(@"Refusing to remove message which doesn't expire until: %lld", message.expiresAt); + return; + } + + OWSLogInfo(@"Removing message which expired at: %lld", message.expiresAt); + [message removeWithTransaction:transaction]; + expirationCount++; + } + transaction:transaction]; + }]; + + OWSLogDebug(@"Removed %lu expired messages", (unsigned long)expirationCount); + + OWSAssertDebug(backgroundTask); + backgroundTask = nil; + return expirationCount; +} + +// deletes any expired messages and schedules the next run. +- (NSUInteger)runLoop +{ + OWSLogVerbose(@"in runLoop"); + AssertIsOnDisappearingMessagesQueue(); + + NSUInteger deletedCount = [self deleteExpiredMessages]; + + __block NSNumber *nextExpirationTimestampNumber; + [self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + nextExpirationTimestampNumber = + [self.disappearingMessagesFinder nextExpirationTimestampWithTransaction:transaction]; + }]; + + if (!nextExpirationTimestampNumber) { + OWSLogDebug(@"No more expiring messages."); + return deletedCount; + } + + uint64_t nextExpirationAt = nextExpirationTimestampNumber.unsignedLongLongValue; + NSDate *nextEpirationDate = [NSDate ows_dateWithMillisecondsSince1970:nextExpirationAt]; + [self scheduleRunByDate:nextEpirationDate]; + + return deletedCount; +} + +- (void)startAnyExpirationForMessage:(TSMessage *)message + expirationStartedAt:(uint64_t)expirationStartedAt + transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + if (!message.isExpiringMessage) { + return; + } + + NSTimeInterval startedSecondsAgo = ([NSDate ows_millisecondTimeStamp] - expirationStartedAt) / 1000.0; + OWSLogDebug(@"Starting expiration for message read %f seconds ago", startedSecondsAgo); + + // Don't clobber if multiple actions simultaneously triggered expiration. + if (message.expireStartedAt == 0 || message.expireStartedAt > expirationStartedAt) { + [message updateWithExpireStartedAt:expirationStartedAt transaction:transaction]; + } + + [transaction addCompletionQueue:nil + completionBlock:^{ + // Necessary that the async expiration run happens *after* the message is saved with it's new + // expiration configuration. + [self scheduleRunByDate:[NSDate ows_dateWithMillisecondsSince1970:message.expiresAt]]; + }]; +} + +#pragma mark - Apply Remote Configuration + +- (void)becomeConsistentWithDisappearingDuration:(uint32_t)duration + thread:(TSThread *)thread + createdByRemoteRecipientId:(nullable NSString *)remoteRecipientId + createdInExistingGroup:(BOOL)createdInExistingGroup + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(thread); + OWSAssertDebug(transaction); + + OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + + NSString *_Nullable remoteContactName = nil; + if (remoteRecipientId) { + remoteContactName = [self.contactsManager displayNameForPhoneIdentifier:remoteRecipientId + transaction:transaction]; + } + + // Become eventually consistent in the case that the remote changed their settings at the same time. + // Also in case remote doesn't support expiring messages + OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration = + [thread disappearingMessagesConfigurationWithTransaction:transaction]; + + if (duration == 0) { + disappearingMessagesConfiguration.enabled = NO; + } else { + disappearingMessagesConfiguration.enabled = YES; + disappearingMessagesConfiguration.durationSeconds = duration; + } + + if (!disappearingMessagesConfiguration.dictionaryValueDidChange) { + return; + } + + OWSLogInfo(@"becoming consistent with disappearing message configuration: %@", + disappearingMessagesConfiguration.dictionaryValue); + + [disappearingMessagesConfiguration saveWithTransaction:transaction]; + + // MJK TODO - should be safe to remove this senderTimestamp + OWSDisappearingConfigurationUpdateInfoMessage *infoMessage = + [[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + thread:thread + configuration:disappearingMessagesConfiguration + createdByRemoteName:remoteContactName + createdInExistingGroup:createdInExistingGroup]; + [infoMessage saveWithTransaction:transaction]; + + OWSAssertDebug(backgroundTask); + backgroundTask = nil; +} + +#pragma mark - + +- (void)startIfNecessary +{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.hasStarted) { + return; + } + self.hasStarted = YES; + + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + // Theoretically this shouldn't be necessary, but there was a race condition when receiving a backlog + // of messages across timer changes which could cause a disappearing message's timer to never be started. + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self cleanupMessagesWhichFailedToStartExpiringWithTransaction:transaction]; + }]; + + [self runLoop]; + }); + }); +} + +- (NSDateFormatter *)dateFormatter +{ + static NSDateFormatter *dateFormatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [NSDateFormatter new]; + dateFormatter.dateStyle = NSDateFormatterNoStyle; + dateFormatter.timeStyle = kCFDateFormatterMediumStyle; + dateFormatter.locale = [NSLocale systemLocale]; + }); + + return dateFormatter; +} + +- (void)scheduleRunByDate:(NSDate *)date +{ + OWSAssertDebug(date); + + dispatch_async(dispatch_get_main_queue(), ^{ + if (!CurrentAppContext().isMainAppAndActive) { + // Don't schedule run when inactive or not in main app. + return; + } + + // Don't run more often than once per second. + const NSTimeInterval kMinDelaySeconds = 1.0; + NSTimeInterval delaySeconds = MAX(kMinDelaySeconds, date.timeIntervalSinceNow); + NSDate *newTimerScheduleDate = [NSDate dateWithTimeIntervalSinceNow:delaySeconds]; + if (self.nextDisappearanceDate && [self.nextDisappearanceDate isBeforeDate:newTimerScheduleDate]) { + OWSLogVerbose(@"Request to run at %@ (%d sec.) ignored due to earlier scheduled run at %@ (%d sec.)", + [self.dateFormatter stringFromDate:date], + (int)round(MAX(0, [date timeIntervalSinceDate:[NSDate new]])), + [self.dateFormatter stringFromDate:self.nextDisappearanceDate], + (int)round(MAX(0, [self.nextDisappearanceDate timeIntervalSinceDate:[NSDate new]]))); + return; + } + + // Update Schedule + OWSLogVerbose(@"Scheduled run at %@ (%d sec.)", + [self.dateFormatter stringFromDate:newTimerScheduleDate], + (int)round(MAX(0, [newTimerScheduleDate timeIntervalSinceDate:[NSDate new]]))); + [self resetNextDisappearanceTimer]; + self.nextDisappearanceDate = newTimerScheduleDate; + self.nextDisappearanceTimer = [NSTimer weakScheduledTimerWithTimeInterval:delaySeconds + target:self + selector:@selector(disappearanceTimerDidFire) + userInfo:nil + repeats:NO]; + }); +} + +- (void)disappearanceTimerDidFire +{ + OWSAssertIsOnMainThread(); + OWSLogDebug(@""); + + if (!CurrentAppContext().isMainAppAndActive) { + // Don't schedule run when inactive or not in main app. + OWSFailDebug(@"Disappearing messages job timer fired while main app inactive."); + return; + } + + [self resetNextDisappearanceTimer]; + + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + [self runLoop]; + }); +} + +- (void)fallbackTimerDidFire +{ + OWSAssertIsOnMainThread(); + OWSLogDebug(@""); + + BOOL recentlyScheduledDisappearanceTimer = NO; + if (fabs(self.nextDisappearanceDate.timeIntervalSinceNow) < 1.0) { + recentlyScheduledDisappearanceTimer = YES; + } + + if (!CurrentAppContext().isMainAppAndActive) { + OWSLogInfo(@"Ignoring fallbacktimer for app which is not main and active."); + return; + } + + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + NSUInteger deletedCount = [self runLoop]; + + // Normally deletions should happen via the disappearanceTimer, to make sure that they're prompt. + // So, if we're deleting something via this fallback timer, something may have gone wrong. The + // exception is if we're in close proximity to the disappearanceTimer, in which case a race condition + // is inevitable. + if (!recentlyScheduledDisappearanceTimer && deletedCount > 0) { + OWSFailDebug(@"unexpectedly deleted disappearing messages via fallback timer."); + } + }); +} + +- (void)resetNextDisappearanceTimer +{ + OWSAssertIsOnMainThread(); + + [self.nextDisappearanceTimer invalidate]; + self.nextDisappearanceTimer = nil; + self.nextDisappearanceDate = nil; +} + +#pragma mark - Cleanup + +- (void)cleanupMessagesWhichFailedToStartExpiringWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self.disappearingMessagesFinder + enumerateMessagesWhichFailedToStartExpiringWithBlock:^(TSMessage *_Nonnull message) { + OWSFailDebug(@"starting old timer for message timestamp: %lu", (unsigned long)message.timestamp); + + // We don't know when it was actually read, so assume it was read as soon as it was received. + uint64_t readTimeBestGuess = message.receivedAtTimestamp; + [self startAnyExpirationForMessage:message expirationStartedAt:readTimeBestGuess transaction:transaction]; + } + transaction:transaction]; +} + +#pragma mark - Notifications + +- (void)applicationDidBecomeActive:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + dispatch_async(OWSDisappearingMessagesJob.serialQueue, ^{ + [self runLoop]; + }); + }]; +} + +- (void)applicationWillResignActive:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [self resetNextDisappearanceTimer]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDispatch.h b/SignalUtilitiesKit/OWSDispatch.h new file mode 100644 index 000000000..ef0dccd93 --- /dev/null +++ b/SignalUtilitiesKit/OWSDispatch.h @@ -0,0 +1,23 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDispatch : NSObject + +/** + * Attachment downloading + */ ++ (dispatch_queue_t)attachmentsQueue; + +/** + * Serial message sending queue + */ ++ (dispatch_queue_t)sendingQueue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDispatch.m b/SignalUtilitiesKit/OWSDispatch.m new file mode 100644 index 000000000..5f300a2f9 --- /dev/null +++ b/SignalUtilitiesKit/OWSDispatch.m @@ -0,0 +1,34 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSDispatch.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSDispatch + ++ (dispatch_queue_t)attachmentsQueue +{ + static dispatch_once_t onceToken; + static dispatch_queue_t queue; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("org.whispersystems.signal.attachments", NULL); + }); + return queue; +} + ++ (dispatch_queue_t)sendingQueue +{ + static dispatch_once_t onceToken; + static dispatch_queue_t queue; + dispatch_once(&onceToken, ^{ + dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); + queue = dispatch_queue_create("org.whispersystems.signal.sendQueue", attributes); + }); + return queue; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDynamicOutgoingMessage.h b/SignalUtilitiesKit/OWSDynamicOutgoingMessage.h new file mode 100644 index 000000000..655eda029 --- /dev/null +++ b/SignalUtilitiesKit/OWSDynamicOutgoingMessage.h @@ -0,0 +1,36 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoDataMessageBuilder; +@class SignalRecipient; + +typedef NSData *_Nonnull (^DynamicOutgoingMessageBlock)(SignalRecipient *); + +/// This class is only used in debug tools +@interface OWSDynamicOutgoingMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithPlainTextDataBlock:(DynamicOutgoingMessageBlock)block thread:(nullable TSThread *)thread; +- (instancetype)initWithPlainTextDataBlock:(DynamicOutgoingMessageBlock)block + timestamp:(uint64_t)timestamp + thread:(nullable TSThread *)thread; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSDynamicOutgoingMessage.m b/SignalUtilitiesKit/OWSDynamicOutgoingMessage.m new file mode 100644 index 000000000..8b760523b --- /dev/null +++ b/SignalUtilitiesKit/OWSDynamicOutgoingMessage.m @@ -0,0 +1,64 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSDynamicOutgoingMessage.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDynamicOutgoingMessage () + +@property (nonatomic, readonly) DynamicOutgoingMessageBlock block; + +@end + +#pragma mark - + +@implementation OWSDynamicOutgoingMessage + +- (instancetype)initWithPlainTextDataBlock:(DynamicOutgoingMessageBlock)block thread:(nullable TSThread *)thread +{ + return [self initWithPlainTextDataBlock:block timestamp:[NSDate ows_millisecondTimeStamp] thread:thread]; +} + +// MJK TODO can we remove sender timestamp? +- (instancetype)initWithPlainTextDataBlock:(DynamicOutgoingMessageBlock)block + timestamp:(uint64_t)timestamp + thread:(nullable TSThread *)thread +{ + self = [super initOutgoingMessageWithTimestamp:timestamp + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + + if (self) { + _block = block; + } + + return self; +} + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient +{ + NSData *plainTextData = self.block(recipient); + OWSAssertDebug(plainTextData); + return plainTextData; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSEndSessionMessage.h b/SignalUtilitiesKit/OWSEndSessionMessage.h new file mode 100644 index 000000000..cd45fc648 --- /dev/null +++ b/SignalUtilitiesKit/OWSEndSessionMessage.h @@ -0,0 +1,30 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(EndSessionMessage) +@interface OWSEndSessionMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +// MJK TODO can we remove the sender timestamp? +- (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSEndSessionMessage.m b/SignalUtilitiesKit/OWSEndSessionMessage.m new file mode 100644 index 000000000..c0bd12c5d --- /dev/null +++ b/SignalUtilitiesKit/OWSEndSessionMessage.m @@ -0,0 +1,74 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSEndSessionMessage.h" +#import "OWSPrimaryStorage+Loki.h" +#import "SignalRecipient.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSEndSessionMessage + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread +{ + return [super initOutgoingMessageWithTimestamp:timestamp + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; +} + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeEphemeral]; } + +- (nullable id)dataMessageBuilder +{ + SSKProtoDataMessageBuilder *_Nullable builder = [super dataMessageBuilder]; + if (!builder) { + return nil; + } + [builder setTimestamp:self.timestamp]; + [builder setFlags:SSKProtoDataMessageFlagsEndSession]; + + return builder; +} + +- (nullable id)prepareCustomContentBuilder:(SignalRecipient *)recipient { + SSKProtoContentBuilder *builder = [super prepareCustomContentBuilder:recipient]; + + PreKeyBundle *bundle = [OWSPrimaryStorage.sharedManager generatePreKeyBundleForContact:recipient.recipientId]; + SSKProtoPrekeyBundleMessageBuilder *preKeyBuilder = [SSKProtoPrekeyBundleMessage builderFromPreKeyBundle:bundle]; + + // Build the pre key bundle message + NSError *error; + SSKProtoPrekeyBundleMessage *_Nullable message = [preKeyBuilder buildAndReturnError:&error]; + if (error || !message) { + OWSFailDebug(@"Failed to build pre key bundle for: %@ due to error: %@.", recipient.recipientId, error); + } else { + [builder setPrekeyBundleMessage:message]; + } + + return builder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSError.h b/SignalUtilitiesKit/OWSError.h new file mode 100644 index 000000000..79f763369 --- /dev/null +++ b/SignalUtilitiesKit/OWSError.h @@ -0,0 +1,71 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const OWSSignalServiceKitErrorDomain; + +typedef NS_ENUM(NSInteger, OWSErrorCode) { + OWSErrorCodeInvalidMethodParameters = 11, + OWSErrorCodeUnableToProcessServerResponse = 12, + OWSErrorCodeFailedToDecodeJson = 13, + OWSErrorCodeFailedToEncodeJson = 14, + OWSErrorCodeFailedToDecodeQR = 15, + OWSErrorCodePrivacyVerificationFailure = 20, + OWSErrorCodeUntrustedIdentity = 25, + OWSErrorCodeFailedToSendOutgoingMessage = 30, + OWSErrorCodeAssertionFailure = 31, + OWSErrorCodeFailedToDecryptMessage = 100, + OWSErrorCodeFailedToDecryptUDMessage = 101, + OWSErrorCodeFailedToEncryptMessage = 110, + OWSErrorCodeFailedToEncryptUDMessage = 111, + OWSErrorCodeSignalServiceFailure = 1001, + OWSErrorCodeSignalServiceRateLimited = 1010, + OWSErrorCodeUserError = 2001, + OWSErrorCodeNoSuchSignalRecipient = 777404, + OWSErrorCodeMessageSendDisabledDueToPreKeyUpdateFailures = 777405, + OWSErrorCodeMessageSendFailedToBlockList = 777406, + OWSErrorCodeMessageSendNoValidRecipients = 777407, + OWSErrorCodeContactsUpdaterRateLimit = 777408, + OWSErrorCodeCouldNotWriteAttachmentData = 777409, + OWSErrorCodeMessageDeletedBeforeSent = 777410, + OWSErrorCodeDatabaseConversionFatalError = 777411, + OWSErrorCodeMoveFileToSharedDataContainerError = 777412, + OWSErrorCodeRegistrationMissing2FAPIN = 777413, + OWSErrorCodeDebugLogUploadFailed = 777414, + // A non-recoverable error occured while exporting a backup. + OWSErrorCodeExportBackupFailed = 777415, + // A possibly recoverable error occured while exporting a backup. + OWSErrorCodeExportBackupError = 777416, + // A non-recoverable error occured while importing a backup. + OWSErrorCodeImportBackupFailed = 777417, + // A possibly recoverable error occured while importing a backup. + OWSErrorCodeImportBackupError = 777418, + // A non-recoverable while importing or exporting a backup. + OWSErrorCodeBackupFailure = 777419, + OWSErrorCodeLocalAuthenticationError = 777420, + OWSErrorCodeMessageRequestFailed = 777421, + OWSErrorCodeMessageResponseFailed = 777422, + OWSErrorCodeInvalidMessage = 777423, + OWSErrorCodeProfileUpdateFailed = 777424, + OWSErrorCodeAvatarWriteFailed = 777425, + OWSErrorCodeAvatarUploadFailed = 777426, + OWSErrorCodeNoSessionForTransientMessage, +}; + +extern NSString *const OWSErrorRecipientIdentifierKey; + +extern NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description); +extern NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSString *recipientId); +extern NSError *OWSErrorMakeUnableToProcessServerResponseError(void); +extern NSError *OWSErrorMakeFailedToSendOutgoingMessageError(void); +extern NSError *OWSErrorMakeNoSuchSignalRecipientError(void); +extern NSError *OWSErrorMakeAssertionError(NSString *description); +extern NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(void); +extern NSError *OWSErrorMakeMessageSendFailedDueToBlockListError(void); +extern NSError *OWSErrorMakeWriteAttachmentDataError(void); + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSError.m b/SignalUtilitiesKit/OWSError.m new file mode 100644 index 000000000..b5cfc1435 --- /dev/null +++ b/SignalUtilitiesKit/OWSError.m @@ -0,0 +1,75 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSError.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSSignalServiceKitErrorDomain = @"OWSSignalServiceKitErrorDomain"; +NSString *const OWSErrorRecipientIdentifierKey = @"OWSErrorKeyRecipientIdentifier"; + +NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description) +{ + return [NSError errorWithDomain:OWSSignalServiceKitErrorDomain + code:code + userInfo:@{ NSLocalizedDescriptionKey: description }]; +} + +NSError *OWSErrorMakeUnableToProcessServerResponseError() +{ + return OWSErrorWithCodeDescription(OWSErrorCodeUnableToProcessServerResponse, + NSLocalizedString(@"ERROR_DESCRIPTION_SERVER_FAILURE", @"Generic server error")); +} + +NSError *OWSErrorMakeFailedToSendOutgoingMessageError() +{ + return OWSErrorWithCodeDescription(OWSErrorCodeFailedToSendOutgoingMessage, + NSLocalizedString(@"ERROR_DESCRIPTION_CLIENT_SENDING_FAILURE", @"Generic notice when message failed to send.")); +} + +NSError *OWSErrorMakeNoSuchSignalRecipientError() +{ + return OWSErrorWithCodeDescription(OWSErrorCodeNoSuchSignalRecipient, + NSLocalizedString( + @"ERROR_DESCRIPTION_UNREGISTERED_RECIPIENT", @"Error message when attempting to send message")); +} + +NSError *OWSErrorMakeAssertionError(NSString *description) +{ + OWSCFailDebug(@"Assertion failed: %@", description); + return OWSErrorWithCodeDescription(OWSErrorCodeAssertionFailure, + NSLocalizedString(@"ERROR_DESCRIPTION_UNKNOWN_ERROR", @"Worst case generic error message")); +} + +NSError *OWSErrorMakeUntrustedIdentityError(NSString *description, NSString *recipientId) +{ + return [NSError + errorWithDomain:OWSSignalServiceKitErrorDomain + code:OWSErrorCodeUntrustedIdentity + userInfo:@{ NSLocalizedDescriptionKey : description, OWSErrorRecipientIdentifierKey : recipientId }]; +} + +NSError *OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError() +{ + return OWSErrorWithCodeDescription(OWSErrorCodeMessageSendDisabledDueToPreKeyUpdateFailures, + NSLocalizedString(@"ERROR_DESCRIPTION_MESSAGE_SEND_DISABLED_PREKEY_UPDATE_FAILURES", + @"Error message indicating that message send is disabled due to prekey update failures")); +} + +NSError *OWSErrorMakeMessageSendFailedDueToBlockListError() +{ + return OWSErrorWithCodeDescription(OWSErrorCodeMessageSendFailedToBlockList, + NSLocalizedString(@"ERROR_DESCRIPTION_MESSAGE_SEND_FAILED_DUE_TO_BLOCK_LIST", + @"Error message indicating that message send failed due to block list")); +} + +NSError *OWSErrorMakeWriteAttachmentDataError() +{ + return OWSErrorWithCodeDescription(OWSErrorCodeCouldNotWriteAttachmentData, + NSLocalizedString(@"ERROR_DESCRIPTION_MESSAGE_SEND_FAILED_DUE_TO_FAILED_ATTACHMENT_WRITE", + @"Error message indicating that message send failed due to failed attachment write")); +} + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.h b/SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.h new file mode 100644 index 000000000..f3fab6d30 --- /dev/null +++ b/SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.h @@ -0,0 +1,31 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class OWSStorage; + +@interface OWSFailedAttachmentDownloadsJob : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + +- (void)run; + ++ (NSString *)databaseExtensionName; ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage; + +#ifdef DEBUG +/** + * Only use the sync version for testing, generally we'll want to register extensions async + */ +- (void)blockingRegisterDatabaseExtensions; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.m b/SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.m new file mode 100644 index 000000000..e653a69c1 --- /dev/null +++ b/SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.m @@ -0,0 +1,142 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSFailedAttachmentDownloadsJob.h" +#import "OWSPrimaryStorage.h" +#import "TSAttachmentPointer.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const OWSFailedAttachmentDownloadsJobAttachmentStateColumn = @"state"; +static NSString *const OWSFailedAttachmentDownloadsJobAttachmentStateIndex = @"index_attachment_downloads_on_state"; + +@interface OWSFailedAttachmentDownloadsJob () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; + +@end + +#pragma mark - + +@implementation OWSFailedAttachmentDownloadsJob + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _primaryStorage = primaryStorage; + + return self; +} + +- (NSArray *)fetchAttemptingOutAttachmentIdsWithTransaction: + (YapDatabaseReadWriteTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *attachmentIds = [NSMutableArray new]; + + NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ != %d", + OWSFailedAttachmentDownloadsJobAttachmentStateColumn, + (int)TSAttachmentPointerStateFailed]; + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:OWSFailedAttachmentDownloadsJobAttachmentStateIndex] + enumerateKeysMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, BOOL *stop) { + [attachmentIds addObject:key]; + }]; + + return [attachmentIds copy]; +} + +- (void)enumerateAttemptingOutAttachmentsWithBlock:(void (^_Nonnull)(TSAttachmentPointer *attachment))block + transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + // Since we can't directly mutate the enumerated attachments, we store only their ids in hopes + // of saving a little memory and then enumerate the (larger) TSAttachment objects one at a time. + for (NSString *attachmentId in [self fetchAttemptingOutAttachmentIdsWithTransaction:transaction]) { + TSAttachmentPointer *_Nullable attachment = + [TSAttachmentPointer fetchObjectWithUniqueID:attachmentId transaction:transaction]; + if ([attachment isKindOfClass:[TSAttachmentPointer class]]) { + block(attachment); + } else { + OWSLogError(@"unexpected object: %@", attachment); + } + } +} + +- (void)run +{ + __block uint count = 0; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self enumerateAttemptingOutAttachmentsWithBlock:^(TSAttachmentPointer *attachment) { + // sanity check + if (attachment.state != TSAttachmentPointerStateFailed) { + attachment.state = TSAttachmentPointerStateFailed; + [attachment saveWithTransaction:transaction]; + count++; + } + } + transaction:transaction]; + }]; + + OWSLogDebug(@"Marked %u attachments as unsent", count); +} + +#pragma mark - YapDatabaseExtension + ++ (YapDatabaseSecondaryIndex *)indexDatabaseExtension +{ + YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new]; + [setup addColumn:OWSFailedAttachmentDownloadsJobAttachmentStateColumn + withType:YapDatabaseSecondaryIndexTypeInteger]; + + YapDatabaseSecondaryIndexHandler *handler = + [YapDatabaseSecondaryIndexHandler withObjectBlock:^(YapDatabaseReadTransaction *transaction, + NSMutableDictionary *dict, + NSString *collection, + NSString *key, + id object) { + if (![object isKindOfClass:[TSAttachmentPointer class]]) { + return; + } + TSAttachmentPointer *attachment = (TSAttachmentPointer *)object; + dict[OWSFailedAttachmentDownloadsJobAttachmentStateColumn] = @(attachment.state); + }]; + + return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:nil]; +} + +#ifdef DEBUG +// Useful for tests, don't use in app startup path because it's slow. +- (void)blockingRegisterDatabaseExtensions +{ + [self.primaryStorage registerExtension:[self.class indexDatabaseExtension] + withName:OWSFailedAttachmentDownloadsJobAttachmentStateIndex]; +} +#endif + ++ (NSString *)databaseExtensionName +{ + return OWSFailedAttachmentDownloadsJobAttachmentStateIndex; +} + ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage +{ + [storage asyncRegisterExtension:[self indexDatabaseExtension] + withName:OWSFailedAttachmentDownloadsJobAttachmentStateIndex]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFailedMessagesJob.h b/SignalUtilitiesKit/OWSFailedMessagesJob.h new file mode 100644 index 000000000..7a5bd0d6a --- /dev/null +++ b/SignalUtilitiesKit/OWSFailedMessagesJob.h @@ -0,0 +1,31 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class OWSStorage; + +@interface OWSFailedMessagesJob : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + +- (void)run; + ++ (NSString *)databaseExtensionName; ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage; + +#ifdef DEBUG +/** + * Only use the sync version for testing, generally we'll want to register extensions async + */ +- (void)blockingRegisterDatabaseExtensions; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFailedMessagesJob.m b/SignalUtilitiesKit/OWSFailedMessagesJob.m new file mode 100644 index 000000000..e5e972b6b --- /dev/null +++ b/SignalUtilitiesKit/OWSFailedMessagesJob.m @@ -0,0 +1,149 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSFailedMessagesJob.h" +#import "OWSPrimaryStorage.h" +#import "TSMessage.h" +#import "TSOutgoingMessage.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const OWSFailedMessagesJobMessageStateColumn = @"message_state"; +static NSString *const OWSFailedMessagesJobMessageStateIndex = @"index_outoing_messages_on_message_state"; + +@interface OWSFailedMessagesJob () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; + +@end + +#pragma mark - + +@implementation OWSFailedMessagesJob + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _primaryStorage = primaryStorage; + + return self; +} + +- (NSArray *)fetchAttemptingOutMessageIdsWithTransaction: + (YapDatabaseReadWriteTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *messageIds = [NSMutableArray new]; + + NSString *formattedString = [NSString + stringWithFormat:@"WHERE %@ == %d", OWSFailedMessagesJobMessageStateColumn, (int)TSOutgoingMessageStateSending]; + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:OWSFailedMessagesJobMessageStateIndex] + enumerateKeysMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, BOOL *stop) { + if (key == nil) { return; } + [messageIds addObject:key]; + }]; + + return [messageIds copy]; +} + +- (void)enumerateAttemptingOutMessagesWithBlock:(void (^_Nonnull)(TSOutgoingMessage *message))block + transaction:(YapDatabaseReadWriteTransaction *_Nonnull)transaction +{ + OWSAssertDebug(transaction); + + // Since we can't directly mutate the enumerated "attempting out" expired messages, we store only their ids in hopes + // of saving a little memory and then enumerate the (larger) TSMessage objects one at a time. + for (NSString *expiredMessageId in [self fetchAttemptingOutMessageIdsWithTransaction:transaction]) { + TSOutgoingMessage *_Nullable message = + [TSOutgoingMessage fetchObjectWithUniqueID:expiredMessageId transaction:transaction]; + if ([message isKindOfClass:[TSOutgoingMessage class]]) { + block(message); + } else { + OWSLogError(@"unexpected object: %@", message); + } + } +} + +- (void)run +{ + __block uint count = 0; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self enumerateAttemptingOutMessagesWithBlock:^(TSOutgoingMessage *message) { + // sanity check + OWSAssertDebug(message.messageState == TSOutgoingMessageStateSending); + if (message.messageState != TSOutgoingMessageStateSending) { + OWSLogError(@"Refusing to mark as unsent message with state: %d", (int)message.messageState); + return; + } + + OWSLogDebug(@"marking message as unsent: %@", message.uniqueId); + [message updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:transaction]; + OWSAssertDebug(message.messageState == TSOutgoingMessageStateFailed); + + count++; + } + transaction:transaction]; + }]; + + OWSLogDebug(@"Marked %u messages as unsent", count); +} + +#pragma mark - YapDatabaseExtension + ++ (YapDatabaseSecondaryIndex *)indexDatabaseExtension +{ + YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new]; + [setup addColumn:OWSFailedMessagesJobMessageStateColumn withType:YapDatabaseSecondaryIndexTypeInteger]; + + YapDatabaseSecondaryIndexHandler *handler = + [YapDatabaseSecondaryIndexHandler withObjectBlock:^(YapDatabaseReadTransaction *transaction, + NSMutableDictionary *dict, + NSString *collection, + NSString *key, + id object) { + if (![object isKindOfClass:[TSOutgoingMessage class]]) { + return; + } + TSOutgoingMessage *message = (TSOutgoingMessage *)object; + + dict[OWSFailedMessagesJobMessageStateColumn] = @(message.messageState); + }]; + + return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:nil]; +} + +#ifdef DEBUG +// Useful for tests, don't use in app startup path because it's slow. +- (void)blockingRegisterDatabaseExtensions +{ + [self.primaryStorage registerExtension:[self.class indexDatabaseExtension] + withName:OWSFailedMessagesJobMessageStateIndex]; +} +#endif + ++ (NSString *)databaseExtensionName +{ + return OWSFailedMessagesJobMessageStateIndex; +} + ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage +{ + [storage asyncRegisterExtension:[self indexDatabaseExtension] withName:OWSFailedMessagesJobMessageStateIndex]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFileSystem.h b/SignalUtilitiesKit/OWSFileSystem.h new file mode 100644 index 000000000..7e059c0a3 --- /dev/null +++ b/SignalUtilitiesKit/OWSFileSystem.h @@ -0,0 +1,59 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// Use instead of NSTemporaryDirectory() +// prefer the more restrictice OWSTemporaryDirectory, +// unless the temp data may need to be accessed while the device is locked. +NSString *OWSTemporaryDirectory(void); +NSString *OWSTemporaryDirectoryAccessibleAfterFirstAuth(void); +void ClearOldTemporaryDirectories(void); + +@interface OWSFileSystem : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (BOOL)protectFileOrFolderAtPath:(NSString *)path; ++ (BOOL)protectFileOrFolderAtPath:(NSString *)path fileProtectionType:(NSFileProtectionType)fileProtectionType; + ++ (BOOL)protectRecursiveContentsAtPath:(NSString *)path; + ++ (NSString *)appDocumentDirectoryPath; + ++ (NSString *)appLibraryDirectoryPath; + ++ (NSString *)appSharedDataDirectoryPath; + ++ (NSString *)cachesDirectoryPath; + ++ (nullable NSError *)renameFilePathUsingRandomExtension:(NSString *)oldFilePath; + ++ (nullable NSError *)moveAppFilePath:(NSString *)oldFilePath sharedDataFilePath:(NSString *)newFilePath; + +// Returns NO IFF the directory does not exist and could not be created. ++ (BOOL)ensureDirectoryExists:(NSString *)dirPath; + ++ (BOOL)ensureFileExists:(NSString *)filePath; + ++ (BOOL)deleteFile:(NSString *)filePath; + ++ (BOOL)deleteFileIfExists:(NSString *)filePath; + ++ (NSArray *_Nullable)allFilesInDirectoryRecursive:(NSString *)dirPath error:(NSError **)error; + ++ (NSString *)temporaryFilePath; ++ (NSString *)temporaryFilePathWithFileExtension:(NSString *_Nullable)fileExtension; + +// Returns nil on failure. ++ (nullable NSString *)writeDataToTemporaryFile:(NSData *)data fileExtension:(NSString *_Nullable)fileExtension; + ++ (nullable NSNumber *)fileSizeOfPath:(NSString *)filePath; ++ (void)logAttributesOfItemAtPathRecursively:(NSString *)path; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFileSystem.m b/SignalUtilitiesKit/OWSFileSystem.m new file mode 100644 index 000000000..63415221e --- /dev/null +++ b/SignalUtilitiesKit/OWSFileSystem.m @@ -0,0 +1,430 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSFileSystem.h" +#import "OWSError.h" +#import "TSConstants.h" +#import "AppContext.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSFileSystem + ++ (BOOL)protectRecursiveContentsAtPath:(NSString *)path +{ + BOOL isDirectory; + if (![NSFileManager.defaultManager fileExistsAtPath:path isDirectory:&isDirectory]) { + return NO; + } + + if (!isDirectory) { + return [self protectFileOrFolderAtPath:path]; + } + NSString *dirPath = path; + + BOOL success = YES; + NSDirectoryEnumerator *directoryEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:dirPath]; + + for (NSString *relativePath in directoryEnumerator) { + NSString *filePath = [dirPath stringByAppendingPathComponent:relativePath]; + OWSLogDebug(@"path: %@ had attributes: %@", filePath, directoryEnumerator.fileAttributes); + + success = success && [self protectFileOrFolderAtPath:filePath]; + } + + OWSLogInfo(@"protected contents at path: %@", path); + return success; +} + ++ (BOOL)protectFileOrFolderAtPath:(NSString *)path +{ + return + [self protectFileOrFolderAtPath:path fileProtectionType:NSFileProtectionCompleteUntilFirstUserAuthentication]; +} + ++ (BOOL)protectFileOrFolderAtPath:(NSString *)path fileProtectionType:(NSFileProtectionType)fileProtectionType +{ + OWSLogVerbose(@"protecting file at path: %@", path); + if (![NSFileManager.defaultManager fileExistsAtPath:path]) { + return NO; + } + + NSError *error; + NSDictionary *fileProtection = @{ NSFileProtectionKey : fileProtectionType }; + [[NSFileManager defaultManager] setAttributes:fileProtection ofItemAtPath:path error:&error]; + + NSDictionary *resourcesAttrs = @{ NSURLIsExcludedFromBackupKey : @YES }; + + NSURL *ressourceURL = [NSURL fileURLWithPath:path]; + BOOL success = [ressourceURL setResourceValues:resourcesAttrs error:&error]; + + if (error || !success) { + OWSFailDebug(@"Could not protect file or folder: %@", error); + return NO; + } + return YES; +} + ++ (void)logAttributesOfItemAtPathRecursively:(NSString *)path +{ + BOOL isDirectory; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]; + if (!exists) { + OWSFailDebug(@"error retrieving file attributes for missing file"); + return; + } + + if (isDirectory) { + NSDirectoryEnumerator *directoryEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:(NSString *)path]; + for (NSString *path in directoryEnumerator) { + OWSLogDebug(@"path: %@ has attributes: %@", path, directoryEnumerator.fileAttributes); + } + } else { + NSError *error; + NSDictionary *_Nullable attributes = + [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&error]; + if (error) { + OWSFailDebug(@"error retrieving file attributes: %@", error); + } else { + OWSLogDebug(@"path: %@ has attributes: %@", path, attributes); + } + } +} + ++ (NSString *)appLibraryDirectoryPath +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *documentDirectoryURL = + [[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject]; + return [documentDirectoryURL path]; +} + ++ (NSString *)appDocumentDirectoryPath +{ + return CurrentAppContext().appDocumentDirectoryPath; +} + ++ (NSString *)appSharedDataDirectoryPath +{ + return CurrentAppContext().appSharedDataDirectoryPath; +} + ++ (NSString *)cachesDirectoryPath +{ + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + OWSAssertDebug(paths.count >= 1); + return paths[0]; +} + ++ (nullable NSError *)renameFilePathUsingRandomExtension:(NSString *)oldFilePath +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:oldFilePath]) { + return nil; + } + + NSString *newFilePath = + [[oldFilePath stringByAppendingString:@"."] stringByAppendingString:[NSUUID UUID].UUIDString]; + + OWSLogInfo(@"Moving file or directory from: %@ to: %@", oldFilePath, newFilePath); + + NSError *_Nullable error; + BOOL success = [fileManager moveItemAtPath:oldFilePath toPath:newFilePath error:&error]; + if (!success || error) { + OWSLogDebug(@"Could not move file or directory from: %@ to: %@, error: %@", oldFilePath, newFilePath, error); + OWSFailDebug(@"Could not move file or directory with error: %@", error); + return error; + } + return nil; +} + ++ (nullable NSError *)moveAppFilePath:(NSString *)oldFilePath sharedDataFilePath:(NSString *)newFilePath +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:oldFilePath]) { + return nil; + } + + OWSLogInfo(@"Moving file or directory from: %@ to: %@", oldFilePath, newFilePath); + + if ([fileManager fileExistsAtPath:newFilePath]) { + // If a file/directory already exists at the destination, + // try to move it "aside" by renaming it with an extension. + NSError *_Nullable error = [self renameFilePathUsingRandomExtension:newFilePath]; + if (error) { + return error; + } + } + + if ([fileManager fileExistsAtPath:newFilePath]) { + OWSLogDebug( + @"Can't move file or directory from: %@ to: %@; destination already exists.", oldFilePath, newFilePath); + OWSFailDebug(@"Can't move file or directory; destination already exists."); + return OWSErrorWithCodeDescription( + OWSErrorCodeMoveFileToSharedDataContainerError, @"Can't move file; destination already exists."); + } + + NSDate *startDate = [NSDate new]; + + NSError *_Nullable error; + BOOL success = [fileManager moveItemAtPath:oldFilePath toPath:newFilePath error:&error]; + if (!success || error) { + OWSLogDebug(@"Could not move file or directory from: %@ to: %@, error: %@", oldFilePath, newFilePath, error); + OWSFailDebug(@"Could not move file or directory with error: %@", error); + return error; + } + + OWSLogInfo(@"Moved file or directory in: %f", fabs([startDate timeIntervalSinceNow])); + OWSLogDebug(@"Moved file or directory from: %@ to: %@ in: %f", + oldFilePath, + newFilePath, + fabs([startDate timeIntervalSinceNow])); + + // Ensure all files moved have the proper data protection class. + // On large directories this can take a while, so we dispatch async + // since we're in the launch path. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self protectRecursiveContentsAtPath:newFilePath]; + }); + + return nil; +} + ++ (BOOL)ensureDirectoryExists:(NSString *)dirPath +{ + return [self ensureDirectoryExists:dirPath fileProtectionType:NSFileProtectionCompleteUntilFirstUserAuthentication]; +} + ++ (BOOL)ensureDirectoryExists:(NSString *)dirPath fileProtectionType:(NSFileProtectionType)fileProtectionType +{ + BOOL isDirectory; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:dirPath isDirectory:&isDirectory]; + if (exists) { + OWSAssertDebug(isDirectory); + + return [self protectFileOrFolderAtPath:dirPath fileProtectionType:fileProtectionType]; + } else { + OWSLogInfo(@"Creating directory at: %@", dirPath); + + NSError *error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:dirPath + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + OWSFailDebug(@"Failed to create directory: %@, error: %@", dirPath, error); + return NO; + } + return [self protectFileOrFolderAtPath:dirPath fileProtectionType:fileProtectionType]; + } +} + ++ (BOOL)ensureFileExists:(NSString *)filePath +{ + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:filePath]; + if (exists) { + return [self protectFileOrFolderAtPath:filePath]; + } else { + BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + if (!success) { + OWSFailDebug(@"Failed to create file."); + return NO; + } + return [self protectFileOrFolderAtPath:filePath]; + } +} + ++ (BOOL)deleteFile:(NSString *)filePath +{ + NSError *error; + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + if (!success || error) { + OWSLogError(@"Failed to delete file: %@", error.description); + return NO; + } + return YES; +} + ++ (BOOL)deleteFileIfExists:(NSString *)filePath +{ + if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + return YES; + } + return [self deleteFile:filePath]; +} + ++ (NSArray *_Nullable)allFilesInDirectoryRecursive:(NSString *)dirPath error:(NSError **)error +{ + OWSAssertDebug(dirPath.length > 0); + + *error = nil; + + NSArray *filenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:error]; + if (*error) { + OWSFailDebug(@"could not find files in directory: %@", *error); + return nil; + } + + NSMutableArray *filePaths = [NSMutableArray new]; + + for (NSString *filename in filenames) { + NSString *filePath = [dirPath stringByAppendingPathComponent:filename]; + + BOOL isDirectory; + [[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]; + if (isDirectory) { + [filePaths addObjectsFromArray:[self allFilesInDirectoryRecursive:filePath error:error]]; + if (*error) { + return nil; + } + } else { + [filePaths addObject:filePath]; + } + } + + return filePaths; +} + ++ (NSString *)temporaryFilePath +{ + return [self temporaryFilePathWithFileExtension:nil]; +} + ++ (NSString *)temporaryFilePathWithFileExtension:(NSString *_Nullable)fileExtension +{ + NSString *temporaryDirectory = OWSTemporaryDirectory(); + NSString *tempFileName = NSUUID.UUID.UUIDString; + if (fileExtension.length > 0) { + tempFileName = [[tempFileName stringByAppendingString:@"."] stringByAppendingString:fileExtension]; + } + NSString *tempFilePath = [temporaryDirectory stringByAppendingPathComponent:tempFileName]; + + return tempFilePath; +} + ++ (nullable NSString *)writeDataToTemporaryFile:(NSData *)data fileExtension:(NSString *_Nullable)fileExtension +{ + OWSAssertDebug(data); + + NSString *tempFilePath = [self temporaryFilePathWithFileExtension:fileExtension]; + NSError *error; + BOOL success = [data writeToFile:tempFilePath options:NSDataWritingAtomic error:&error]; + if (!success || error) { + OWSFailDebug(@"could not write to temporary file: %@", error); + return nil; + } + + [self protectFileOrFolderAtPath:tempFilePath]; + + return tempFilePath; +} + ++ (nullable NSNumber *)fileSizeOfPath:(NSString *)filePath +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *_Nullable error; + unsigned long long fileSize = + [[fileManager attributesOfItemAtPath:filePath error:&error][NSFileSize] unsignedLongLongValue]; + if (error) { + OWSLogError(@"Couldn't fetch file size[%@]: %@", filePath, error); + return nil; + } else { + return @(fileSize); + } +} + +@end + +#pragma mark - + +NSString *OWSTemporaryDirectory(void) +{ + static NSString *dirPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *dirName = [NSString stringWithFormat:@"ows_temp_%@", NSUUID.UUID.UUIDString]; + dirPath = [NSTemporaryDirectory() stringByAppendingPathComponent:dirName]; + BOOL success = [OWSFileSystem ensureDirectoryExists:dirPath fileProtectionType:NSFileProtectionComplete]; + OWSCAssert(success); + }); + return dirPath; +} + +NSString *OWSTemporaryDirectoryAccessibleAfterFirstAuth(void) +{ + NSString *dirPath = NSTemporaryDirectory(); + BOOL success = [OWSFileSystem ensureDirectoryExists:dirPath + fileProtectionType:NSFileProtectionCompleteUntilFirstUserAuthentication]; + OWSCAssert(success); + return dirPath; +} + +void ClearOldTemporaryDirectoriesSync(void) +{ + // Ignore the "current" temp directory. + NSString *currentTempDirName = OWSTemporaryDirectory().lastPathComponent; + + NSDate *thresholdDate = CurrentAppContext().appLaunchTime; + NSString *dirPath = NSTemporaryDirectory(); + NSError *error; + NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error]; + if (error) { + OWSCFailDebug(@"contentsOfDirectoryAtPath error: %@", error); + return; + } + for (NSString *fileName in fileNames) { + if (!CurrentAppContext().isAppForegroundAndActive) { + // Abort if app not active. + return; + } + if ([fileName isEqualToString:currentTempDirName]) { + continue; + } + + NSString *filePath = [dirPath stringByAppendingPathComponent:fileName]; + + // Delete files with either: + // + // a) "ows_temp" name prefix. + // b) modified time before app launch time. + if (![fileName hasPrefix:@"ows_temp"]) { + NSError *error; + NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error]; + if (!attributes || error) { + // This is fine; the file may have been deleted since we found it. + OWSLogError(@"Could not get attributes of file or directory at: %@", filePath); + continue; + } + // Don't delete files which were created in the last N minutes. + NSDate *creationDate = attributes.fileModificationDate; + if ([creationDate isAfterDate:thresholdDate]) { + OWSLogInfo(@"Skipping file due to age: %f", fabs([creationDate timeIntervalSinceNow])); + continue; + } + } + + OWSLogVerbose(@"Removing temp file or directory: %@", filePath); + if (![OWSFileSystem deleteFile:filePath]) { + // This can happen if the app launches before the phone is unlocked. + // Clean up will occur when app becomes active. + OWSLogWarn(@"Could not delete old temp directory: %@", filePath); + } + } +} + +// NOTE: We need to call this method on launch _and_ every time the app becomes active, +// since file protection may prevent it from succeeding in the background. +void ClearOldTemporaryDirectories(void) +{ + // We use the lowest priority queue for this, and wait N seconds + // to avoid interfering with app startup. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.f * NSEC_PER_SEC)), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), + ^{ + ClearOldTemporaryDirectoriesSync(); + }); +} + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFingerprint.h b/SignalUtilitiesKit/OWSFingerprint.h new file mode 100644 index 000000000..36904a41d --- /dev/null +++ b/SignalUtilitiesKit/OWSFingerprint.h @@ -0,0 +1,52 @@ +// Created by Michael Kirk on 9/14/16. +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class UIImage; + +@interface OWSFingerprint : NSObject + +#pragma mark - Initializers + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithMyStableId:(NSString *)myStableId + myIdentityKey:(NSData *)myIdentityKeyWithoutKeyType + theirStableId:(NSString *)theirStableId + theirIdentityKey:(NSData *)theirIdentityKeyWithoutKeyType + theirName:(NSString *)theirName + hashIterations:(uint32_t)hashIterations NS_DESIGNATED_INITIALIZER; + ++ (instancetype)fingerprintWithMyStableId:(NSString *)myStableId + myIdentityKey:(NSData *)myIdentityKeyWithoutKeyType + theirStableId:(NSString *)theirStableId + theirIdentityKey:(NSData *)theirIdentityKeyWithoutKeyType + theirName:(NSString *)theirName + hashIterations:(uint32_t)hashIterations; + ++ (instancetype)fingerprintWithMyStableId:(NSString *)myStableId + myIdentityKey:(NSData *)myIdentityKeyWithoutKeyType + theirStableId:(NSString *)theirStableId + theirIdentityKey:(NSData *)theirIdentityKeyWithoutKeyType + theirName:(NSString *)theirName; + +#pragma mark - Properties + +@property (nonatomic, readonly) NSData *myStableIdData; +@property (nonatomic, readonly) NSData *myIdentityKey; +@property (nonatomic, readonly) NSString *theirStableId; +@property (nonatomic, readonly) NSData *theirStableIdData; +@property (nonatomic, readonly) NSData *theirIdentityKey; +@property (nonatomic, readonly) NSString *displayableText; +@property (nullable, nonatomic, readonly) UIImage *image; + +#pragma mark - Instance Methods + +- (BOOL)matchesLogicalFingerprintsData:(NSData *)data error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFingerprint.m b/SignalUtilitiesKit/OWSFingerprint.m new file mode 100644 index 000000000..b7ea82fff --- /dev/null +++ b/SignalUtilitiesKit/OWSFingerprint.m @@ -0,0 +1,334 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSFingerprint.h" +#import "OWSError.h" +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static uint32_t const OWSFingerprintHashingVersion = 0; +static uint32_t const OWSFingerprintScannableFormatVersion = 1; +static uint32_t const OWSFingerprintDefaultHashIterations = 5200; + +@interface OWSFingerprint () + +@property (nonatomic, readonly) NSUInteger hashIterations; +@property (nonatomic, readonly) NSString *text; +@property (nonatomic, readonly) NSData *myFingerprintData; +@property (nonatomic, readonly) NSData *theirFingerprintData; +@property (nonatomic, readonly) NSString *theirName; + +@end + +#pragma mark - + +@implementation OWSFingerprint + +- (instancetype)initWithMyStableId:(NSString *)myStableId + myIdentityKey:(NSData *)myIdentityKeyWithoutKeyType + theirStableId:(NSString *)theirStableId + theirIdentityKey:(NSData *)theirIdentityKeyWithoutKeyType + theirName:(NSString *)theirName + hashIterations:(uint32_t)hashIterations +{ + OWSAssertDebug(theirIdentityKeyWithoutKeyType.length == 32); + OWSAssertDebug(myIdentityKeyWithoutKeyType.length == 32); + + self = [super init]; + if (!self) { + return self; + } + + _myStableIdData = [myStableId dataUsingEncoding:NSUTF8StringEncoding]; + _myIdentityKey = [myIdentityKeyWithoutKeyType prependKeyType]; + _theirStableId = theirStableId; + _theirStableIdData = [theirStableId dataUsingEncoding:NSUTF8StringEncoding]; + _theirIdentityKey = [theirIdentityKeyWithoutKeyType prependKeyType]; + _theirName = theirName; + _hashIterations = hashIterations; + + _myFingerprintData = [self dataForStableId:_myStableIdData publicKey:_myIdentityKey]; + _theirFingerprintData = [self dataForStableId:_theirStableIdData publicKey:_theirIdentityKey]; + + return self; +} + ++ (instancetype)fingerprintWithMyStableId:(NSString *)myStableId + myIdentityKey:(NSData *)myIdentityKeyWithoutKeyType + theirStableId:(NSString *)theirStableId + theirIdentityKey:(NSData *)theirIdentityKeyWithoutKeyType + theirName:(NSString *)theirName + hashIterations:(uint32_t)hashIterations +{ + return [[self alloc] initWithMyStableId:myStableId + myIdentityKey:myIdentityKeyWithoutKeyType + theirStableId:theirStableId + theirIdentityKey:theirIdentityKeyWithoutKeyType + theirName:theirName + hashIterations:hashIterations]; +} + ++ (instancetype)fingerprintWithMyStableId:(NSString *)myStableId + myIdentityKey:(NSData *)myIdentityKeyWithoutKeyType + theirStableId:(NSString *)theirStableId + theirIdentityKey:(NSData *)theirIdentityKeyWithoutKeyType + theirName:(NSString *)theirName +{ + return [[self alloc] initWithMyStableId:myStableId + myIdentityKey:myIdentityKeyWithoutKeyType + theirStableId:theirStableId + theirIdentityKey:theirIdentityKeyWithoutKeyType + theirName:theirName + hashIterations:OWSFingerprintDefaultHashIterations]; +} + +- (BOOL)matchesLogicalFingerprintsData:(NSData *)data error:(NSError **)error +{ + OWSAssertDebug(data.length > 0); + OWSAssertDebug(error); + + *error = nil; + FingerprintProtoLogicalFingerprints *_Nullable logicalFingerprints; + logicalFingerprints = [FingerprintProtoLogicalFingerprints parseData:data error:error]; + if (!logicalFingerprints || *error) { + OWSFailDebug(@"fingerprint failure: %@", *error); + + NSString *description = NSLocalizedString(@"PRIVACY_VERIFICATION_FAILURE_INVALID_QRCODE", @"alert body"); + *error = OWSErrorWithCodeDescription(OWSErrorCodePrivacyVerificationFailure, description); + return NO; + } + + if (logicalFingerprints.version < OWSFingerprintScannableFormatVersion) { + OWSLogWarn(@"Verification failed. They're running an old version."); + NSString *description + = NSLocalizedString(@"PRIVACY_VERIFICATION_FAILED_WITH_OLD_REMOTE_VERSION", @"alert body"); + *error = OWSErrorWithCodeDescription(OWSErrorCodePrivacyVerificationFailure, description); + return NO; + } + + if (logicalFingerprints.version > OWSFingerprintScannableFormatVersion) { + OWSLogWarn(@"Verification failed. We're running an old version."); + NSString *description = NSLocalizedString(@"PRIVACY_VERIFICATION_FAILED_WITH_OLD_LOCAL_VERSION", @"alert body"); + *error = OWSErrorWithCodeDescription(OWSErrorCodePrivacyVerificationFailure, description); + return NO; + } + + // Their local is *our* remote. + FingerprintProtoLogicalFingerprint *localFingerprint = logicalFingerprints.remoteFingerprint; + FingerprintProtoLogicalFingerprint *remoteFingerprint = logicalFingerprints.localFingerprint; + + if (![remoteFingerprint.identityData isEqual:[self scannableData:self.theirFingerprintData]]) { + OWSLogWarn(@"Verification failed. We have the wrong fingerprint for them"); + NSString *descriptionFormat = NSLocalizedString(@"PRIVACY_VERIFICATION_FAILED_I_HAVE_WRONG_KEY_FOR_THEM", + @"Alert body when verifying with {{contact name}}"); + NSString *description = [NSString stringWithFormat:descriptionFormat, self.theirName]; + *error = OWSErrorWithCodeDescription(OWSErrorCodePrivacyVerificationFailure, description); + return NO; + } + + if (![localFingerprint.identityData isEqual:[self scannableData:self.myFingerprintData]]) { + OWSLogWarn(@"Verification failed. They have the wrong fingerprint for us"); + NSString *descriptionFormat = NSLocalizedString(@"PRIVACY_VERIFICATION_FAILED_THEY_HAVE_WRONG_KEY_FOR_ME", + @"Alert body when verifying with {{contact name}}"); + NSString *description = [NSString stringWithFormat:descriptionFormat, self.theirName]; + *error = OWSErrorWithCodeDescription(OWSErrorCodePrivacyVerificationFailure, description); + return NO; + } + + OWSLogWarn(@"Verification Succeeded."); + return YES; +} + +- (NSString *)text +{ + NSString *myDisplayString = [self stringForFingerprintData:self.myFingerprintData]; + NSString *theirDisplayString = [self stringForFingerprintData:self.theirFingerprintData]; + + if ([theirDisplayString compare:myDisplayString] == NSOrderedAscending) { + return [NSString stringWithFormat:@"%@%@", theirDisplayString, myDisplayString]; + } else { + return [NSString stringWithFormat:@"%@%@", myDisplayString, theirDisplayString]; + } +} + +/** + * Formats numeric fingerprint, 3 lines in groups of 5 digits. + */ +- (NSString *)displayableText +{ + NSString *input = self.text; + + NSMutableArray *lines = [NSMutableArray new]; + + NSUInteger lineLength = self.text.length / 3; + for (uint i = 0; i < 3; i++) { + NSString *line = [input substringWithRange:NSMakeRange(i * lineLength, lineLength)]; + + NSMutableArray *chunks = [NSMutableArray new]; + for (uint i = 0; i < line.length / 5; i++) { + NSString *nextChunk = [line substringWithRange:NSMakeRange(i * 5, 5)]; + [chunks addObject:nextChunk]; + } + [lines addObject:[chunks componentsJoinedByString:@" "]]; + } + + return [lines componentsJoinedByString:@"\n"]; +} + + +- (NSData *)dataFromShort:(uint32_t)aShort +{ + uint8_t bytes[] = { + ((uint8_t)(aShort & 0xFF00) >> 8), + (uint8_t)(aShort & 0x00FF) + }; + + return [NSData dataWithBytes:bytes length:2]; +} + +/** + * An identifier for a mutable public key, belonging to an immutable identifier (stableId). + * + * This method is intended to be somewhat expensive to produce in order to be brute force adverse. + * + * @param stableIdData + * Immutable global identifier e.g. Signal Identifier, an e164 formatted phone number encoded as UTF-8 data + * @param publicKey + * The current public key for + * @return + * All-number textual representation + */ +- (NSData *)dataForStableId:(NSData *)stableIdData publicKey:(NSData *)publicKey +{ + OWSAssertDebug(stableIdData); + OWSAssertDebug(publicKey); + + NSData *versionData = [self dataFromShort:OWSFingerprintHashingVersion]; + NSMutableData *hash = [versionData mutableCopy]; + [hash appendData:publicKey]; + [hash appendData:stableIdData]; + + NSMutableData *_Nullable digestData = [[NSMutableData alloc] initWithLength:CC_SHA512_DIGEST_LENGTH]; + if (!digestData) { + @throw [NSException exceptionWithName:NSGenericException reason:@"Couldn't allocate buffer." userInfo:nil]; + } + for (int i = 0; i < self.hashIterations; i++) { + [hash appendData:publicKey]; + + if (hash.length >= UINT32_MAX) { + @throw [NSException exceptionWithName:@"Oversize Data" reason:@"Oversize hash." userInfo:nil]; + } + + CC_SHA512(hash.bytes, (uint32_t)hash.length, digestData.mutableBytes); + // TODO get rid of this loop-allocation + hash = [digestData mutableCopy]; + } + + return [hash copy]; +} + + +- (NSString *)stringForFingerprintData:(NSData *)data +{ + OWSAssertDebug(data); + + return [NSString stringWithFormat:@"%@%@%@%@%@%@", + [self encodedChunkFromData:data offset:0], + [self encodedChunkFromData:data offset:5], + [self encodedChunkFromData:data offset:10], + [self encodedChunkFromData:data offset:15], + [self encodedChunkFromData:data offset:20], + [self encodedChunkFromData:data offset:25]]; +} + +- (NSString *)encodedChunkFromData:(NSData *)data offset:(uint)offset +{ + OWSAssertDebug(data); + + uint8_t fiveBytes[5]; + [data getBytes:fiveBytes range:NSMakeRange(offset, 5)]; + + int chunk = [self uint64From5Bytes:fiveBytes] % 100000; + return [NSString stringWithFormat:@"%05d", chunk]; +} + +- (int64_t)uint64From5Bytes:(uint8_t[])bytes +{ + int64_t result = ((bytes[0] & 0xffLL) << 32) | + ((bytes[1] & 0xffLL) << 24) | + ((bytes[2] & 0xffLL) << 16) | + ((bytes[3] & 0xffLL) << 8) | + ((bytes[4] & 0xffLL)); + + return result; +} + +- (NSData *)scannableData:(NSData *)data +{ + return [data subdataWithRange:NSMakeRange(0, 32)]; +} + +- (nullable UIImage *)image +{ + FingerprintProtoLogicalFingerprintBuilder *remoteFingerprintBuilder = + [FingerprintProtoLogicalFingerprint builderWithIdentityData:[self scannableData:self.theirFingerprintData]]; + NSError *error; + FingerprintProtoLogicalFingerprint *_Nullable remoteFingerprint = + [remoteFingerprintBuilder buildAndReturnError:&error]; + if (!remoteFingerprint || error) { + OWSFailDebug(@"could not build proto: %@", error); + return nil; + } + + FingerprintProtoLogicalFingerprintBuilder *localFingerprintBuilder = + [FingerprintProtoLogicalFingerprint builderWithIdentityData:[self scannableData:self.myFingerprintData]]; + FingerprintProtoLogicalFingerprint *_Nullable localFingerprint = + [localFingerprintBuilder buildAndReturnError:&error]; + if (!localFingerprint || error) { + OWSFailDebug(@"could not build proto: %@", error); + return nil; + } + + FingerprintProtoLogicalFingerprintsBuilder *logicalFingerprintsBuilder = + [FingerprintProtoLogicalFingerprints builderWithVersion:OWSFingerprintScannableFormatVersion + localFingerprint:localFingerprint + remoteFingerprint:remoteFingerprint]; + + // Build ByteMode QR (Latin-1 encodable data) + NSData *_Nullable fingerprintData = [logicalFingerprintsBuilder buildSerializedDataAndReturnError:&error]; + if (!fingerprintData || error) { + OWSFailDebug(@"could not serialize proto: %@", error); + return nil; + } + + OWSLogDebug(@"Building fingerprint with data: %@", fingerprintData); + + CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"]; + [filter setDefaults]; + [filter setValue:fingerprintData forKey:@"inputMessage"]; + + CIImage *ciImage = [filter outputImage]; + if (!ciImage) { + OWSLogError(@"Failed to create QR image from fingerprint text: %@", self.text); + return nil; + } + + // UIImages backed by a CIImage won't render without antialiasing, so we convert the backign image to a CGImage, + // which can be scaled crisply. + CIContext *context = [CIContext contextWithOptions:nil]; + CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent]; + UIImage *qrImage = [UIImage imageWithCGImage:cgImage]; + CGImageRelease(cgImage); + + return qrImage; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFingerprintBuilder.h b/SignalUtilitiesKit/OWSFingerprintBuilder.h new file mode 100644 index 000000000..0f0b2bb0a --- /dev/null +++ b/SignalUtilitiesKit/OWSFingerprintBuilder.h @@ -0,0 +1,32 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSAccountManager; +@class OWSFingerprint; +@protocol ContactsManagerProtocol; + +@interface OWSFingerprintBuilder : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithAccountManager:(TSAccountManager *)accountManager + contactsManager:(id)contactsManager NS_DESIGNATED_INITIALIZER; + +/** + * Builds a fingerprint combining your current credentials with their most recently accepted credentials. + */ +- (nullable OWSFingerprint *)fingerprintWithTheirSignalId:(NSString *)theirSignalId; + +/** + * Builds a fingerprint combining your current credentials with the specified identity key. + * You can use this to present a new identity key for verification. + */ +- (OWSFingerprint *)fingerprintWithTheirSignalId:(NSString *)theirSignalId theirIdentityKey:(NSData *)theirIdentityKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSFingerprintBuilder.m b/SignalUtilitiesKit/OWSFingerprintBuilder.m new file mode 100644 index 000000000..2b3df6d24 --- /dev/null +++ b/SignalUtilitiesKit/OWSFingerprintBuilder.m @@ -0,0 +1,66 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSFingerprintBuilder.h" +#import "ContactsManagerProtocol.h" +#import "OWSFingerprint.h" +#import "OWSIdentityManager.h" +#import "TSAccountManager.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSFingerprintBuilder () + +@property (nonatomic, readonly) TSAccountManager *accountManager; +@property (nonatomic, readonly) id contactsManager; + +@end + +@implementation OWSFingerprintBuilder + +- (instancetype)initWithAccountManager:(TSAccountManager *)accountManager + contactsManager:(id)contactsManager +{ + self = [super init]; + if (!self) { + return self; + } + + _accountManager = accountManager; + _contactsManager = contactsManager; + + return self; +} + +- (nullable OWSFingerprint *)fingerprintWithTheirSignalId:(NSString *)theirSignalId +{ + NSData *_Nullable theirIdentityKey = [[OWSIdentityManager sharedManager] identityKeyForRecipientId:theirSignalId]; + + if (theirIdentityKey == nil) { + OWSFailDebug(@"Missing their identity key"); + return nil; + } + + return [self fingerprintWithTheirSignalId:theirSignalId theirIdentityKey:theirIdentityKey]; +} + +- (OWSFingerprint *)fingerprintWithTheirSignalId:(NSString *)theirSignalId theirIdentityKey:(NSData *)theirIdentityKey +{ + NSString *theirName = [self.contactsManager displayNameForPhoneIdentifier:theirSignalId]; + + NSString *mySignalId = [self.accountManager localNumber]; + NSData *myIdentityKey = [[OWSIdentityManager sharedManager] identityKeyPair].publicKey; + + return [OWSFingerprint fingerprintWithMyStableId:mySignalId + myIdentityKey:myIdentityKey + theirStableId:theirSignalId + theirIdentityKey:theirIdentityKey + theirName:theirName]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSGroupsOutputStream.h b/SignalUtilitiesKit/OWSGroupsOutputStream.h new file mode 100644 index 000000000..f45a65bba --- /dev/null +++ b/SignalUtilitiesKit/OWSGroupsOutputStream.h @@ -0,0 +1,18 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSChunkedOutputStream.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSGroupThread; +@class YapDatabaseReadTransaction; + +@interface OWSGroupsOutputStream : OWSChunkedOutputStream + +- (void)writeGroup:(TSGroupThread *)groupThread transaction:(YapDatabaseReadTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSGroupsOutputStream.m b/SignalUtilitiesKit/OWSGroupsOutputStream.m new file mode 100644 index 000000000..7c7a2af23 --- /dev/null +++ b/SignalUtilitiesKit/OWSGroupsOutputStream.m @@ -0,0 +1,85 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSGroupsOutputStream.h" +#import "MIMETypeUtil.h" +#import "OWSBlockingManager.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import "TSGroupModel.h" +#import "TSGroupThread.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSGroupsOutputStream + +- (void)writeGroup:(TSGroupThread *)groupThread transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(groupThread); + OWSAssertDebug(transaction); + + TSGroupModel *group = groupThread.groupModel; + OWSAssertDebug(group); + + SSKProtoGroupDetailsBuilder *groupBuilder = [SSKProtoGroupDetails builderWithId:group.groupId]; + [groupBuilder setName:group.groupName]; + [groupBuilder setMembers:group.groupMemberIds]; + [groupBuilder setColor:groupThread.conversationColorName]; + [groupBuilder setAdmins:group.groupAdminIds]; + + if ([OWSBlockingManager.sharedManager isGroupIdBlocked:group.groupId]) { + [groupBuilder setBlocked:YES]; + } + /* + NSData *avatarPng; + if (group.groupImage) { + SSKProtoGroupDetailsAvatarBuilder *avatarBuilder = [SSKProtoGroupDetailsAvatar builder]; + + [avatarBuilder setContentType:OWSMimeTypeImagePng]; + avatarPng = UIImagePNGRepresentation(group.groupImage); + [avatarBuilder setLength:(uint32_t)avatarPng.length]; + + NSError *error; + SSKProtoGroupDetailsAvatar *_Nullable avatarProto = [avatarBuilder buildAndReturnError:&error]; + if (error || !avatarProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + } else { + [groupBuilder setAvatar:avatarProto]; + } + } */ + + OWSDisappearingMessagesConfiguration *_Nullable disappearingMessagesConfiguration = + [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:groupThread.uniqueId transaction:transaction]; + + if (disappearingMessagesConfiguration && disappearingMessagesConfiguration.isEnabled) { + [groupBuilder setExpireTimer:disappearingMessagesConfiguration.durationSeconds]; + } else { + // Rather than *not* set the field, we expicitly set it to 0 so desktop + // can easily distinguish between a modern client declaring "off" vs a + // legacy client "not specifying". + [groupBuilder setExpireTimer:0]; + } + + NSError *error; + NSData *_Nullable groupData = [groupBuilder buildSerializedDataAndReturnError:&error]; + if (error || !groupData) { + OWSFailDebug(@"could not serialize protobuf: %@", error); + return; + } + + uint32_t groupDataLength = (uint32_t)groupData.length; + + [self writeUInt32:groupDataLength]; + [self writeData:groupData]; + + /* + if (avatarPng) { + [self writeData:avatarPng]; + } + */ +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSHTTPSecurityPolicy.h b/SignalUtilitiesKit/OWSHTTPSecurityPolicy.h new file mode 100644 index 000000000..2980c6112 --- /dev/null +++ b/SignalUtilitiesKit/OWSHTTPSecurityPolicy.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSData *SSKTextSecureServiceCertificateData(void); + +@interface OWSHTTPSecurityPolicy : AFSecurityPolicy + ++ (instancetype)sharedPolicy; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSHTTPSecurityPolicy.m b/SignalUtilitiesKit/OWSHTTPSecurityPolicy.m new file mode 100644 index 000000000..951a53bed --- /dev/null +++ b/SignalUtilitiesKit/OWSHTTPSecurityPolicy.m @@ -0,0 +1,106 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSHTTPSecurityPolicy.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSHTTPSecurityPolicy + ++ (instancetype)sharedPolicy { + static OWSHTTPSecurityPolicy *httpSecurityPolicy = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + httpSecurityPolicy = [[self alloc] initWithOWSPolicy]; + }); + return httpSecurityPolicy; +} + +- (instancetype)initWithOWSPolicy { + self = [[super class] defaultPolicy]; + + if (self) { + self.pinnedCertificates = [NSSet setWithArray:@[ + [self.class certificateDataForService:@"textsecure"] + ]]; + } + + return self; +} + ++ (NSData *)dataFromCertificateFileForService:(NSString *)service +{ + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + NSString *path = [bundle pathForResource:service ofType:@"cer"]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + OWSFail(@"Missing signing certificate for service %@", service); + } + + NSData *data = [NSData dataWithContentsOfFile:path]; + OWSAssert(data.length > 0); + + return data; +} + ++ (NSData *)certificateDataForService:(NSString *)service { + SecCertificateRef certRef = [self certificateForService:service]; + return (__bridge_transfer NSData *)SecCertificateCopyData(certRef); +} + ++ (SecCertificateRef)certificateForService:(NSString *)service +{ + NSData *certificateData = [self dataFromCertificateFileForService:service]; + return SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(certificateData)); +} + +- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(nullable NSString *)domain +{ + NSMutableArray *policies = [NSMutableArray array]; + [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)]; + + if (SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies) != errSecSuccess) { + OWSLogError(@"The trust policy couldn't be set."); + return NO; + } + + NSMutableArray *pinnedCertificates = [NSMutableArray array]; + for (NSData *certificateData in self.pinnedCertificates) { + [pinnedCertificates + addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)]; + } + + if (SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates) != errSecSuccess) { + OWSLogError(@"The anchor certificates couldn't be set."); + return NO; + } + + if (!AFServerTrustIsValid(serverTrust)) { + return NO; + } + + return YES; +} + +static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) { + BOOL isValid = NO; + SecTrustResultType result; + __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out); + + isValid = (result == kSecTrustResultUnspecified); + +_out: + return isValid; +} + +NSData *SSKTextSecureServiceCertificateData() +{ + return [OWSHTTPSecurityPolicy dataFromCertificateFileForService:@"textsecure"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIdentityManager.h b/SignalUtilitiesKit/OWSIdentityManager.h new file mode 100644 index 000000000..cda40de9c --- /dev/null +++ b/SignalUtilitiesKit/OWSIdentityManager.h @@ -0,0 +1,92 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSRecipientIdentity.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const OWSPrimaryStorageIdentityKeyStoreIdentityKey; +extern NSString *const LKSeedKey; +extern NSString *const LKED25519SecretKey; +extern NSString *const LKED25519PublicKey; +extern NSString *const OWSPrimaryStorageIdentityKeyStoreCollection; + +extern NSString *const OWSPrimaryStorageTrustedKeysCollection; + +// This notification will be fired whenever identities are created +// or their verification state changes. +extern NSString *const kNSNotificationName_IdentityStateDidChange; + +// number of bytes in a signal identity key, excluding the key-type byte. +extern const NSUInteger kIdentityKeyLength; + +#ifdef DEBUG +extern const NSUInteger kStoredIdentityKeyLength; +#endif + +@class OWSRecipientIdentity; +@class OWSStorage; +@class SSKProtoVerified; +@class YapDatabaseReadWriteTransaction; + +// This class can be safely accessed and used from any thread. +@interface OWSIdentityManager : NSObject + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + ++ (instancetype)sharedManager; + +- (void)generateNewIdentityKeyPair; +- (void)generateNewIdentityKeyPairFromSeed:(NSData *)seed; +- (void)clearIdentityKey; + +- (void)setVerificationState:(OWSVerificationState)verificationState + identityKey:(NSData *)identityKey + recipientId:(NSString *)recipientId + isUserInitiatedChange:(BOOL)isUserInitiatedChange + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (OWSVerificationState)verificationStateForRecipientId:(NSString *)recipientId; +- (OWSVerificationState)verificationStateForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadTransaction *)transaction; + +- (void)setVerificationState:(OWSVerificationState)verificationState + identityKey:(NSData *)identityKey + recipientId:(NSString *)recipientId + isUserInitiatedChange:(BOOL)isUserInitiatedChange; + +- (nullable OWSRecipientIdentity *)recipientIdentityForRecipientId:(NSString *)recipientId; + +/** + * @param recipientId unique stable identifier for the recipient, e.g. e164 phone number + * @returns nil if the recipient does not exist, or is trusted for sending + * else returns the untrusted recipient. + */ +- (nullable OWSRecipientIdentity *)untrustedIdentityForSendingToRecipientId:(NSString *)recipientId; + +// This method can be called from any thread. +- (void)throws_processIncomingSyncMessage:(SSKProtoVerified *)verified + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (BOOL)saveRemoteIdentity:(NSData *)identityKey recipientId:(NSString *)recipientId; + +- (nullable ECKeyPair *)identityKeyPair; + +#pragma mark - Debug + +#if DEBUG +// Clears everything except the local identity key. +- (void)clearIdentityState:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)snapshotIdentityState:(YapDatabaseReadWriteTransaction *)transaction; +- (void)restoreIdentityState:(YapDatabaseReadWriteTransaction *)transaction; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIdentityManager.m b/SignalUtilitiesKit/OWSIdentityManager.m new file mode 100644 index 000000000..d582a2f84 --- /dev/null +++ b/SignalUtilitiesKit/OWSIdentityManager.m @@ -0,0 +1,977 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSIdentityManager.h" +#import "AppContext.h" +#import "AppReadiness.h" +#import "NSNotificationCenter+OWS.h" +#import "NotificationsProtocol.h" +#import "OWSError.h" +#import "OWSFileSystem.h" +#import "OWSMessageSender.h" +#import "OWSOutgoingNullMessage.h" +#import "OWSPrimaryStorage+sessionStore.h" +#import "OWSPrimaryStorage.h" +#import "OWSRecipientIdentity.h" +#import "OWSVerificationStateChangeMessage.h" +#import "OWSVerificationStateSyncMessage.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSContactThread.h" +#import "TSErrorMessage.h" +#import "TSGroupThread.h" +#import "YapDatabaseConnection+OWS.h" +#import "YapDatabaseTransaction+OWS.h" +#import +#import +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +// Storing our own identity key +NSString *const OWSPrimaryStorageIdentityKeyStoreIdentityKey = @"TSStorageManagerIdentityKeyStoreIdentityKey"; +NSString *const LKSeedKey = @"LKLokiSeed"; +NSString *const LKED25519SecretKey = @"LKED25519SecretKey"; +NSString *const LKED25519PublicKey = @"LKED25519PublicKey"; +NSString *const OWSPrimaryStorageIdentityKeyStoreCollection = @"TSStorageManagerIdentityKeyStoreCollection"; + +// Storing recipients identity keys +NSString *const OWSPrimaryStorageTrustedKeysCollection = @"TSStorageManagerTrustedKeysCollection"; + +NSString *const OWSIdentityManager_QueuedVerificationStateSyncMessages = + @"OWSIdentityManager_QueuedVerificationStateSyncMessages"; + +// Don't trust an identity for sending to unless they've been around for at least this long +const NSTimeInterval kIdentityKeyStoreNonBlockingSecondsThreshold = 5.0; + +// The canonical key includes 32 bytes of identity material plus one byte specifying the key type +const NSUInteger kIdentityKeyLength = 33; + +// Cryptographic operations do not use the "type" byte of the identity key, so, for legacy reasons we store just +// the identity material. +// TODO: migrate to storing the full 33 byte representation. +const NSUInteger kStoredIdentityKeyLength = 32; + +NSString *const kNSNotificationName_IdentityStateDidChange = @"kNSNotificationName_IdentityStateDidChange"; + +@interface OWSIdentityManager () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; + +@end + +#pragma mark - + +@implementation OWSIdentityManager + ++ (instancetype)sharedManager +{ + OWSAssertDebug(SSKEnvironment.shared.identityManager); + + return SSKEnvironment.shared.identityManager; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSAssertDebug(primaryStorage); + + _primaryStorage = primaryStorage; + _dbConnection = primaryStorage.newDatabaseConnection; + self.dbConnection.objectCacheEnabled = NO; + + OWSSingletonAssert(); + + [self observeNotifications]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Dependencies + +- (OWSMessageSender *)messageSender +{ + OWSAssertDebug(SSKEnvironment.shared.messageSender); + + return SSKEnvironment.shared.messageSender; +} + +#pragma mark - + +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; +} + +- (void)generateNewIdentityKeyPair +{ + ECKeyPair *keyPair = [Curve25519 generateKeyPair]; + [self.dbConnection setObject:keyPair forKey:OWSPrimaryStorageIdentityKeyStoreIdentityKey inCollection:OWSPrimaryStorageIdentityKeyStoreCollection]; +} + +- (void)generateNewIdentityKeyPairFromSeed:(NSData *)seed +{ + ECKeyPair *keyPair = nil;//[Curve25519 generateKeyPairFromSeed:seed]; + [self.dbConnection setObject:keyPair forKey:OWSPrimaryStorageIdentityKeyStoreIdentityKey inCollection:OWSPrimaryStorageIdentityKeyStoreCollection]; +} + +- (void)clearIdentityKey +{ + [self.dbConnection removeObjectForKey:OWSPrimaryStorageIdentityKeyStoreIdentityKey + inCollection:OWSPrimaryStorageIdentityKeyStoreCollection]; +} + +- (nullable NSData *)identityKeyForRecipientId:(NSString *)recipientId +{ + __block NSData *_Nullable result = nil; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + result = [self identityKeyForRecipientId:recipientId transaction:transaction]; + }]; + return result; +} + +- (nullable NSData *)identityKeyForRecipientId:(NSString *)recipientId protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadTransaction class]]); + + YapDatabaseReadTransaction *transaction = protocolContext; + + return [self identityKeyForRecipientId:recipientId transaction:transaction]; +} + +- (nullable NSData *)identityKeyForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + return [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction].identityKey; +} + +- (nullable ECKeyPair *)identityKeyPair +{ + __block ECKeyPair *_Nullable identityKeyPair = nil; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + identityKeyPair = [self identityKeyPairWithTransaction:transaction]; + }]; + return identityKeyPair; +} + +// This method should only be called from SignalProtocolKit, which doesn't know about YapDatabaseTransactions. +// Whenever possible, prefer to call the strongly typed variant: `identityKeyPairWithTransaction:`. +- (nullable ECKeyPair *)identityKeyPair:(nullable id)protocolContext +{ + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadTransaction class]]); + + YapDatabaseReadTransaction *transaction = protocolContext; + + return [self identityKeyPairWithTransaction:transaction]; +} + +- (nullable ECKeyPair *)identityKeyPairWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + ECKeyPair *_Nullable identityKeyPair = [transaction keyPairForKey:OWSPrimaryStorageIdentityKeyStoreIdentityKey + inCollection:OWSPrimaryStorageIdentityKeyStoreCollection]; + return identityKeyPair; +} + +- (int)localRegistrationId:(nullable id)protocolContext +{ + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + return (int)[TSAccountManager getOrGenerateRegistrationId:transaction]; +} + +- (BOOL)saveRemoteIdentity:(NSData *)identityKey recipientId:(NSString *)recipientId +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + OWSAssertDebug(recipientId.length > 0); + + __block BOOL result; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + result = [self saveRemoteIdentity:identityKey recipientId:recipientId protocolContext:transaction]; + }]; + + return result; +} + +- (BOOL)saveRemoteIdentity:(NSData *)identityKey + recipientId:(NSString *)recipientId + protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + // Deprecated. We actually no longer use the OWSPrimaryStorageTrustedKeysCollection for trust + // decisions, but it's desirable to try to keep it up to date with our trusted identitys + // while we're switching between versions, e.g. so we don't get into a state where we have a + // session for an identity not in our key store. + [transaction setObject:identityKey forKey:recipientId inCollection:OWSPrimaryStorageTrustedKeysCollection]; + + OWSRecipientIdentity *existingIdentity = + [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction]; + + if (existingIdentity == nil) { + OWSLogInfo(@"saving first use identity for recipient: %@", recipientId); + [[[OWSRecipientIdentity alloc] initWithRecipientId:recipientId + identityKey:identityKey + isFirstKnownKey:YES + createdAt:[NSDate new] + verificationState:OWSVerificationStateDefault] + saveWithTransaction:transaction]; + + // Cancel any pending verification state sync messages for this recipient. + [self clearSyncMessageForRecipientId:recipientId transaction:transaction]; + + [self fireIdentityStateChangeNotification]; + + return NO; + } + + if (![existingIdentity.identityKey isEqual:identityKey]) { + OWSVerificationState verificationState; + switch (existingIdentity.verificationState) { + case OWSVerificationStateDefault: + verificationState = OWSVerificationStateDefault; + break; + case OWSVerificationStateVerified: + case OWSVerificationStateNoLongerVerified: + verificationState = OWSVerificationStateNoLongerVerified; + break; + } + + OWSLogInfo(@"replacing identity for existing recipient: %@ (%@ -> %@)", + recipientId, + OWSVerificationStateToString(existingIdentity.verificationState), + OWSVerificationStateToString(verificationState)); + [self createIdentityChangeInfoMessageForRecipientId:recipientId transaction:transaction]; + + [[[OWSRecipientIdentity alloc] initWithRecipientId:recipientId + identityKey:identityKey + isFirstKnownKey:NO + createdAt:[NSDate new] + verificationState:verificationState] saveWithTransaction:transaction]; + + [self.primaryStorage archiveAllSessionsForContact:recipientId protocolContext:protocolContext]; + + // Cancel any pending verification state sync messages for this recipient. + [self clearSyncMessageForRecipientId:recipientId transaction:transaction]; + + [self fireIdentityStateChangeNotification]; + + return YES; + } + + return NO; +} + +- (void)setVerificationState:(OWSVerificationState)verificationState + identityKey:(NSData *)identityKey + recipientId:(NSString *)recipientId + isUserInitiatedChange:(BOOL)isUserInitiatedChange +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + OWSAssertDebug(recipientId.length > 0); + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self setVerificationState:verificationState + identityKey:identityKey + recipientId:recipientId + isUserInitiatedChange:isUserInitiatedChange + transaction:transaction]; + }]; +} + +- (void)setVerificationState:(OWSVerificationState)verificationState + identityKey:(NSData *)identityKey + recipientId:(NSString *)recipientId + isUserInitiatedChange:(BOOL)isUserInitiatedChange + protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + [self setVerificationState:verificationState + identityKey:identityKey + recipientId:recipientId + isUserInitiatedChange:isUserInitiatedChange + transaction:transaction]; +} + +- (void)setVerificationState:(OWSVerificationState)verificationState + identityKey:(NSData *)identityKey + recipientId:(NSString *)recipientId + isUserInitiatedChange:(BOOL)isUserInitiatedChange + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + // Ensure a remote identity exists for this key. We may be learning about + // it for the first time. + [self saveRemoteIdentity:identityKey recipientId:recipientId protocolContext:transaction]; + + OWSRecipientIdentity *recipientIdentity = + [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction]; + + if (recipientIdentity == nil) { + OWSFailDebug(@"Missing expected identity: %@", recipientId); + return; + } + + if (recipientIdentity.verificationState == verificationState) { + return; + } + + OWSLogInfo(@"setVerificationState: %@ (%@ -> %@)", + recipientId, + OWSVerificationStateToString(recipientIdentity.verificationState), + OWSVerificationStateToString(verificationState)); + + [recipientIdentity updateWithVerificationState:verificationState transaction:transaction]; + + if (isUserInitiatedChange) { + [self saveChangeMessagesForRecipientId:recipientId + verificationState:verificationState + isLocalChange:YES + transaction:transaction]; + [self enqueueSyncMessageForVerificationStateForRecipientId:recipientId transaction:transaction]; + } else { + // Cancel any pending verification state sync messages for this recipient. + [self clearSyncMessageForRecipientId:recipientId transaction:transaction]; + } + + [self fireIdentityStateChangeNotification]; +} + +- (OWSVerificationState)verificationStateForRecipientId:(NSString *)recipientId +{ + __block OWSVerificationState result; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + result = [self verificationStateForRecipientId:recipientId transaction:transaction]; + }]; + return result; +} + +- (OWSVerificationState)verificationStateForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + OWSRecipientIdentity *_Nullable currentIdentity = + [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction]; + + if (!currentIdentity) { + // We might not know the identity for this recipient yet. + return OWSVerificationStateDefault; + } + + return currentIdentity.verificationState; +} + +- (nullable OWSRecipientIdentity *)recipientIdentityForRecipientId:(NSString *)recipientId +{ + OWSAssertDebug(recipientId.length > 0); + + __block OWSRecipientIdentity *_Nullable result; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + result = [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction]; + }]; + return result; +} + +- (nullable OWSRecipientIdentity *)untrustedIdentityForSendingToRecipientId:(NSString *)recipientId +{ + OWSAssertDebug(recipientId.length > 0); + + __block OWSRecipientIdentity *_Nullable result; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + OWSRecipientIdentity *_Nullable recipientIdentity = + [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction]; + + if (recipientIdentity == nil) { + // trust on first use + return; + } + + BOOL isTrusted = [self isTrustedIdentityKey:recipientIdentity.identityKey + recipientId:recipientId + direction:TSMessageDirectionOutgoing + transaction:transaction]; + if (isTrusted) { + return; + } else { + result = recipientIdentity; + } + }]; + return result; +} + +- (void)fireIdentityStateChangeNotification +{ + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_IdentityStateDidChange + object:nil + userInfo:nil]; +} + +- (BOOL)isTrustedIdentityKey:(NSData *)identityKey + recipientId:(NSString *)recipientId + direction:(TSMessageDirection)direction + protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(direction != TSMessageDirectionUnknown); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + return [self isTrustedIdentityKey:identityKey recipientId:recipientId direction:direction transaction:transaction]; +} + +- (BOOL)isTrustedIdentityKey:(NSData *)identityKey + recipientId:(NSString *)recipientId + direction:(TSMessageDirection)direction + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(direction != TSMessageDirectionUnknown); + OWSAssertDebug(transaction); + + if ([[TSAccountManager localNumber] isEqualToString:recipientId]) { + ECKeyPair *_Nullable localIdentityKeyPair = [self identityKeyPairWithTransaction:transaction]; + + if ([localIdentityKeyPair.publicKey isEqualToData:identityKey]) { + return YES; + } else { + OWSFailDebug(@"Wrong identity: %@ for local key: %@, recipientId: %@", + identityKey, + localIdentityKeyPair.publicKey, + recipientId); + return NO; + } + } + + switch (direction) { + case TSMessageDirectionIncoming: { + return YES; + } + case TSMessageDirectionOutgoing: { + OWSRecipientIdentity *existingIdentity = + [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction]; + return [self isTrustedKey:identityKey forSendingToIdentity:existingIdentity]; + } + default: { + OWSFailDebug(@"unexpected message direction: %ld", (long)direction); + return NO; + } + } +} + +- (BOOL)isTrustedKey:(NSData *)identityKey forSendingToIdentity:(nullable OWSRecipientIdentity *)recipientIdentity +{ + OWSAssertDebug(identityKey.length == kStoredIdentityKeyLength); + + if (recipientIdentity == nil) { + return YES; + } + + OWSAssertDebug(recipientIdentity.identityKey.length == kStoredIdentityKeyLength); + if (![recipientIdentity.identityKey isEqualToData:identityKey]) { + OWSLogWarn(@"key mismatch for recipient: %@", recipientIdentity.recipientId); + return NO; + } + + if ([recipientIdentity isFirstKnownKey]) { + return YES; + } + + switch (recipientIdentity.verificationState) { + case OWSVerificationStateDefault: { + BOOL isNew = (fabs([recipientIdentity.createdAt timeIntervalSinceNow]) + < kIdentityKeyStoreNonBlockingSecondsThreshold); + if (isNew) { + OWSLogWarn(@"not trusting new identity for recipient: %@", recipientIdentity.recipientId); + return NO; + } else { + return YES; + } + } + case OWSVerificationStateVerified: + return YES; + case OWSVerificationStateNoLongerVerified: + OWSLogWarn(@"not trusting no longer verified identity for recipient: %@", recipientIdentity.recipientId); + return NO; + } +} + +- (void)createIdentityChangeInfoMessageForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + NSMutableArray *messages = [NSMutableArray new]; + + TSContactThread *contactThread = + [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction]; + OWSAssertDebug(contactThread != nil); + + TSErrorMessage *errorMessage = + [TSErrorMessage nonblockingIdentityChangeInThread:contactThread recipientId:recipientId]; + [messages addObject:errorMessage]; + + for (TSGroupThread *groupThread in [TSGroupThread groupThreadsWithRecipientId:recipientId transaction:transaction]) { + [messages addObject:[TSErrorMessage nonblockingIdentityChangeInThread:groupThread recipientId:recipientId]]; + } + + // MJK TODO - why not save immediately, why build up this array? + for (TSMessage *message in messages) { + [message saveWithTransaction:transaction]; + } + + [SSKEnvironment.shared.notificationsManager notifyUserForErrorMessage:errorMessage + thread:contactThread + transaction:transaction]; +} + +- (void)enqueueSyncMessageForVerificationStateForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + [transaction setObject:recipientId + forKey:recipientId + inCollection:OWSIdentityManager_QueuedVerificationStateSyncMessages]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self tryToSyncQueuedVerificationStates]; + }); +} + +- (void)tryToSyncQueuedVerificationStates +{ + OWSAssertIsOnMainThread(); + + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [self syncQueuedVerificationStates]; + }]; +} + +- (void)syncQueuedVerificationStates +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSMutableArray *recipientIds = [NSMutableArray new]; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [transaction + enumerateKeysAndObjectsInCollection:OWSIdentityManager_QueuedVerificationStateSyncMessages + usingBlock:^( + NSString *_Nonnull recipientId, id _Nonnull object, BOOL *_Nonnull stop) { + [recipientIds addObject:recipientId]; + }]; + }]; + + NSMutableArray *messages = [NSMutableArray new]; + for (NSString *recipientId in recipientIds) { + OWSRecipientIdentity *recipientIdentity = [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId]; + if (!recipientIdentity) { + OWSFailDebug(@"Could not load recipient identity for recipientId: %@", recipientId); + continue; + } + if (recipientIdentity.recipientId.length < 1) { + OWSFailDebug(@"Invalid recipient identity for recipientId: %@", recipientId); + continue; + } + + // Prepend key type for transit. + // TODO we should just be storing the key type so we don't have to juggle re-adding it. + NSData *identityKey = [recipientIdentity.identityKey prependKeyType]; + if (identityKey.length != kIdentityKeyLength) { + OWSFailDebug(@"Invalid recipient identitykey for recipientId: %@ key: %@", recipientId, identityKey); + continue; + } + if (recipientIdentity.verificationState == OWSVerificationStateNoLongerVerified) { + // We don't want to sync "no longer verified" state. Other clients can + // figure this out from the /profile/ endpoint, and this can cause data + // loss as a user's devices overwrite each other's verification. + OWSFailDebug(@"Queue verification state had unexpected value: %@ recipientId: %@", + OWSVerificationStateToString(recipientIdentity.verificationState), + recipientId); + continue; + } + OWSVerificationStateSyncMessage *message = + [[OWSVerificationStateSyncMessage alloc] initWithVerificationState:recipientIdentity.verificationState + identityKey:identityKey + verificationForRecipientId:recipientIdentity.recipientId]; + [messages addObject:message]; + } + if (messages.count > 0) { + for (OWSVerificationStateSyncMessage *message in messages) { + [self sendSyncVerificationStateMessage:message]; + } + } + }); +} + +- (void)sendSyncVerificationStateMessage:(OWSVerificationStateSyncMessage *)message +{ + OWSAssertDebug(message); + OWSAssertDebug(message.verificationForRecipientId.length > 0); + + TSContactThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:message.verificationForRecipientId]; + + // Send null message to appear as though we're sending a normal message to cover the sync messsage sent + // subsequently + OWSOutgoingNullMessage *nullMessage = [[OWSOutgoingNullMessage alloc] initWithContactThread:contactThread + verificationStateSyncMessage:message]; + + // DURABLE CLEANUP - we could replace the custom durability logic in this class + // with a durable JobQueue. + [self.messageSender sendMessage:nullMessage + success:^{ + OWSLogInfo(@"Successfully sent verification state NullMessage"); + [self.messageSender sendMessage:message + success:^{ + OWSLogInfo(@"Successfully sent verification state sync message"); + + // Record that this verification state was successfully synced. + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self clearSyncMessageForRecipientId:message.verificationForRecipientId + transaction:transaction]; + }]; + } + failure:^(NSError *error) { + OWSLogError(@"Failed to send verification state sync message with error: %@", error); + }]; + } + failure:^(NSError *_Nonnull error) { + OWSLogError(@"Failed to send verification state NullMessage with error: %@", error); + if (error.code == OWSErrorCodeNoSuchSignalRecipient) { + OWSLogInfo(@"Removing retries for syncing verification state, since user is no longer registered: %@", + message.verificationForRecipientId); + // Otherwise this will fail forever. + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self clearSyncMessageForRecipientId:message.verificationForRecipientId transaction:transaction]; + }]; + } + }]; +} + +- (void)clearSyncMessageForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + [transaction removeObjectForKey:recipientId inCollection:OWSIdentityManager_QueuedVerificationStateSyncMessages]; +} + +- (void)throws_processIncomingSyncMessage:(SSKProtoVerified *)verified + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(verified); + OWSAssertDebug(transaction); + + NSString *recipientId = verified.destination; + if (recipientId.length < 1) { + OWSFailDebug(@"Verification state sync message missing recipientId."); + return; + } + NSData *rawIdentityKey = verified.identityKey; + if (rawIdentityKey.length != kIdentityKeyLength) { + OWSFailDebug(@"Verification state sync message for recipient: %@ with malformed identityKey: %@", + recipientId, + rawIdentityKey); + return; + } + NSData *identityKey = [rawIdentityKey throws_removeKeyType]; + + switch (verified.state) { + case SSKProtoVerifiedStateDefault: + [self tryToApplyVerificationStateFromSyncMessage:OWSVerificationStateDefault + recipientId:recipientId + identityKey:identityKey + overwriteOnConflict:NO + transaction:transaction]; + break; + case SSKProtoVerifiedStateVerified: + [self tryToApplyVerificationStateFromSyncMessage:OWSVerificationStateVerified + recipientId:recipientId + identityKey:identityKey + overwriteOnConflict:YES + transaction:transaction]; + break; + case SSKProtoVerifiedStateUnverified: + OWSFailDebug(@"Verification state sync message for recipientId: %@ has unexpected value: %@.", + recipientId, + OWSVerificationStateToString(OWSVerificationStateNoLongerVerified)); + return; + } + + [self fireIdentityStateChangeNotification]; +} + +- (void)tryToApplyVerificationStateFromSyncMessage:(OWSVerificationState)verificationState + recipientId:(NSString *)recipientId + identityKey:(NSData *)identityKey + overwriteOnConflict:(BOOL)overwriteOnConflict + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + if (recipientId.length < 1) { + OWSFailDebug(@"Verification state sync message missing recipientId."); + return; + } + + if (identityKey.length != kStoredIdentityKeyLength) { + OWSFailDebug(@"Verification state sync message missing identityKey: %@", recipientId); + return; + } + + OWSRecipientIdentity *_Nullable recipientIdentity = [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId + transaction:transaction]; + if (!recipientIdentity) { + // There's no existing recipient identity for this recipient. + // We should probably create one. + + if (verificationState == OWSVerificationStateDefault) { + // There's no point in creating a new recipient identity just to + // set its verification state to default. + return; + } + + // Ensure a remote identity exists for this key. We may be learning about + // it for the first time. + [self saveRemoteIdentity:identityKey recipientId:recipientId protocolContext:transaction]; + + recipientIdentity = [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId + transaction:transaction]; + + if (recipientIdentity == nil) { + OWSFailDebug(@"Missing expected identity: %@", recipientId); + return; + } + + if (![recipientIdentity.recipientId isEqualToString:recipientId]) { + OWSFailDebug(@"recipientIdentity has unexpected recipientId: %@", recipientId); + return; + } + + if (![recipientIdentity.identityKey isEqualToData:identityKey]) { + OWSFailDebug(@"recipientIdentity has unexpected identityKey: %@", recipientId); + return; + } + + if (recipientIdentity.verificationState == verificationState) { + return; + } + + OWSLogInfo(@"setVerificationState: %@ (%@ -> %@)", + recipientId, + OWSVerificationStateToString(recipientIdentity.verificationState), + OWSVerificationStateToString(verificationState)); + + [recipientIdentity updateWithVerificationState:verificationState + transaction:transaction]; + + // No need to call [saveChangeMessagesForRecipientId:..] since this is + // a new recipient. + } else { + // There's an existing recipient identity for this recipient. + // We should update it. + if (![recipientIdentity.recipientId isEqualToString:recipientId]) { + OWSFailDebug(@"recipientIdentity has unexpected recipientId: %@", recipientId); + return; + } + + if (![recipientIdentity.identityKey isEqualToData:identityKey]) { + // The conflict case where we receive a verification sync message + // whose identity key disagrees with the local identity key for + // this recipient. + if (!overwriteOnConflict) { + OWSLogWarn(@"recipientIdentity has non-matching identityKey: %@", recipientId); + return; + } + + OWSLogWarn(@"recipientIdentity has non-matching identityKey; overwriting: %@", recipientId); + [self saveRemoteIdentity:identityKey recipientId:recipientId protocolContext:transaction]; + + recipientIdentity = [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId + transaction:transaction]; + + if (recipientIdentity == nil) { + OWSFailDebug(@"Missing expected identity: %@", recipientId); + return; + } + + if (![recipientIdentity.recipientId isEqualToString:recipientId]) { + OWSFailDebug(@"recipientIdentity has unexpected recipientId: %@", recipientId); + return; + } + + if (![recipientIdentity.identityKey isEqualToData:identityKey]) { + OWSFailDebug(@"recipientIdentity has unexpected identityKey: %@", recipientId); + return; + } + } + + if (recipientIdentity.verificationState == verificationState) { + return; + } + + [recipientIdentity updateWithVerificationState:verificationState + transaction:transaction]; + + [self saveChangeMessagesForRecipientId:recipientId + verificationState:verificationState + isLocalChange:NO + transaction:transaction]; + } +} + +// We only want to create change messages in response to user activity, +// on any of their devices. +- (void)saveChangeMessagesForRecipientId:(NSString *)recipientId + verificationState:(OWSVerificationState)verificationState + isLocalChange:(BOOL)isLocalChange + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + NSMutableArray *messages = [NSMutableArray new]; + + TSContactThread *contactThread = + [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction]; + OWSAssertDebug(contactThread); + // MJK TODO - should be safe to remove senderTimestamp + [messages addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + thread:contactThread + recipientId:recipientId + verificationState:verificationState + isLocalChange:isLocalChange]]; + + for (TSGroupThread *groupThread in + [TSGroupThread groupThreadsWithRecipientId:recipientId transaction:transaction]) { + // MJK TODO - should be safe to remove senderTimestamp + [messages + addObject:[[OWSVerificationStateChangeMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + thread:groupThread + recipientId:recipientId + verificationState:verificationState + isLocalChange:isLocalChange]]; + } + + // MJK TODO - why not save in-line, vs storing in an array and saving the array? + for (TSMessage *message in messages) { + [message saveWithTransaction:transaction]; + } +} + +#pragma mark - Debug + +#if DEBUG +- (void)clearIdentityState:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *identityKeysToRemove = [NSMutableArray new]; + [transaction enumerateKeysInCollection:OWSPrimaryStorageIdentityKeyStoreCollection + usingBlock:^(NSString *_Nonnull key, BOOL *_Nonnull stop) { + if ([key isEqualToString:OWSPrimaryStorageIdentityKeyStoreIdentityKey]) { + // Don't delete our own key. + return; + } + [identityKeysToRemove addObject:key]; + }]; + for (NSString *key in identityKeysToRemove) { + [transaction removeObjectForKey:key inCollection:OWSPrimaryStorageIdentityKeyStoreCollection]; + } + [transaction removeAllObjectsInCollection:OWSPrimaryStorageTrustedKeysCollection]; +} + +- (NSString *)identityKeySnapshotFilePath +{ + // Prefix name with period "." so that backups will ignore these snapshots. + NSString *dirPath = [OWSFileSystem appDocumentDirectoryPath]; + return [dirPath stringByAppendingPathComponent:@".identity-key-snapshot"]; +} + +- (NSString *)trustedKeySnapshotFilePath +{ + // Prefix name with period "." so that backups will ignore these snapshots. + NSString *dirPath = [OWSFileSystem appDocumentDirectoryPath]; + return [dirPath stringByAppendingPathComponent:@".trusted-key-snapshot"]; +} + +- (void)snapshotIdentityState:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [transaction snapshotCollection:OWSPrimaryStorageIdentityKeyStoreCollection + snapshotFilePath:self.identityKeySnapshotFilePath]; + [transaction snapshotCollection:OWSPrimaryStorageTrustedKeysCollection + snapshotFilePath:self.trustedKeySnapshotFilePath]; +} + +- (void)restoreIdentityState:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [transaction restoreSnapshotOfCollection:OWSPrimaryStorageIdentityKeyStoreCollection + snapshotFilePath:self.identityKeySnapshotFilePath]; + [transaction restoreSnapshotOfCollection:OWSPrimaryStorageTrustedKeysCollection + snapshotFilePath:self.trustedKeySnapshotFilePath]; +} + +#endif + +#pragma mark - Notifications + +- (void)applicationDidBecomeActive:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + // We want to defer this so that we never call this method until + // [UIApplicationDelegate applicationDidBecomeActive:] is complete. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self tryToSyncQueuedVerificationStates]; + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIncomingMessageFinder.h b/SignalUtilitiesKit/OWSIncomingMessageFinder.h new file mode 100644 index 000000000..4a1fe8e41 --- /dev/null +++ b/SignalUtilitiesKit/OWSIncomingMessageFinder.h @@ -0,0 +1,30 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class OWSStorage; +@class YapDatabaseReadTransaction; + +@interface OWSIncomingMessageFinder : NSObject + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + ++ (NSString *)databaseExtensionName; ++ (void)asyncRegisterExtensionWithPrimaryStorage:(OWSStorage *)storage; + +/** + * Detects existance of a duplicate incoming message. + */ +- (BOOL)existsMessageWithTimestamp:(uint64_t)timestamp + sourceId:(NSString *)sourceId + sourceDeviceId:(uint32_t)sourceDeviceId + transaction:(YapDatabaseReadTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIncomingMessageFinder.m b/SignalUtilitiesKit/OWSIncomingMessageFinder.m new file mode 100644 index 000000000..f9c60ca9d --- /dev/null +++ b/SignalUtilitiesKit/OWSIncomingMessageFinder.m @@ -0,0 +1,151 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSIncomingMessageFinder.h" +#import "OWSPrimaryStorage.h" +#import "TSIncomingMessage.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSIncomingMessageFinderExtensionName = @"OWSIncomingMessageFinderExtensionName"; + +NSString *const OWSIncomingMessageFinderColumnTimestamp = @"OWSIncomingMessageFinderColumnTimestamp"; +NSString *const OWSIncomingMessageFinderColumnSourceId = @"OWSIncomingMessageFinderColumnSourceId"; +NSString *const OWSIncomingMessageFinderColumnSourceDeviceId = @"OWSIncomingMessageFinderColumnSourceDeviceId"; + +@interface OWSIncomingMessageFinder () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@end + +@implementation OWSIncomingMessageFinder + +@synthesize dbConnection = _dbConnection; + +#pragma mark - init + +- (instancetype)init +{ + OWSAssertDebug([OWSPrimaryStorage sharedManager]); + + return [self initWithPrimaryStorage:[OWSPrimaryStorage sharedManager]]; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _primaryStorage = primaryStorage; + + return self; +} + +#pragma mark - properties + +- (YapDatabaseConnection *)dbConnection +{ + @synchronized(self) { + if (!_dbConnection) { + _dbConnection = [self.primaryStorage newDatabaseConnection]; + } + } + return _dbConnection; +} + +#pragma mark - YAP integration + ++ (YapDatabaseSecondaryIndex *)indexExtension +{ + YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new]; + + [setup addColumn:OWSIncomingMessageFinderColumnTimestamp withType:YapDatabaseSecondaryIndexTypeInteger]; + [setup addColumn:OWSIncomingMessageFinderColumnSourceId withType:YapDatabaseSecondaryIndexTypeText]; + [setup addColumn:OWSIncomingMessageFinderColumnSourceDeviceId withType:YapDatabaseSecondaryIndexTypeInteger]; + + YapDatabaseSecondaryIndexWithObjectBlock block = ^(YapDatabaseReadTransaction *transaction, + NSMutableDictionary *dict, + NSString *collection, + NSString *key, + id object) { + if ([object isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object; + + // On new messages authorId should be set on all incoming messages, but there was a time when authorId was + // only set on incoming group messages. + NSObject *authorIdOrNull = incomingMessage.authorId ? incomingMessage.authorId : [NSNull null]; + [dict setObject:@(incomingMessage.timestamp) forKey:OWSIncomingMessageFinderColumnTimestamp]; + [dict setObject:authorIdOrNull forKey:OWSIncomingMessageFinderColumnSourceId]; + [dict setObject:@(incomingMessage.sourceDeviceId) forKey:OWSIncomingMessageFinderColumnSourceDeviceId]; + } + }; + + YapDatabaseSecondaryIndexHandler *handler = [YapDatabaseSecondaryIndexHandler withObjectBlock:block]; + + return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:nil]; +} + ++ (NSString *)databaseExtensionName +{ + return OWSIncomingMessageFinderExtensionName; +} + ++ (void)asyncRegisterExtensionWithPrimaryStorage:(OWSStorage *)storage +{ + OWSLogInfo(@"registering async."); + [storage asyncRegisterExtension:self.indexExtension withName:OWSIncomingMessageFinderExtensionName]; +} + +#ifdef DEBUG +// We should not normally hit this, as we should have prefer registering async, but it is useful for testing. +- (void)registerExtension +{ + OWSLogError(@"registering SYNC. We should prefer async when possible."); + [self.primaryStorage registerExtension:self.class.indexExtension withName:OWSIncomingMessageFinderExtensionName]; +} +#endif + +#pragma mark - instance methods + +- (BOOL)existsMessageWithTimestamp:(uint64_t)timestamp + sourceId:(NSString *)sourceId + sourceDeviceId:(uint32_t)sourceDeviceId + transaction:(YapDatabaseReadTransaction *)transaction +{ +#ifdef DEBUG + if (![self.primaryStorage registeredExtension:OWSIncomingMessageFinderExtensionName]) { + OWSFailDebug(@"but extension is not registered"); + + // we should be initializing this at startup rather than have an unexpectedly slow lazy setup at random. + [self registerExtension]; + } +#endif + + NSString *queryFormat = [NSString stringWithFormat:@"WHERE %@ = ? AND %@ = ? AND %@ = ?", + OWSIncomingMessageFinderColumnTimestamp, + OWSIncomingMessageFinderColumnSourceId, + OWSIncomingMessageFinderColumnSourceDeviceId]; + // YapDatabaseQuery params must be objects + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:queryFormat, @(timestamp), sourceId, @(sourceDeviceId)]; + + NSUInteger count; + BOOL success = [[transaction ext:OWSIncomingMessageFinderExtensionName] getNumberOfRows:&count matchingQuery:query]; + if (!success) { + OWSFailDebug(@"Could not execute query"); + return NO; + } + + return count > 0; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIncomingSentMessageTranscript.h b/SignalUtilitiesKit/OWSIncomingSentMessageTranscript.h new file mode 100644 index 000000000..02da7d4c0 --- /dev/null +++ b/SignalUtilitiesKit/OWSIncomingSentMessageTranscript.h @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSContact; +@class OWSLinkPreview; +@class SSKProtoAttachmentPointer; +@class SSKProtoDataMessage; +@class SSKProtoSyncMessageSent; +@class TSQuotedMessage; +@class TSThread; +@class YapDatabaseReadWriteTransaction; + +/** + * Represents notification of a message sent on our behalf from another device. + * E.g. When we send a message from Signal-Desktop we want to see it in our conversation on iPhone. + */ +@interface OWSIncomingSentMessageTranscript : NSObject + +- (instancetype)initWithProto:(SSKProtoSyncMessageSent *)sentProto + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@property (nonatomic, readonly) SSKProtoDataMessage *dataMessage; +@property (nonatomic, readonly) NSString *recipientId; +@property (nonatomic, readonly) uint64_t timestamp; +@property (nonatomic, readonly) uint64_t expirationStartedAt; +@property (nonatomic, readonly) uint32_t expirationDuration; +@property (nonatomic, readonly) BOOL isGroupUpdate; +@property (nonatomic, readonly) BOOL isGroupQuit; +@property (nonatomic, readonly) BOOL isExpirationTimerUpdate; +@property (nonatomic, readonly) BOOL isEndSessionMessage; +@property (nonatomic, readonly, nullable) NSData *groupId; +@property (nonatomic, readonly) NSString *body; +@property (nonatomic, readonly) NSArray *attachmentPointerProtos; +@property (nonatomic, readonly, nullable) TSThread *thread; +@property (nonatomic, readonly, nullable) TSQuotedMessage *quotedMessage; +@property (nonatomic, readonly, nullable) OWSContact *contact; +@property (nonatomic, readonly, nullable) OWSLinkPreview *linkPreview; +@property (nonatomic, readonly) BOOL isRecipientUpdate; + +// If either nonUdRecipientIds or udRecipientIds is nil, +// this is either a legacy transcript or it reflects a legacy sync message. +@property (nonatomic, readonly, nullable) NSArray *nonUdRecipientIds; +@property (nonatomic, readonly, nullable) NSArray *udRecipientIds; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIncomingSentMessageTranscript.m b/SignalUtilitiesKit/OWSIncomingSentMessageTranscript.m new file mode 100644 index 000000000..5c73f7347 --- /dev/null +++ b/SignalUtilitiesKit/OWSIncomingSentMessageTranscript.m @@ -0,0 +1,108 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSIncomingSentMessageTranscript.h" +#import "OWSContact.h" +#import "OWSMessageManager.h" +#import "OWSPrimaryStorage.h" +#import "TSContactThread.h" +#import "TSGroupModel.h" +#import "TSGroupThread.h" +#import "TSOutgoingMessage.h" +#import "TSQuotedMessage.h" +#import "TSThread.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSIncomingSentMessageTranscript + +- (instancetype)initWithProto:(SSKProtoSyncMessageSent *)sentProto + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + self = [super init]; + if (!self) { + return self; + } + + _dataMessage = sentProto.message; + _recipientId = sentProto.destination; + _timestamp = sentProto.timestamp; + _expirationStartedAt = sentProto.expirationStartTimestamp; + _expirationDuration = sentProto.message.expireTimer; + _body = _dataMessage.body; + _groupId = _dataMessage.group.id; + _isGroupUpdate = _dataMessage.group != nil && (_dataMessage.group.type == SSKProtoGroupContextTypeUpdate); + _isGroupQuit = _dataMessage.group != nil && (_dataMessage.group.type == SSKProtoGroupContextTypeQuit); + _isExpirationTimerUpdate = (_dataMessage.flags & SSKProtoDataMessageFlagsExpirationTimerUpdate) != 0; + _isEndSessionMessage = (_dataMessage.flags & SSKProtoDataMessageFlagsEndSession) != 0; + _isRecipientUpdate = sentProto.isRecipientUpdate; + + if (self.isRecipientUpdate) { + // Fetch, don't create. We don't want recipient updates to resurrect messages or threads. + if (self.dataMessage.group) { + _thread = [TSGroupThread threadWithGroupId:_dataMessage.group.id transaction:transaction]; + } else { + OWSFailDebug(@"We should never receive a 'recipient update' for messages in contact threads."); + } + // Skip the other processing for recipient updates. + } else { + if (self.dataMessage.group) { + _thread = [TSGroupThread getOrCreateThreadWithGroupId:_dataMessage.group.id groupType:closedGroup transaction:transaction]; + } else { + _thread = [TSContactThread getOrCreateThreadWithContactId:_recipientId transaction:transaction]; + } + + _quotedMessage = + [TSQuotedMessage quotedMessageForDataMessage:_dataMessage thread:_thread transaction:transaction]; + _contact = [OWSContacts contactForDataMessage:_dataMessage transaction:transaction]; + + NSError *linkPreviewError; + _linkPreview = [OWSLinkPreview buildValidatedLinkPreviewWithDataMessage:_dataMessage + body:_body + transaction:transaction + error:&linkPreviewError]; + if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { + OWSLogError(@"linkPreviewError: %@", linkPreviewError); + } + } + + if (sentProto.unidentifiedStatus.count > 0) { + NSMutableArray *nonUdRecipientIds = [NSMutableArray new]; + NSMutableArray *udRecipientIds = [NSMutableArray new]; + for (SSKProtoSyncMessageSentUnidentifiedDeliveryStatus *statusProto in sentProto.unidentifiedStatus) { + if (!statusProto.hasDestination || statusProto.destination.length < 1) { + OWSFailDebug(@"Delivery status proto is missing destination."); + continue; + } + if (!statusProto.hasUnidentified) { + OWSFailDebug(@"Delivery status proto is missing value."); + continue; + } + NSString *recipientId = statusProto.destination; + if (statusProto.unidentified) { + [udRecipientIds addObject:recipientId]; + } else { + [nonUdRecipientIds addObject:recipientId]; + } + } + _nonUdRecipientIds = [nonUdRecipientIds copy]; + _udRecipientIds = [udRecipientIds copy]; + } + + return self; +} + +- (NSArray *)attachmentPointerProtos +{ + if (self.isGroupUpdate && self.dataMessage.group.avatar) { + return @[ self.dataMessage.group.avatar ]; + } else { + return self.dataMessage.attachments; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIncompleteCallsJob.h b/SignalUtilitiesKit/OWSIncompleteCallsJob.h new file mode 100644 index 000000000..2b2e1f311 --- /dev/null +++ b/SignalUtilitiesKit/OWSIncompleteCallsJob.h @@ -0,0 +1,31 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class OWSStorage; + +@interface OWSIncompleteCallsJob : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + +- (void)run; + ++ (NSString *)databaseExtensionName; ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage; + +#ifdef DEBUG +/** + * Only use the sync version for testing, generally we'll want to register extensions async + */ +- (void)blockingRegisterDatabaseExtensions; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSIncompleteCallsJob.m b/SignalUtilitiesKit/OWSIncompleteCallsJob.m new file mode 100644 index 000000000..3d09fa723 --- /dev/null +++ b/SignalUtilitiesKit/OWSIncompleteCallsJob.m @@ -0,0 +1,160 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSIncompleteCallsJob.h" +#import "AppContext.h" +#import "OWSPrimaryStorage.h" +#import "TSCall.h" +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const OWSIncompleteCallsJobCallTypeColumn = @"call_type"; +static NSString *const OWSIncompleteCallsJobCallTypeIndex = @"index_calls_on_call_type"; + +@interface OWSIncompleteCallsJob () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; + +@end + +#pragma mark - + +@implementation OWSIncompleteCallsJob + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _primaryStorage = primaryStorage; + + return self; +} + +- (NSArray *)fetchIncompleteCallIdsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + NSMutableArray *messageIds = [NSMutableArray new]; + + NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ == %d OR %@ == %d", + OWSIncompleteCallsJobCallTypeColumn, + (int)RPRecentCallTypeOutgoingIncomplete, + OWSIncompleteCallsJobCallTypeColumn, + (int)RPRecentCallTypeIncomingIncomplete]; + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:OWSIncompleteCallsJobCallTypeIndex] + enumerateKeysMatchingQuery:query + usingBlock:^void(NSString *collection, NSString *key, BOOL *stop) { + [messageIds addObject:key]; + }]; + + return [messageIds copy]; +} + +- (void)enumerateIncompleteCallsWithBlock:(void (^)(TSCall *call))block + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + // Since we can't directly mutate the enumerated "incomplete" calls, we store only their ids in hopes + // of saving a little memory and then enumerate the (larger) TSCall objects one at a time. + for (NSString *callId in [self fetchIncompleteCallIdsWithTransaction:transaction]) { + TSCall *_Nullable call = [TSCall fetchObjectWithUniqueID:callId transaction:transaction]; + if ([call isKindOfClass:[TSCall class]]) { + block(call); + } else { + OWSLogError(@"unexpected object: %@", call); + } + } +} + +- (void)run +{ + __block uint count = 0; + + OWSAssertDebug(CurrentAppContext().appLaunchTime); + uint64_t cutoffTimestamp = [NSDate ows_millisecondsSince1970ForDate:CurrentAppContext().appLaunchTime]; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self + enumerateIncompleteCallsWithBlock:^(TSCall *call) { + if (call.timestamp <= cutoffTimestamp) { + OWSLogInfo(@"ignoring new call: %@", call.uniqueId); + return; + } + + if (call.callType == RPRecentCallTypeOutgoingIncomplete) { + OWSLogDebug(@"marking call as missed: %@", call.uniqueId); + [call updateCallType:RPRecentCallTypeOutgoingMissed transaction:transaction]; + OWSAssertDebug(call.callType == RPRecentCallTypeOutgoingMissed); + } else if (call.callType == RPRecentCallTypeIncomingIncomplete) { + OWSLogDebug(@"marking call as missed: %@", call.uniqueId); + [call updateCallType:RPRecentCallTypeIncomingMissed transaction:transaction]; + OWSAssertDebug(call.callType == RPRecentCallTypeIncomingMissed); + } else { + OWSFailDebug(@"call has unexpected call type: %@", NSStringFromCallType(call.callType)); + return; + } + count++; + } + transaction:transaction]; + }]; + + OWSLogInfo(@"Marked %u calls as missed", count); +} + +#pragma mark - YapDatabaseExtension + ++ (YapDatabaseSecondaryIndex *)indexDatabaseExtension +{ + YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new]; + [setup addColumn:OWSIncompleteCallsJobCallTypeColumn withType:YapDatabaseSecondaryIndexTypeInteger]; + + YapDatabaseSecondaryIndexHandler *handler = + [YapDatabaseSecondaryIndexHandler withObjectBlock:^(YapDatabaseReadTransaction *transaction, + NSMutableDictionary *dict, + NSString *collection, + NSString *key, + id object) { + if (![object isKindOfClass:[TSCall class]]) { + return; + } + TSCall *call = (TSCall *)object; + + dict[OWSIncompleteCallsJobCallTypeColumn] = @(call.callType); + }]; + + return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:nil]; +} + +#ifdef DEBUG +// Useful for tests, don't use in app startup path because it's slow. +- (void)blockingRegisterDatabaseExtensions +{ + [self.primaryStorage registerExtension:[self.class indexDatabaseExtension] + withName:OWSIncompleteCallsJobCallTypeIndex]; +} +#endif + ++ (NSString *)databaseExtensionName +{ + return OWSIncompleteCallsJobCallTypeIndex; +} + ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage +{ + [storage asyncRegisterExtension:[self indexDatabaseExtension] withName:OWSIncompleteCallsJobCallTypeIndex]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSLinkPreview.swift b/SignalUtilitiesKit/OWSLinkPreview.swift new file mode 100644 index 000000000..80faf4545 --- /dev/null +++ b/SignalUtilitiesKit/OWSLinkPreview.swift @@ -0,0 +1,907 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +@objc +public enum LinkPreviewError: Int, Error { + case invalidInput + case noPreview + case assertionFailure + case couldNotDownload + case featureDisabled + case invalidContent + case invalidMediaContent + case attachmentFailedToSave +} + +// MARK: - OWSLinkPreviewDraft + +public class OWSLinkPreviewContents: NSObject { + @objc + public var title: String? + + @objc + public var imageUrl: String? + + public init(title: String?, imageUrl: String? = nil) { + self.title = title + self.imageUrl = imageUrl + + super.init() + } +} + +// This contains the info for a link preview "draft". +public class OWSLinkPreviewDraft: NSObject { + @objc + public var urlString: String + + @objc + public var title: String? + + @objc + public var jpegImageData: Data? + + public init(urlString: String, title: String?, jpegImageData: Data? = nil) { + self.urlString = urlString + self.title = title + self.jpegImageData = jpegImageData + + super.init() + } + + fileprivate func isValid() -> Bool { + var hasTitle = false + if let titleValue = title { + hasTitle = titleValue.count > 0 + } + let hasImage = jpegImageData != nil + return hasTitle || hasImage + } + + @objc + public func displayDomain() -> String? { + return OWSLinkPreview.displayDomain(forUrl: urlString) + } +} + +// MARK: - OWSLinkPreview + +@objc +public class OWSLinkPreview: MTLModel { + @objc + public static let featureEnabled = true + + @objc + public var urlString: String? + + @objc + public var title: String? + + @objc + public var imageAttachmentId: String? + + // Whether this preview can be rendered as an attachment + @objc + public var isDirectAttachment: Bool = false + + @objc + public init(urlString: String, title: String?, imageAttachmentId: String?, isDirectAttachment: Bool = false) { + self.urlString = urlString + self.title = title + self.imageAttachmentId = imageAttachmentId + self.isDirectAttachment = isDirectAttachment + + super.init() + } + + @objc + public override init() { + super.init() + } + + @objc + public required init!(coder: NSCoder) { + super.init(coder: coder) + } + + @objc + public required init(dictionary dictionaryValue: [String: Any]!) throws { + try super.init(dictionary: dictionaryValue) + } + + @objc + public class func isNoPreviewError(_ error: Error) -> Bool { + guard let error = error as? LinkPreviewError else { + return false + } + return error == .noPreview + } + + @objc + public class func isInvalidContentError(_ error: Error) -> Bool { + guard let error = error as? LinkPreviewError else { return false } + return error == .invalidContent + } + + @objc + public class func buildValidatedLinkPreview(dataMessage: SSKProtoDataMessage, + body: String?, + transaction: YapDatabaseReadWriteTransaction) throws -> OWSLinkPreview { + guard OWSLinkPreview.featureEnabled else { + throw LinkPreviewError.noPreview + } + guard let previewProto = dataMessage.preview.first else { + throw LinkPreviewError.noPreview + } + guard dataMessage.attachments.count < 1 else { + Logger.error("Discarding link preview; message has attachments.") + throw LinkPreviewError.invalidInput + } + let urlString = previewProto.url + + guard URL(string: urlString) != nil else { + Logger.error("Could not parse preview URL.") + throw LinkPreviewError.invalidInput + } + + guard let body = body else { + Logger.error("Preview for message without body.") + throw LinkPreviewError.invalidInput + } + let previewUrls = allPreviewUrls(forMessageBodyText: body) + guard previewUrls.contains(urlString) else { + Logger.error("URL not present in body.") + throw LinkPreviewError.invalidInput + } + + guard isValidLinkUrl(urlString) else { + Logger.verbose("Invalid link URL \(urlString).") + Logger.error("Invalid link URL.") + throw LinkPreviewError.invalidInput + } + + var title: String? + if let rawTitle = previewProto.title { + let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle) + if normalizedTitle.count > 0 { + title = normalizedTitle + } + } + + var imageAttachmentId: String? + if let imageProto = previewProto.image { + if let imageAttachmentPointer = TSAttachmentPointer(fromProto: imageProto, albumMessage: nil) { + imageAttachmentPointer.save(with: transaction) + imageAttachmentId = imageAttachmentPointer.uniqueId + } else { + Logger.error("Could not parse image proto.") + throw LinkPreviewError.invalidInput + } + } + + let linkPreview = OWSLinkPreview(urlString: urlString, title: title, imageAttachmentId: imageAttachmentId) + + guard linkPreview.isValid() else { + Logger.error("Preview has neither title nor image.") + throw LinkPreviewError.invalidInput + } + + return linkPreview + } + + @objc + public class func buildValidatedLinkPreview(fromInfo info: OWSLinkPreviewDraft, + transaction: YapDatabaseReadWriteTransaction) throws -> OWSLinkPreview { + guard OWSLinkPreview.featureEnabled else { + throw LinkPreviewError.noPreview + } + guard SSKPreferences.areLinkPreviewsEnabled else { + throw LinkPreviewError.noPreview + } + let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(jpegImageData: info.jpegImageData, + transaction: transaction) + + let linkPreview = OWSLinkPreview(urlString: info.urlString, title: info.title, imageAttachmentId: imageAttachmentId) + + guard linkPreview.isValid() else { + owsFailDebug("Preview has neither title nor image.") + throw LinkPreviewError.invalidInput + } + + return linkPreview + } + + private class func saveAttachmentIfPossible(jpegImageData: Data?, + transaction: YapDatabaseReadWriteTransaction) -> String? { + return saveAttachmentIfPossible(imageData: jpegImageData, mimeType: OWSMimeTypeImageJpeg, transaction: transaction); + } + + private class func saveAttachmentIfPossible(imageData: Data?, mimeType: String, transaction: YapDatabaseReadWriteTransaction) -> String? { + guard let imageData = imageData else { return nil } + + let fileSize = imageData.count + guard fileSize > 0 else { + owsFailDebug("Invalid file size for image data.") + return nil + } + + guard let fileExtension = fileExtension(forMimeType: mimeType) else { return nil } + let filePath = OWSFileSystem.temporaryFilePath(withFileExtension: fileExtension) + do { + try imageData.write(to: NSURL.fileURL(withPath: filePath), options: .atomicWrite) + } catch let error as NSError { + owsFailDebug("file write failed: \(filePath), \(error)") + return nil + } + + guard let dataSource = DataSourcePath.dataSource(withFilePath: filePath, shouldDeleteOnDeallocation: true) else { + owsFailDebug("Could not create data source for path: \(filePath)") + return nil + } + let attachment = TSAttachmentStream(contentType: mimeType, byteCount: UInt32(fileSize), sourceFilename: nil, caption: nil, albumMessageId: nil) + guard attachment.write(dataSource) else { + owsFailDebug("Could not write data source for path: \(filePath)") + return nil + } + attachment.save(with: transaction) + + return attachment.uniqueId + } + + private func isValid() -> Bool { + var hasTitle = false + if let titleValue = title { + hasTitle = titleValue.count > 0 + } + let hasImage = imageAttachmentId != nil + return hasTitle || hasImage + } + + @objc + public func removeAttachment(transaction: YapDatabaseReadWriteTransaction) { + guard let imageAttachmentId = imageAttachmentId else { + owsFailDebug("No attachment id.") + return + } + guard let attachment = TSAttachment.fetch(uniqueId: imageAttachmentId, transaction: transaction) else { + owsFailDebug("Could not load attachment.") + return + } + attachment.remove(with: transaction) + } + + private class func normalizeTitle(title: String) -> String { + var result = title + // Truncate title after 2 lines of text. + let maxLineCount = 2 + var components = result.components(separatedBy: .newlines) + if components.count > maxLineCount { + components = Array(components[0.. maxCharacterCount { + let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount) + result = String(result[.. String? { + return OWSLinkPreview.displayDomain(forUrl: urlString) + } + + @objc + public class func displayDomain(forUrl urlString: String?) -> String? { + guard let urlString = urlString else { + owsFailDebug("Missing url.") + return nil + } + guard let url = URL(string: urlString) else { + owsFailDebug("Invalid url.") + return nil + } + guard let result = whitelistedDomain(forUrl: url, + domainWhitelist: OWSLinkPreview.linkDomainWhitelist, + allowSubdomains: false) else { + Logger.error("Missing domain.") + return nil + } + return result + } + + @objc + public class func isValidLinkUrl(_ urlString: String) -> Bool { + guard let url = URL(string: urlString) else { + return false + } + return whitelistedDomain(forUrl: url, + domainWhitelist: OWSLinkPreview.linkDomainWhitelist, + allowSubdomains: false) != nil + } + + @objc + public class func isValidMediaUrl(_ urlString: String) -> Bool { + guard let url = URL(string: urlString) else { + return false + } + return whitelistedDomain(forUrl: url, + domainWhitelist: OWSLinkPreview.mediaDomainWhitelist, + allowSubdomains: true) != nil + } + + private class func whitelistedDomain(forUrl url: URL, domainWhitelist: [String], allowSubdomains: Bool) -> String? { + guard let urlProtocol = url.scheme?.lowercased() else { + return nil + } + guard protocolWhitelist.contains(urlProtocol) else { + return nil + } + guard let domain = url.host?.lowercased() else { + return nil + } + guard url.path.count > 1 else { + // URL must have non-empty path. + return nil + } + + for whitelistedDomain in domainWhitelist { + if domain == whitelistedDomain.lowercased() { + return whitelistedDomain + } + if allowSubdomains, + domain.hasSuffix("." + whitelistedDomain.lowercased()) { + return whitelistedDomain + } + } + return nil + } + + // MARK: - Serial Queue + + private static let serialQueue = DispatchQueue(label: "org.signal.linkPreview") + + private class func assertIsOnSerialQueue() { + if _isDebugAssertConfiguration(), #available(iOS 10.0, *) { + assertOnQueue(serialQueue) + } + } + + // MARK: - Text Parsing + + // This cache should only be accessed on main thread. + private static var previewUrlCache: NSCache = NSCache() + + @objc + public class func previewUrl(forRawBodyText body: String?, selectedRange: NSRange) -> String? { + return previewUrl(forMessageBodyText: body, selectedRange: selectedRange) + } + + @objc + public class func previewURL(forRawBodyText body: String?) -> String? { + return previewUrl(forMessageBodyText: body, selectedRange: nil) + } + + public class func previewUrl(forMessageBodyText body: String?, selectedRange: NSRange?) -> String? { + AssertIsOnMainThread() + + // Exit early if link previews are not enabled in order to avoid + // tainting the cache. + guard OWSLinkPreview.featureEnabled else { + return nil + } + + guard SSKPreferences.areLinkPreviewsEnabled else { + return nil + } + + guard let body = body else { + return nil + } + + if let cachedUrl = previewUrlCache.object(forKey: body as NSString) as String? { + Logger.verbose("URL parsing cache hit.") + guard cachedUrl.count > 0 else { + return nil + } + return cachedUrl + } + let previewUrlMatches = allPreviewUrlMatches(forMessageBodyText: body) + guard let urlMatch = previewUrlMatches.first else { + // Use empty string to indicate "no preview URL" in the cache. + previewUrlCache.setObject("", forKey: body as NSString) + return nil + } + + if let selectedRange = selectedRange { + Logger.verbose("match: urlString: \(urlMatch.urlString) range: \(urlMatch.matchRange) selectedRange: \(selectedRange)") + let cursorAtEndOfMatch = urlMatch.matchRange.location + urlMatch.matchRange.length == selectedRange.location + if selectedRange.location != body.count, + (urlMatch.matchRange.intersection(selectedRange) != nil || cursorAtEndOfMatch) { + Logger.debug("ignoring URL, since the user is currently editing it.") + // we don't want to cache the result here, as we want to fetch the link preview + // if the user moves the cursor. + return nil + } + Logger.debug("considering URL, since the user is not currently editing it.") + } + + previewUrlCache.setObject(urlMatch.urlString as NSString, forKey: body as NSString) + return urlMatch.urlString + } + + struct URLMatchResult { + let urlString: String + let matchRange: NSRange + } + + class func allPreviewUrls(forMessageBodyText body: String) -> [String] { + return allPreviewUrlMatches(forMessageBodyText: body).map { $0.urlString } + } + + class func allPreviewUrlMatches(forMessageBodyText body: String) -> [URLMatchResult] { + guard OWSLinkPreview.featureEnabled else { + return [] + } + guard SSKPreferences.areLinkPreviewsEnabled else { + return [] + } + + let detector: NSDataDetector + do { + detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + } catch { + owsFailDebug("Could not create NSDataDetector: \(error).") + return [] + } + + var urlMatches: [URLMatchResult] = [] + let matches = detector.matches(in: body, options: [], range: NSRange(location: 0, length: body.count)) + for match in matches { + guard let matchURL = match.url else { + owsFailDebug("Match missing url") + continue + } + let urlString = matchURL.absoluteString + if isValidLinkUrl(urlString) { + let matchResult = URLMatchResult(urlString: urlString, matchRange: match.range) + urlMatches.append(matchResult) + } + } + return urlMatches + } + + // MARK: - Preview Construction + + // This cache should only be accessed on serialQueue. + // + // We should only maintain a "cache" of the last known draft. + private static var linkPreviewDraftCache: OWSLinkPreviewDraft? + + private class func cachedLinkPreview(forPreviewUrl previewUrl: String) -> OWSLinkPreviewDraft? { + return serialQueue.sync { + guard let linkPreviewDraft = linkPreviewDraftCache, + linkPreviewDraft.urlString == previewUrl else { + Logger.verbose("----- Cache miss.") + return nil + } + Logger.verbose("----- Cache hit.") + return linkPreviewDraft + } + } + + private class func setCachedLinkPreview(_ linkPreviewDraft: OWSLinkPreviewDraft, + forPreviewUrl previewUrl: String) { + assert(previewUrl == linkPreviewDraft.urlString) + + // Exit early if link previews are not enabled in order to avoid + // tainting the cache. + guard OWSLinkPreview.featureEnabled else { + return + } + guard SSKPreferences.areLinkPreviewsEnabled else { + return + } + + serialQueue.sync { + linkPreviewDraftCache = linkPreviewDraft + } + } + + @objc + public class func tryToBuildPreviewInfoObjc(previewUrl: String?) -> AnyPromise { + return AnyPromise(tryToBuildPreviewInfo(previewUrl: previewUrl)) + } + + public class func tryToBuildPreviewInfo(previewUrl: String?) -> Promise { + guard OWSLinkPreview.featureEnabled else { + return Promise(error: LinkPreviewError.featureDisabled) + } + guard SSKPreferences.areLinkPreviewsEnabled else { + return Promise(error: LinkPreviewError.featureDisabled) + } + guard let previewUrl = previewUrl else { + return Promise(error: LinkPreviewError.invalidInput) + } + if let cachedInfo = cachedLinkPreview(forPreviewUrl: previewUrl) { + Logger.verbose("Link preview info cache hit.") + return Promise.value(cachedInfo) + } + return downloadLink(url: previewUrl) + .then(on: DispatchQueue.global()) { (data) -> Promise in + return parseLinkDataAndBuildDraft(linkData: data, linkUrlString: previewUrl) + }.then(on: DispatchQueue.global()) { (linkPreviewDraft) -> Promise in + guard linkPreviewDraft.isValid() else { + throw LinkPreviewError.noPreview + } + setCachedLinkPreview(linkPreviewDraft, forPreviewUrl: previewUrl) + + return Promise.value(linkPreviewDraft) + } + } + + class func downloadLink(url urlString: String, + remainingRetries: UInt = 3) -> Promise { + + Logger.verbose("url: \(urlString)") + + // let sessionConfiguration = ContentProxy.sessionConfiguration() // Loki: Signal's proxy appears to have been banned by YouTube + let sessionConfiguration = URLSessionConfiguration.ephemeral + + // Don't use any caching to protect privacy of these requests. + sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData + sessionConfiguration.urlCache = nil + + let sessionManager = AFHTTPSessionManager(baseURL: nil, + sessionConfiguration: sessionConfiguration) + sessionManager.requestSerializer = AFHTTPRequestSerializer() + sessionManager.responseSerializer = AFHTTPResponseSerializer() + + guard ContentProxy.configureSessionManager(sessionManager: sessionManager, forUrl: urlString) else { + owsFailDebug("Could not configure url: \(urlString).") + return Promise(error: LinkPreviewError.assertionFailure) + } + + let (promise, resolver) = Promise.pending() + sessionManager.get(urlString, + parameters: [String: AnyObject](), + progress: nil, + success: { task, value in + + guard let response = task.response as? HTTPURLResponse else { + Logger.warn("Invalid response: \(type(of: task.response)).") + resolver.reject(LinkPreviewError.assertionFailure) + return + } + if let contentType = response.allHeaderFields["Content-Type"] as? String { + guard contentType.lowercased().hasPrefix("text/") else { + Logger.warn("Invalid content type: \(contentType).") + resolver.reject(LinkPreviewError.invalidContent) + return + } + } + guard let data = value as? Data else { + Logger.warn("Result is not data: \(type(of: value)).") + resolver.reject(LinkPreviewError.assertionFailure) + return + } + guard data.count > 0 else { + Logger.warn("Empty data: \(type(of: value)).") + resolver.reject(LinkPreviewError.invalidContent) + return + } + resolver.fulfill(data) + }, + failure: { _, error in + Logger.verbose("Error: \(error)") + + guard isRetryable(error: error) else { + Logger.warn("Error is not retryable.") + resolver.reject(LinkPreviewError.couldNotDownload) + return + } + + guard remainingRetries > 0 else { + Logger.warn("No more retries.") + resolver.reject(LinkPreviewError.couldNotDownload) + return + } + OWSLinkPreview.downloadLink(url: urlString, remainingRetries: remainingRetries - 1) + .done(on: DispatchQueue.global()) { (data) in + resolver.fulfill(data) + }.catch(on: DispatchQueue.global()) { (error) in + resolver.reject(error) + }.retainUntilComplete() + }) + return promise + } + + private class func downloadImage(url urlString: String, imageMimeType: String) -> Promise { + + Logger.verbose("url: \(urlString)") + + guard let url = URL(string: urlString) else { + Logger.error("Could not parse URL.") + return Promise(error: LinkPreviewError.invalidInput) + } + + guard let assetDescription = ProxiedContentAssetDescription(url: url as NSURL) else { + Logger.error("Could not create asset description.") + return Promise(error: LinkPreviewError.invalidInput) + } + let (promise, resolver) = Promise.pending() + DispatchQueue.main.async { + _ = ProxiedContentDownloader.defaultDownloader.requestAsset(assetDescription: assetDescription, + priority: .high, + success: { (_, asset) in + resolver.fulfill(asset) + }, failure: { (_) in + Logger.warn("Error downloading asset") + resolver.reject(LinkPreviewError.couldNotDownload) + }) + } + return promise.then(on: DispatchQueue.global()) { (asset: ProxiedContentAsset) -> Promise in + do { + let imageSize = NSData.imageSize(forFilePath: asset.filePath, mimeType: imageMimeType) + guard imageSize.width > 0, imageSize.height > 0 else { + Logger.error("Link preview is invalid or has invalid size.") + return Promise(error: LinkPreviewError.invalidContent) + } + let data = try Data(contentsOf: URL(fileURLWithPath: asset.filePath)) + + guard let srcImage = UIImage(data: data) else { + Logger.error("Could not parse image.") + return Promise(error: LinkPreviewError.invalidContent) + } + + // Loki: If it's a GIF then ensure its validity and don't download it as a JPG + if (imageMimeType == OWSMimeTypeImageGif && NSData(data: data).ows_isValidImage(withMimeType: OWSMimeTypeImageGif)) { return Promise.value(data) } + + let maxImageSize: CGFloat = 1024 + let shouldResize = imageSize.width > maxImageSize || imageSize.height > maxImageSize + guard shouldResize else { + guard let dstData = srcImage.jpegData(compressionQuality: 0.8) else { + Logger.error("Could not write resized image.") + return Promise(error: LinkPreviewError.invalidContent) + } + return Promise.value(dstData) + } + + guard let dstImage = srcImage.resized(withMaxDimensionPoints: maxImageSize) else { + Logger.error("Could not resize image.") + return Promise(error: LinkPreviewError.invalidContent) + } + guard let dstData = dstImage.jpegData(compressionQuality: 0.8) else { + Logger.error("Could not write resized image.") + return Promise(error: LinkPreviewError.invalidContent) + } + return Promise.value(dstData) + } catch { + owsFailDebug("Could not load asset data: \(type(of: asset.filePath)).") + return Promise(error: LinkPreviewError.assertionFailure) + } + } + } + + private class func isRetryable(error: Error) -> Bool { + let nsError = error as NSError + if nsError.domain == kCFErrorDomainCFNetwork as String { + // Network failures are retried. + return true + } + return false + } + + class func parseLinkDataAndBuildDraft(linkData: Data, + linkUrlString: String) -> Promise { + do { + let contents = try parse(linkData: linkData) + + let title = contents.title + guard let imageUrl = contents.imageUrl else { + return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) + } + + guard isValidMediaUrl(imageUrl) else { + Logger.error("Invalid image URL.") + return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) + } + guard let imageFileExtension = fileExtension(forImageUrl: imageUrl) else { + Logger.error("Image URL has unknown or invalid file extension: \(imageUrl).") + return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) + } + guard let imageMimeType = mimetype(forImageFileExtension: imageFileExtension) else { + Logger.error("Image URL has unknown or invalid content type: \(imageUrl).") + return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) + } + + return downloadImage(url: imageUrl, imageMimeType: imageMimeType) + .map(on: DispatchQueue.global()) { (imageData: Data) -> OWSLinkPreviewDraft in + // We always recompress images to Jpeg. + let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, jpegImageData: imageData) + return linkPreviewDraft + } + .recover(on: DispatchQueue.global()) { (_) -> Promise in + return Promise.value(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) + } + } catch { + owsFailDebug("Could not parse link data: \(error).") + return Promise(error: error) + } + } + + // Example: + // + // + // + class func parse(linkData: Data) throws -> OWSLinkPreviewContents { + guard let linkText = String(bytes: linkData, encoding: .utf8) else { + owsFailDebug("Could not parse link text.") + throw LinkPreviewError.invalidInput + } + + var title: String? + if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "]*content\\s*=\\s*\"(.*?)\"\\s*[^>]*/?>", + text: linkText, + options: .dotMatchesLineSeparators) { + if let decodedTitle = decodeHTMLEntities(inString: rawTitle) { + let normalizedTitle = OWSLinkPreview.normalizeTitle(title: decodedTitle) + if normalizedTitle.count > 0 { + title = normalizedTitle + } + } + } + + Logger.verbose("title: \(String(describing: title))") + + guard let rawImageUrlString = NSRegularExpression.parseFirstMatch(pattern: "]*content\\s*=\\s*\"(.*?)\"[^>]*/?>", text: linkText) else { + return OWSLinkPreviewContents(title: title) + } + guard let imageUrlString = decodeHTMLEntities(inString: rawImageUrlString)?.ows_stripped() else { + return OWSLinkPreviewContents(title: title) + } + + return OWSLinkPreviewContents(title: title, imageUrl: imageUrlString) + } + + class func fileExtension(forImageUrl urlString: String) -> String? { + guard let imageUrl = URL(string: urlString) else { + Logger.error("Could not parse image URL.") + return nil + } + let imageFilename = imageUrl.lastPathComponent + let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased() + guard imageFileExtension.count > 0 else { + return nil + } + return imageFileExtension + } + + class func fileExtension(forMimeType mimeType: String) -> String? { + switch mimeType { + case OWSMimeTypeImageGif: return "gif" + case OWSMimeTypeImagePng: return "png" + case OWSMimeTypeImageJpeg: return "jpg" + default: return nil + } + } + + class func mimetype(forImageFileExtension imageFileExtension: String) -> String? { + guard imageFileExtension.count > 0 else { + return nil + } + guard let imageMimeType = MIMETypeUtil.mimeType(forFileExtension: imageFileExtension) else { + Logger.error("Image URL has unknown content type: \(imageFileExtension).") + return nil + } + let kValidMimeTypes = [ + OWSMimeTypeImagePng, + OWSMimeTypeImageJpeg, + OWSMimeTypeImageGif, + ] + guard kValidMimeTypes.contains(imageMimeType) else { + Logger.error("Image URL has invalid content type: \(imageMimeType).") + return nil + } + return imageMimeType + } + + private class func decodeHTMLEntities(inString value: String) -> String? { + guard let data = value.data(using: .utf8) else { + return nil + } + + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ + NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html, + NSAttributedString.DocumentReadingOptionKey.characterEncoding: String.Encoding.utf8.rawValue + ] + + guard let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else { + return nil + } + + return attributedString.string + } +} diff --git a/SignalUtilitiesKit/OWSLinkedDeviceReadReceipt.h b/SignalUtilitiesKit/OWSLinkedDeviceReadReceipt.h new file mode 100644 index 000000000..eb5d5f79a --- /dev/null +++ b/SignalUtilitiesKit/OWSLinkedDeviceReadReceipt.h @@ -0,0 +1,26 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSLinkedDeviceReadReceipt : TSYapDatabaseObject + +@property (nonatomic, readonly) NSString *senderId; +@property (nonatomic, readonly) uint64_t messageIdTimestamp; +@property (nonatomic, readonly) uint64_t readTimestamp; + +- (instancetype)initWithSenderId:(NSString *)senderId + messageIdTimestamp:(uint64_t)messageIdtimestamp + readTimestamp:(uint64_t)readTimestamp; + ++ (nullable OWSLinkedDeviceReadReceipt *)findLinkedDeviceReadReceiptWithSenderId:(NSString *)senderId + messageIdTimestamp:(uint64_t)messageIdTimestamp + transaction: + (YapDatabaseReadTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSLinkedDeviceReadReceipt.m b/SignalUtilitiesKit/OWSLinkedDeviceReadReceipt.m new file mode 100644 index 000000000..e113afa68 --- /dev/null +++ b/SignalUtilitiesKit/OWSLinkedDeviceReadReceipt.m @@ -0,0 +1,77 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSLinkedDeviceReadReceipt.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSLinkedDeviceReadReceipt + +- (instancetype)initWithSenderId:(NSString *)senderId + messageIdTimestamp:(uint64_t)messageIdTimestamp + readTimestamp:(uint64_t)readTimestamp +{ + OWSAssertDebug(senderId.length > 0 && messageIdTimestamp > 0); + + NSString *receiptId = + [OWSLinkedDeviceReadReceipt uniqueIdForSenderId:senderId messageIdTimestamp:messageIdTimestamp]; + self = [super initWithUniqueId:receiptId]; + if (!self) { + return self; + } + + _senderId = senderId; + _messageIdTimestamp = messageIdTimestamp; + _readTimestamp = readTimestamp; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + // renamed timestamp -> messageIdTimestamp + if (!_messageIdTimestamp) { + NSNumber *_Nullable legacyTimestamp = (NSNumber *)[coder decodeObjectForKey:@"timestamp"]; + OWSAssertDebug(legacyTimestamp.unsignedLongLongValue > 0); + _messageIdTimestamp = legacyTimestamp.unsignedLongLongValue; + } + + // For legacy objects, before we were tracking read time, use the original messages "sent" timestamp + // as the local read time. This will always be at least a little bit earlier than the message was + // actually read, which isn't ideal, but safer than persisting a disappearing message too long, especially + // since we know they read it on their linked desktop. + if (_readTimestamp == 0) { + _readTimestamp = _messageIdTimestamp; + } + + return self; +} + ++ (NSString *)uniqueIdForSenderId:(NSString *)senderId messageIdTimestamp:(uint64_t)messageIdTimestamp +{ + OWSAssertDebug(senderId.length > 0 && messageIdTimestamp > 0); + + return [NSString stringWithFormat:@"%@-%llu", senderId, messageIdTimestamp]; +} + ++ (nullable OWSLinkedDeviceReadReceipt *)findLinkedDeviceReadReceiptWithSenderId:(NSString *)senderId + messageIdTimestamp:(uint64_t)messageIdTimestamp + transaction: + (YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + NSString *receiptId = + [OWSLinkedDeviceReadReceipt uniqueIdForSenderId:senderId messageIdTimestamp:messageIdTimestamp]; + return [OWSLinkedDeviceReadReceipt fetchObjectWithUniqueID:receiptId transaction:transaction]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMath.h b/SignalUtilitiesKit/OWSMath.h new file mode 100644 index 000000000..c626dd767 --- /dev/null +++ b/SignalUtilitiesKit/OWSMath.h @@ -0,0 +1,114 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +CG_INLINE CGFloat CGFloatClamp(CGFloat value, CGFloat minValue, CGFloat maxValue) +{ + return MAX(minValue, MIN(maxValue, value)); +} + +CG_INLINE CGFloat CGFloatClamp01(CGFloat value) +{ + return CGFloatClamp(value, 0.f, 1.f); +} + +CG_INLINE CGFloat CGFloatLerp(CGFloat left, CGFloat right, CGFloat alpha) +{ + return (left * (1.f - alpha)) + (right * alpha); +} + +CG_INLINE CGFloat CGFloatInverseLerp(CGFloat value, CGFloat minValue, CGFloat maxValue) +{ + return (value - minValue) / (maxValue - minValue); +} + +// Ceil to an even number +CG_INLINE CGFloat CeilEven(CGFloat value) +{ + return 2.f * (CGFloat)ceil(value * 0.5f); +} + +CG_INLINE CGSize CGSizeCeil(CGSize size) +{ + return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height)); +} + +CG_INLINE CGSize CGSizeFloor(CGSize size) +{ + return CGSizeMake((CGFloat)floor(size.width), (CGFloat)floor(size.height)); +} + +CG_INLINE CGSize CGSizeRound(CGSize size) +{ + return CGSizeMake((CGFloat)round(size.width), (CGFloat)round(size.height)); +} + +CG_INLINE CGSize CGSizeMax(CGSize size1, CGSize size2) +{ + return CGSizeMake(MAX(size1.width, size2.width), MAX(size1.height, size2.height)); +} + +CG_INLINE CGPoint CGPointAdd(CGPoint left, CGPoint right) +{ + return CGPointMake(left.x + right.x, left.y + right.y); +} + +CG_INLINE CGPoint CGPointSubtract(CGPoint left, CGPoint right) +{ + return CGPointMake(left.x - right.x, left.y - right.y); +} + +CG_INLINE CGPoint CGPointScale(CGPoint point, CGFloat factor) +{ + return CGPointMake(point.x * factor, point.y * factor); +} + +CG_INLINE CGFloat CGPointDistance(CGPoint left, CGPoint right) +{ + CGPoint delta = CGPointSubtract(left, right); + return sqrt(delta.x * delta.x + delta.y * delta.y); +} + +CG_INLINE CGPoint CGPointMin(CGPoint left, CGPoint right) +{ + return CGPointMake(MIN(left.x, right.x), MIN(left.y, right.y)); +} + +CG_INLINE CGPoint CGPointMax(CGPoint left, CGPoint right) +{ + return CGPointMake(MAX(left.x, right.x), MAX(left.y, right.y)); +} + +CG_INLINE CGPoint CGPointClamp01(CGPoint point) +{ + return CGPointMake(CGFloatClamp01(point.x), CGFloatClamp01(point.y)); +} + +CG_INLINE CGPoint CGPointInvert(CGPoint point) +{ + return CGPointMake(-point.x, -point.y); +} + +CG_INLINE CGSize CGSizeScale(CGSize size, CGFloat factor) +{ + return CGSizeMake(size.width * factor, size.height * factor); +} + +CG_INLINE CGSize CGSizeAdd(CGSize left, CGSize right) +{ + return CGSizeMake(left.width + right.width, left.height + right.height); +} + +CG_INLINE CGRect CGRectScale(CGRect rect, CGFloat factor) +{ + CGRect result; + result.origin = CGPointScale(rect.origin, factor); + result.size = CGSizeScale(rect.size, factor); + return result; +} + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMediaGalleryFinder.h b/SignalUtilitiesKit/OWSMediaGalleryFinder.h new file mode 100644 index 000000000..bdcc32e17 --- /dev/null +++ b/SignalUtilitiesKit/OWSMediaGalleryFinder.h @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSStorage; +@class TSAttachment; +@class TSThread; +@class YapDatabaseAutoViewTransaction; +@class YapDatabaseConnection; +@class YapDatabaseReadTransaction; +@class YapDatabaseViewRowChange; + +@interface OWSMediaGalleryFinder : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithThread:(TSThread *)thread NS_DESIGNATED_INITIALIZER; + +// How many media items a thread has +- (NSUInteger)mediaCountWithTransaction:(YapDatabaseReadTransaction *)transaction NS_SWIFT_NAME(mediaCount(transaction:)); + +// The ordinal position of an attachment within a thread's media gallery +- (nullable NSNumber *)mediaIndexForAttachment:(TSAttachment *)attachment + transaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(mediaIndex(attachment:transaction:)); + +- (nullable TSAttachment *)oldestMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(oldestMediaAttachment(transaction:)); +- (nullable TSAttachment *)mostRecentMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(mostRecentMediaAttachment(transaction:)); + +- (void)enumerateMediaAttachmentsWithRange:(NSRange)range + transaction:(YapDatabaseReadTransaction *)transaction + block:(void (^)(TSAttachment *))attachmentBlock + NS_SWIFT_NAME(enumerateMediaAttachments(range:transaction:block:)); + +- (BOOL)hasMediaChangesInNotifications:(NSArray *)notifications + dbConnection:(YapDatabaseConnection *)dbConnection; + +#pragma mark - Extension registration + +@property (nonatomic, readonly) NSString *mediaGroup; +- (YapDatabaseAutoViewTransaction *)galleryExtensionWithTransaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(galleryExtension(transaction:)); ++ (NSString *)databaseExtensionName; ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMediaGalleryFinder.m b/SignalUtilitiesKit/OWSMediaGalleryFinder.m new file mode 100644 index 000000000..04b39fb94 --- /dev/null +++ b/SignalUtilitiesKit/OWSMediaGalleryFinder.m @@ -0,0 +1,219 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSMediaGalleryFinder.h" +#import "OWSStorage.h" +#import "TSAttachmentStream.h" +#import "TSMessage.h" +#import "TSThread.h" +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const OWSMediaGalleryFinderExtensionName = @"OWSMediaGalleryFinderExtensionName"; + +@interface OWSMediaGalleryFinder () + +@property (nonatomic, readonly) TSThread *thread; + +@end + +@implementation OWSMediaGalleryFinder + +- (instancetype)initWithThread:(TSThread *)thread +{ + self = [super init]; + if (!self) { + return self; + } + + _thread = thread; + + return self; +} + +#pragma mark - Public Finder Methods + +- (NSUInteger)mediaCountWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [[self galleryExtensionWithTransaction:transaction] numberOfItemsInGroup:self.mediaGroup]; +} + +- (nullable NSNumber *)mediaIndexForAttachment:(TSAttachment *)attachment + transaction:(YapDatabaseReadTransaction *)transaction +{ + NSString *groupId; + NSUInteger index; + + BOOL wasFound = [[self galleryExtensionWithTransaction:transaction] getGroup:&groupId + index:&index + forKey:attachment.uniqueId + inCollection:[TSAttachment collection]]; + + if (!wasFound) { + return nil; + } + + OWSAssertDebug([self.mediaGroup isEqual:groupId]); + + return @(index); +} + +- (nullable TSAttachment *)oldestMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [[self galleryExtensionWithTransaction:transaction] firstObjectInGroup:self.mediaGroup]; +} + +- (nullable TSAttachment *)mostRecentMediaAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [[self galleryExtensionWithTransaction:transaction] lastObjectInGroup:self.mediaGroup]; +} + +- (void)enumerateMediaAttachmentsWithRange:(NSRange)range + transaction:(YapDatabaseReadTransaction *)transaction + block:(void (^)(TSAttachment *))attachmentBlock +{ + + [[self galleryExtensionWithTransaction:transaction] + enumerateKeysAndObjectsInGroup:self.mediaGroup + withOptions:0 + range:range + usingBlock:^(NSString *_Nonnull collection, + NSString *_Nonnull key, + id _Nonnull object, + NSUInteger index, + BOOL *_Nonnull stop) { + OWSAssertDebug([object isKindOfClass:[TSAttachment class]]); + attachmentBlock((TSAttachment *)object); + }]; +} + +- (BOOL)hasMediaChangesInNotifications:(NSArray *)notifications + dbConnection:(YapDatabaseConnection *)dbConnection +{ + YapDatabaseAutoViewConnection *extConnection = [dbConnection ext:OWSMediaGalleryFinderExtensionName]; + OWSAssert(extConnection); + + return [extConnection hasChangesForGroup:self.mediaGroup inNotifications:notifications]; +} + +#pragma mark - Util + +- (YapDatabaseAutoViewTransaction *)galleryExtensionWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + YapDatabaseAutoViewTransaction *extension = [transaction extension:OWSMediaGalleryFinderExtensionName]; + OWSAssertDebug(extension); + + return extension; +} + ++ (NSString *)mediaGroupWithThreadId:(NSString *)threadId +{ + return [NSString stringWithFormat:@"%@-media", threadId]; +} + +- (NSString *)mediaGroup +{ + return [[self class] mediaGroupWithThreadId:self.thread.uniqueId]; +} + +#pragma mark - Extension registration + ++ (NSString *)databaseExtensionName +{ + return OWSMediaGalleryFinderExtensionName; +} + ++ (void)asyncRegisterDatabaseExtensionsWithPrimaryStorage:(OWSStorage *)storage +{ + [storage asyncRegisterExtension:[self mediaGalleryDatabaseExtension] + withName:OWSMediaGalleryFinderExtensionName]; +} + ++ (YapDatabaseAutoView *)mediaGalleryDatabaseExtension +{ + YapDatabaseViewSorting *sorting = + [YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *_Nonnull transaction, + NSString *_Nonnull group, + NSString *_Nonnull collection1, + NSString *_Nonnull key1, + id _Nonnull object1, + NSString *_Nonnull collection2, + NSString *_Nonnull key2, + id _Nonnull object2) { + if (![object1 isKindOfClass:[TSAttachment class]]) { + OWSFailDebug(@"Unexpected object while sorting: %@", [object1 class]); + return NSOrderedSame; + } + TSAttachment *attachment1 = (TSAttachment *)object1; + + if (![object2 isKindOfClass:[TSAttachment class]]) { + OWSFailDebug(@"Unexpected object while sorting: %@", [object2 class]); + return NSOrderedSame; + } + TSAttachment *attachment2 = (TSAttachment *)object2; + + TSMessage *_Nullable message1 = [attachment1 fetchAlbumMessageWithTransaction:transaction]; + TSMessage *_Nullable message2 = [attachment2 fetchAlbumMessageWithTransaction:transaction]; + if (message1 == nil || message2 == nil) { + OWSFailDebug(@"couldn't find albumMessage"); + return NSOrderedSame; + } + + if ([message1.uniqueId isEqualToString:message2.uniqueId]) { + NSUInteger index1 = [message1.attachmentIds indexOfObject:attachment1.uniqueId]; + NSUInteger index2 = [message1.attachmentIds indexOfObject:attachment2.uniqueId]; + + if (index1 == NSNotFound || index2 == NSNotFound) { + OWSFailDebug(@"couldn't find attachmentId in it's albumMessage"); + return NSOrderedSame; + } + return [@(index1) compare:@(index2)]; + } else { + return [message1 compareForSorting:message2]; + } + }]; + + YapDatabaseViewGrouping *grouping = + [YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable(YapDatabaseReadTransaction *_Nonnull transaction, + NSString *_Nonnull collection, + NSString *_Nonnull key, + id _Nonnull object) { + // Don't include nil or not yet downloaded attachments. + if (![object isKindOfClass:[TSAttachmentStream class]]) { + return nil; + } + + TSAttachmentStream *attachment = (TSAttachmentStream *)object; + if (attachment.albumMessageId == nil) { + return nil; + } + + if (!attachment.isValidVisualMedia) { + return nil; + } + + TSMessage *message = [attachment fetchAlbumMessageWithTransaction:transaction]; + if (message == nil) { + OWSFailDebug(@"message was unexpectedly nil"); + return nil; + } + + return [self mediaGroupWithThreadId:message.uniqueThreadId]; + }]; + + YapDatabaseViewOptions *options = [YapDatabaseViewOptions new]; + options.allowedCollections = + [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:TSAttachment.collection]]; + + return [[YapDatabaseAutoView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"4" options:options]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMediaUtils.swift b/SignalUtilitiesKit/OWSMediaUtils.swift new file mode 100644 index 000000000..c74223e9a --- /dev/null +++ b/SignalUtilitiesKit/OWSMediaUtils.swift @@ -0,0 +1,134 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import AVFoundation + +public enum OWSMediaError: Error { + case failure(description: String) +} + +@objc public class OWSMediaUtils: NSObject { + + @available(*, unavailable, message:"do not instantiate this class.") + private override init() { + } + + @objc public class func thumbnail(forImageAtPath path: String, maxDimension: CGFloat) throws -> UIImage { + Logger.verbose("thumbnailing image: \(path)") + + guard FileManager.default.fileExists(atPath: path) else { + throw OWSMediaError.failure(description: "Media file missing.") + } + guard NSData.ows_isValidImage(atPath: path) else { + throw OWSMediaError.failure(description: "Invalid image.") + } + guard let originalImage = UIImage(contentsOfFile: path) else { + throw OWSMediaError.failure(description: "Could not load original image.") + } + guard let thumbnailImage = originalImage.resized(withMaxDimensionPoints: maxDimension) else { + throw OWSMediaError.failure(description: "Could not thumbnail image.") + } + return thumbnailImage + } + + @objc public class func thumbnail(forVideoAtPath path: String, maxDimension: CGFloat) throws -> UIImage { + Logger.verbose("thumbnailing video: \(path)") + + guard isVideoOfValidContentTypeAndSize(path: path) else { + throw OWSMediaError.failure(description: "Media file has missing or invalid length.") + } + + let maxSize = CGSize(width: maxDimension, height: maxDimension) + let url = URL(fileURLWithPath: path) + let asset = AVURLAsset(url: url, options: nil) + guard isValidVideo(asset: asset) else { + throw OWSMediaError.failure(description: "Invalid video.") + } + + let generator = AVAssetImageGenerator(asset: asset) + generator.maximumSize = maxSize + generator.appliesPreferredTrackTransform = true + let time: CMTime = CMTimeMake(value: 1, timescale: 60) + let cgImage = try generator.copyCGImage(at: time, actualTime: nil) + let image = UIImage(cgImage: cgImage) + return image + } + + @objc public class func isValidVideo(path: String) -> Bool { + guard isVideoOfValidContentTypeAndSize(path: path) else { + Logger.error("Media file has missing or invalid length.") + return false + } + + let url = URL(fileURLWithPath: path) + let asset = AVURLAsset(url: url, options: nil) + return isValidVideo(asset: asset) + } + + private class func isVideoOfValidContentTypeAndSize(path: String) -> Bool { + guard FileManager.default.fileExists(atPath: path) else { + Logger.error("Media file missing.") + return false + } + let fileExtension = URL(fileURLWithPath: path).pathExtension + guard let contentType = MIMETypeUtil.mimeType(forFileExtension: fileExtension) else { + Logger.error("Media file has unknown content type.") + return false + } + guard MIMETypeUtil.isSupportedVideoMIMEType(contentType) else { + Logger.error("Media file has invalid content type.") + return false + } + + guard let fileSize = OWSFileSystem.fileSize(ofPath: path) else { + Logger.error("Media file has unknown length.") + return false + } + return fileSize.uintValue <= kMaxFileSizeVideo + } + + private class func isValidVideo(asset: AVURLAsset) -> Bool { + var maxTrackSize = CGSize.zero + for track: AVAssetTrack in asset.tracks(withMediaType: .video) { + let trackSize: CGSize = track.naturalSize + maxTrackSize.width = max(maxTrackSize.width, trackSize.width) + maxTrackSize.height = max(maxTrackSize.height, trackSize.height) + } + if maxTrackSize.width < 1.0 || maxTrackSize.height < 1.0 { + Logger.error("Invalid video size: \(maxTrackSize)") + return false + } + if maxTrackSize.width > kMaxVideoDimensions || maxTrackSize.height > kMaxVideoDimensions { + Logger.error("Invalid video dimensions: \(maxTrackSize)") + return false + } + return true + } + + // MARK: Constants + + /** + * Media Size constraints from Signal-Android + * + * https://github.com/signalapp/Signal-Android/blob/master/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java + */ + @objc + public static var kMaxFileSizeAnimatedImage: UInt { UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier) } + @objc + public static var kMaxFileSizeImage: UInt { UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier) } + @objc + public static var kMaxFileSizeVideo: UInt { UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier) } + @objc + public static var kMaxFileSizeAudio: UInt { UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier) } + @objc + public static var kMaxFileSizeGeneric: UInt { UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier) } + + @objc + public static let kMaxVideoDimensions: CGFloat = 3 * 1024 + @objc + public static let kMaxAnimatedImageDimensions: UInt = 1 * 1024 + @objc + public static let kMaxStillImageDimensions: UInt = 8 * 1024 +} diff --git a/SignalUtilitiesKit/OWSMessageDecrypter.h b/SignalUtilitiesKit/OWSMessageDecrypter.h new file mode 100644 index 000000000..dda173286 --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageDecrypter.h @@ -0,0 +1,47 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "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 diff --git a/SignalUtilitiesKit/OWSMessageDecrypter.m b/SignalUtilitiesKit/OWSMessageDecrypter.m new file mode 100644 index 000000000..c2a7910ac --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageDecrypter.m @@ -0,0 +1,686 @@ +// +// 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 +#import +#import +#import +#import +#import +#import +#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)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 (^_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 = 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 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 diff --git a/SignalUtilitiesKit/OWSMessageHandler.h b/SignalUtilitiesKit/OWSMessageHandler.h new file mode 100644 index 000000000..8722028b7 --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageHandler.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +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 diff --git a/SignalUtilitiesKit/OWSMessageHandler.m b/SignalUtilitiesKit/OWSMessageHandler.m new file mode 100644 index 000000000..892540eae --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageHandler.m @@ -0,0 +1,183 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageHandler.h" +#import + +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:@"", + [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:@"", [self descriptionForSyncMessage:content.syncMessage]]; + } else if (content.dataMessage) { + return [NSString stringWithFormat:@"", [self descriptionForDataMessage:content.dataMessage]]; + } else if (content.callMessage) { + NSString *callMessageDescription = [self descriptionForCallMessage:content.callMessage]; + return [NSString stringWithFormat:@"", callMessageDescription]; + } else if (content.nullMessage) { + return [NSString stringWithFormat:@"", content.nullMessage]; + } else if (content.receiptMessage) { + return [NSString stringWithFormat:@"", content.receiptMessage]; + } else if (content.typingMessage) { + return [NSString stringWithFormat:@"", 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 diff --git a/SignalUtilitiesKit/OWSMessageManager.h b/SignalUtilitiesKit/OWSMessageManager.h new file mode 100644 index 000000000..b6c2817bc --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageManager.h @@ -0,0 +1,33 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "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 diff --git a/SignalUtilitiesKit/OWSMessageManager.m b/SignalUtilitiesKit/OWSMessageManager.m new file mode 100644 index 000000000..17bf623ef --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageManager.m @@ -0,0 +1,1735 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageManager.h" +#import "AppContext.h" +#import "AppReadiness.h" +#import "ContactsManagerProtocol.h" +#import "MimeTypeUtil.h" +#import "NSNotificationCenter+OWS.h" +#import "NSString+SSK.h" +#import "NotificationsProtocol.h" +#import "OWSAttachmentDownloads.h" +#import "OWSBlockingManager.h" +#import "OWSCallMessageHandler.h" +#import "OWSContact.h" +#import "OWSDevice.h" +#import "OWSDevicesService.h" +#import "OWSDisappearingConfigurationUpdateInfoMessage.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import "OWSDisappearingMessagesJob.h" +#import "LKDeviceLinkMessage.h" +#import "OWSIdentityManager.h" +#import "OWSIncomingMessageFinder.h" +#import "OWSIncomingSentMessageTranscript.h" +#import "OWSMessageSender.h" +#import "OWSMessageUtils.h" +#import "OWSOutgoingNullMessage.h" +#import "OWSOutgoingReceiptManager.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSPrimaryStorage+Loki.h" +#import "OWSPrimaryStorage.h" +#import "OWSReadReceiptManager.h" +#import "OWSRecordTranscriptJob.h" +#import "OWSSyncGroupsMessage.h" +#import "OWSSyncGroupsRequestMessage.h" +#import "ProfileManagerProtocol.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "SSKAsserts.h" +#import "TSAttachment.h" +#import "TSAttachmentPointer.h" +#import "TSAttachmentStream.h" +#import "TSContactThread.h" +#import "TSDatabaseView.h" +#import "TSGroupModel.h" +#import "TSGroupThread.h" +#import "TSIncomingMessage.h" +#import "TSInfoMessage.h" +#import "TSNetworkManager.h" +#import "TSOutgoingMessage.h" +#import "TSQuotedMessage.h" +#import +#import +#import +#import +#import +#import +#import "OWSDispatch.h" +#import "OWSBatchMessageProcessor.h" +#import "OWSQueues.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSMessageManager () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; +@property (nonatomic, readonly) OWSIncomingMessageFinder *incomingMessageFinder; + +@end + +#pragma mark - + +@implementation OWSMessageManager + ++ (instancetype)sharedManager +{ + OWSAssertDebug(SSKEnvironment.shared.messageManager); + + return SSKEnvironment.shared.messageManager; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + + if (!self) { + return self; + } + + _primaryStorage = primaryStorage; + _dbConnection = primaryStorage.newDatabaseConnection; + _incomingMessageFinder = [[OWSIncomingMessageFinder alloc] initWithPrimaryStorage:primaryStorage]; + + OWSSingletonAssert(); + + return self; +} + +- (void)dealloc { + [NSNotificationCenter.defaultCenter removeObserver:self]; +} + +#pragma mark - + +- (id)callMessageHandler +{ + OWSAssertDebug(SSKEnvironment.shared.callMessageHandler); + + return SSKEnvironment.shared.callMessageHandler; +} + +- (id)contactsManager +{ + OWSAssertDebug(SSKEnvironment.shared.contactsManager); + + return SSKEnvironment.shared.contactsManager; +} + +- (SSKMessageSenderJobQueue *)messageSenderJobQueue +{ + return SSKEnvironment.shared.messageSenderJobQueue; +} + +- (OWSBlockingManager *)blockingManager +{ + OWSAssertDebug(SSKEnvironment.shared.blockingManager); + + return SSKEnvironment.shared.blockingManager; +} + +- (OWSIdentityManager *)identityManager +{ + OWSAssertDebug(SSKEnvironment.shared.identityManager); + + return SSKEnvironment.shared.identityManager; +} + +- (TSNetworkManager *)networkManager +{ + OWSAssertDebug(SSKEnvironment.shared.networkManager); + + return SSKEnvironment.shared.networkManager; +} + +- (OWSOutgoingReceiptManager *)outgoingReceiptManager +{ + OWSAssertDebug(SSKEnvironment.shared.outgoingReceiptManager); + + return SSKEnvironment.shared.outgoingReceiptManager; +} + +- (id)syncManager +{ + OWSAssertDebug(SSKEnvironment.shared.syncManager); + + return SSKEnvironment.shared.syncManager; +} + +- (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +- (id)profileManager +{ + return SSKEnvironment.shared.profileManager; +} + +- (id)typingIndicators +{ + return SSKEnvironment.shared.typingIndicators; +} + +- (OWSAttachmentDownloads *)attachmentDownloads +{ + return SSKEnvironment.shared.attachmentDownloads; +} + +#pragma mark - + +- (void)startObserving +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModified:) + name:YapDatabaseModifiedNotification + object:OWSPrimaryStorage.sharedManager.dbNotificationObject]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModified:) + name:YapDatabaseModifiedExternallyNotification + object:nil]; +} + +- (void)yapDatabaseModified:(NSNotification *)notification +{ + if (AppReadiness.isAppReady) { + [OWSMessageUtils.sharedManager updateApplicationBadgeCount]; + } else { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [OWSMessageUtils.sharedManager updateApplicationBadgeCount]; + }]; + }); + } +} + +#pragma mark - + +- (BOOL)isEnvelopeSenderBlocked:(SSKProtoEnvelope *)envelope +{ + OWSAssertDebug(envelope); + + return [self.blockingManager isRecipientIdBlocked:envelope.source]; +} + +- (BOOL)isDataMessageBlocked:(SSKProtoDataMessage *)dataMessage envelope:(SSKProtoEnvelope *)envelope +{ + OWSAssertDebug(dataMessage); + OWSAssertDebug(envelope); + + if (dataMessage.group) { + return [self.blockingManager isGroupIdBlocked:dataMessage.group.id]; + } else { + BOOL senderBlocked = [self isEnvelopeSenderBlocked:envelope]; + + // If the envelopeSender was blocked, we never should have gotten as far as decrypting the dataMessage. + OWSAssertDebug(!senderBlocked); + + return senderBlocked; + } +} + +#pragma mark - + +- (void)throws_processEnvelope:(SSKProtoEnvelope *)envelope + plaintextData:(NSData *_Nullable)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction + serverID:(uint64_t)serverID +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + if (!self.tsAccountManager.isRegistered) { + OWSFailDebug(@"Not registered."); + return; + } + + OWSLogInfo(@"Handling decrypted envelope: %@.", [self descriptionForEnvelope:envelope]); + + if (!envelope.hasSource || envelope.source.length < 1) { + OWSFailDebug(@"Incoming envelope with invalid source."); + return; + } + if (!envelope.hasSourceDevice || envelope.sourceDevice < 1) { + OWSFailDebug(@"Incoming envelope with invalid source device."); + return; + } + + if ([self isEnvelopeSenderBlocked:envelope]) { + return; + } + + [self checkForUnknownLinkedDevice:envelope transaction:transaction]; + + switch (envelope.type) { + case SSKProtoEnvelopeTypeFallbackMessage: + case SSKProtoEnvelopeTypeCiphertext: + case SSKProtoEnvelopeTypePrekeyBundle: + case SSKProtoEnvelopeTypeClosedGroupCiphertext: + case SSKProtoEnvelopeTypeUnidentifiedSender: + if (!plaintextData) { + OWSFailDebug(@"missing decrypted data for envelope: %@", [self descriptionForEnvelope:envelope]); + return; + } + [self throws_handleEnvelope:envelope + plaintextData:plaintextData + wasReceivedByUD:wasReceivedByUD + transaction:transaction + serverID:serverID]; + break; + case SSKProtoEnvelopeTypeReceipt: + OWSAssertDebug(!plaintextData); + [self handleDeliveryReceipt:envelope transaction:transaction]; + break; + // Other messages are just dismissed for now. + case SSKProtoEnvelopeTypeKeyExchange: + OWSLogWarn(@"Received Key Exchange Message, not supported"); + break; + case SSKProtoEnvelopeTypeUnknown: + OWSLogWarn(@"Received an unknown message type"); + break; + default: + OWSLogWarn(@"Received unhandled envelope type: %d", (int)envelope.type); + break; + } +} + +- (void)handleDeliveryReceipt:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + // Old-style delivery notices don't include a "delivery timestamp". + [self processDeliveryReceiptsFromRecipientId:envelope.source + sentTimestamps:@[ + @(envelope.timestamp), + ] + deliveryTimestamp:nil + transaction:transaction]; +} + +// deliveryTimestamp is an optional parameter, since legacy +// delivery receipts don't have a "delivery timestamp". Those +// messages repurpose the "timestamp" field to indicate when the +// corresponding message was originally sent. +- (void)processDeliveryReceiptsFromRecipientId:(NSString *)recipientId + sentTimestamps:(NSArray *)sentTimestamps + deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (recipientId.length < 1) { + OWSFailDebug(@"Empty recipientId."); + return; + } + if (sentTimestamps.count < 1) { + OWSFailDebug(@"Missing sentTimestamps."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + for (NSNumber *nsTimestamp in sentTimestamps) { + uint64_t timestamp = [nsTimestamp unsignedLongLongValue]; + + NSArray *messages + = (NSArray *)[TSInteraction interactionsWithTimestamp:timestamp + ofClass:[TSOutgoingMessage class] + withTransaction:transaction]; + if (messages.count < 1) { + // The service sends delivery receipts for "unpersisted" messages + // like group updates, so these errors are expected to a certain extent. + // + // TODO: persist "early" delivery receipts. + OWSLogInfo(@"Missing message for delivery receipt: %llu", timestamp); + } else { + if (messages.count > 1) { + OWSLogInfo(@"More than one message (%lu) for delivery receipt: %llu", + (unsigned long)messages.count, + timestamp); + } + for (TSOutgoingMessage *outgoingMessage in messages) { + [outgoingMessage updateWithDeliveredRecipient:recipientId + deliveryTimestamp:deliveryTimestamp + transaction:transaction]; + } + } + } +} + +- (void)throws_handleEnvelope:(SSKProtoEnvelope *)envelope + plaintextData:(NSData *)plaintextData + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction + serverID:(uint64_t)serverID +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!plaintextData) { + OWSFailDebug(@"Missing plaintextData."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + if (envelope.timestamp < 1) { + OWSFailDebug(@"Invalid timestamp."); + return; + } + if (envelope.source.length < 1) { + OWSFailDebug(@"Missing source."); + return; + } + if (envelope.sourceDevice < 1) { + OWSFailDebug(@"Invalid source device."); + return; + } + + OWSPrimaryStorage *storage = OWSPrimaryStorage.sharedManager; + __block NSSet *senderLinkedDevices; + [storage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + senderLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:envelope.source in:transaction]; + }]; + + BOOL duplicateEnvelope = NO; + for (NSString *publicKey in senderLinkedDevices) { + duplicateEnvelope = duplicateEnvelope + || [self.incomingMessageFinder existsMessageWithTimestamp:envelope.timestamp + sourceId:publicKey + sourceDeviceId:envelope.sourceDevice + transaction:transaction]; + } + + if (duplicateEnvelope) { + OWSLogInfo(@"Ignoring previously received envelope from: %@ with timestamp: %llu.", + envelopeAddress(envelope), + envelope.timestamp); + return; + } + + if (envelope.content != nil) { + NSError *error; + SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:plaintextData error:&error]; + if (error != nil || contentProto == nil) { + OWSFailDebug(@"Couldn't parse proto due to error: %@.", error); + return; + } + OWSLogInfo(@"Handling content: .", [self descriptionForContent:contentProto]); + + if ([LKSyncMessagesProtocol isDuplicateSyncMessage:contentProto fromPublicKey:envelope.source]) { + [LKLogger print:@"[Loki] Ignoring duplicate sync transcript."]; + return; + } + + [LKSessionManagementProtocol handlePreKeyBundleMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction]; + + if (contentProto.lokiDeviceLinkMessage != nil) { + [LKMultiDeviceProtocol handleDeviceLinkMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction]; + } else if (contentProto.syncMessage) { + [self throws_handleIncomingEnvelope:envelope + withSyncMessage:contentProto.syncMessage + transaction:transaction + serverID:serverID]; + + [[OWSDeviceManager sharedManager] setHasReceivedSyncMessage]; + } else if (contentProto.dataMessage) { + [self handleIncomingEnvelope:envelope + withDataMessage:contentProto.dataMessage + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; + } else if (contentProto.callMessage) { + [self handleIncomingEnvelope:envelope withCallMessage:contentProto.callMessage]; + } else if (contentProto.typingMessage) { + [self handleIncomingEnvelope:envelope withTypingMessage:contentProto.typingMessage transaction:transaction]; + } else if (contentProto.receiptMessage) { + [self handleIncomingEnvelope:envelope + withReceiptMessage:contentProto.receiptMessage + transaction:transaction]; + } else { + OWSLogWarn(@"Ignoring envelope. Content with no known payload"); + } + } else if (envelope.legacyMessage != nil) { // DEPRECATED - Remove after all clients have been upgraded. + NSError *error; + SSKProtoDataMessage *_Nullable dataMessageProto = [SSKProtoDataMessage parseData:plaintextData error:&error]; + if (error || !dataMessageProto) { + OWSFailDebug(@"could not parse proto: %@", error); + return; + } + OWSLogInfo(@"handling message: ", [self descriptionForDataMessage:dataMessageProto]); + + [self handleIncomingEnvelope:envelope + withDataMessage:dataMessageProto + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; + } else { + + } +} + +- (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope + withDataMessage:(SSKProtoDataMessage *)dataMessage + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + if ([self isDataMessageBlocked:dataMessage envelope:envelope]) { + NSString *logMessage = [NSString stringWithFormat:@"Ignoring blocked message from sender: %@.", envelope.source]; + if (dataMessage.group) { + logMessage = [logMessage stringByAppendingFormat:@" in group: %@", dataMessage.group.id]; + } + OWSLogError(@"%@", logMessage); + return; + } + + if (dataMessage.hasTimestamp) { + if (dataMessage.timestamp <= 0) { + OWSFailDebug(@"Ignoring data message with invalid timestamp: %@.", envelope.source); + return; + } + // This prevents replay attacks by the service. + if (dataMessage.timestamp != envelope.timestamp) { + OWSFailDebug(@"Ignoring data message with non-matching timestamp: %@.", envelope.source); + return; + } + } + + [LKClosedGroupsProtocol handleSharedSenderKeysUpdateIfNeeded:dataMessage from:envelope.source transaction:transaction]; + + if (dataMessage.group) { + TSGroupThread *_Nullable groupThread = + [TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction]; + + if (groupThread) { + if (groupThread.groupModel.groupType == closedGroup) { + if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupMessage:dataMessage inThread:groupThread wrappedIn:envelope]) { return; } + } + + if (dataMessage.group.type != SSKProtoGroupContextTypeUpdate) { + if (![groupThread isCurrentUserInGroupWithTransaction:transaction]) { + OWSLogInfo(@"Ignoring messages for left group."); + return; + } + } + } else { + // Unknown group + if (dataMessage.group.type == SSKProtoGroupContextTypeUpdate) { + // Accept group updates for unknown groups + } else if (dataMessage.group.type == SSKProtoGroupContextTypeDeliver) { + [self sendGroupInfoRequest:dataMessage.group.id envelope:envelope transaction:transaction]; + return; + } else { + OWSLogInfo(@"Ignoring group message for unknown group from: %@.", envelope.source); + return; + } + } + } + + if ((dataMessage.flags & SSKProtoDataMessageFlagsEndSession) != 0) { + [self handleEndSessionMessageWithEnvelope:envelope dataMessage:dataMessage transaction:transaction]; + } else if ((dataMessage.flags & SSKProtoDataMessageFlagsExpirationTimerUpdate) != 0) { + [self handleExpirationTimerUpdateMessageWithEnvelope:envelope dataMessage:dataMessage transaction:transaction]; + } else if ((dataMessage.flags & SSKProtoDataMessageFlagsProfileKeyUpdate) != 0) { + [self handleProfileKeyMessageWithEnvelope:envelope dataMessage:dataMessage]; + } else if ([LKMultiDeviceProtocol isUnlinkDeviceMessage:dataMessage]) { + [LKMultiDeviceProtocol handleUnlinkDeviceMessage:dataMessage wrappedIn:envelope transaction:transaction]; + } else if (dataMessage.attachments.count > 0) { + [self handleReceivedMediaWithEnvelope:envelope + dataMessage:dataMessage + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; + } else { + [self handleReceivedTextMessageWithEnvelope:envelope + dataMessage:dataMessage + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; + + if ([self isDataMessageGroupAvatarUpdate:dataMessage]) { + OWSLogVerbose(@"Data message had group avatar attachment"); + [self handleReceivedGroupAvatarUpdateWithEnvelope:envelope dataMessage:dataMessage transaction:transaction]; + } + } + + // Send delivery receipts for "valid data" messages received via UD. + if (wasReceivedByUD) { + [self.outgoingReceiptManager enqueueDeliveryReceiptForEnvelope:envelope]; + } +} + +- (void)sendGroupInfoRequest:(NSData *)groupId + envelope:(SSKProtoEnvelope *)envelope + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + if (groupId.length < 1) { + OWSFailDebug(@"Invalid groupId."); + return; + } + + // FIXME: https://github.com/signalapp/Signal-iOS/issues/1340 + OWSLogInfo(@"Sending group info request: %@", envelopeAddress(envelope)); + + NSString *recipientId = envelope.source; + + TSThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId transaction:transaction]; + + OWSSyncGroupsRequestMessage *syncGroupsRequestMessage = + [[OWSSyncGroupsRequestMessage alloc] initWithThread:thread groupId:groupId]; + + [self.messageSenderJobQueue addMessage:syncGroupsRequestMessage transaction:transaction]; +} + +- (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope + withReceiptMessage:(SSKProtoReceiptMessage *)receiptMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!receiptMessage) { + OWSFailDebug(@"Missing receiptMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + NSArray *sentTimestamps = receiptMessage.timestamp; + + switch (receiptMessage.type) { + case SSKProtoReceiptMessageTypeDelivery: + OWSLogVerbose(@"Processing receipt message with delivery receipts."); + [self processDeliveryReceiptsFromRecipientId:envelope.source + sentTimestamps:sentTimestamps + deliveryTimestamp:@(envelope.timestamp) + transaction:transaction]; + return; + case SSKProtoReceiptMessageTypeRead: + OWSLogVerbose(@"Processing receipt message with read receipts."); + [OWSReadReceiptManager.sharedManager processReadReceiptsFromRecipientId:envelope.source + sentTimestamps:sentTimestamps + readTimestamp:envelope.timestamp]; + break; + default: + OWSLogInfo(@"Ignoring receipt message of unknown type: %d.", (int)receiptMessage.type); + return; + } +} + +- (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope + withCallMessage:(SSKProtoCallMessage *)callMessage +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!callMessage) { + OWSFailDebug(@"Missing callMessage."); + return; + } + + if ([callMessage hasProfileKey]) { + NSData *profileKey = [callMessage profileKey]; + NSString *recipientId = envelope.source; + [self.profileManager setProfileKeyData:profileKey forRecipientId:recipientId]; + } + + // By dispatching async, we introduce the possibility that these messages might be lost + // if the app exits before this block is executed. This is fine, since the call by + // definition will end if the app exits. + dispatch_async(dispatch_get_main_queue(), ^{ + if (callMessage.offer) { + [self.callMessageHandler receivedOffer:callMessage.offer fromCallerId:envelope.source]; + } else if (callMessage.answer) { + [self.callMessageHandler receivedAnswer:callMessage.answer fromCallerId:envelope.source]; + } else if (callMessage.iceUpdate.count > 0) { + for (SSKProtoCallMessageIceUpdate *iceUpdate in callMessage.iceUpdate) { + [self.callMessageHandler receivedIceUpdate:iceUpdate fromCallerId:envelope.source]; + } + } else if (callMessage.hangup) { + OWSLogVerbose(@"Received CallMessage with Hangup."); + [self.callMessageHandler receivedHangup:callMessage.hangup fromCallerId:envelope.source]; + } else if (callMessage.busy) { + [self.callMessageHandler receivedBusy:callMessage.busy fromCallerId:envelope.source]; + } else { + + } + }); +} + +- (void)handleIncomingEnvelope:(SSKProtoEnvelope *)envelope + withTypingMessage:(SSKProtoTypingMessage *)typingMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!typingMessage) { + OWSFailDebug(@"Missing typingMessage."); + return; + } + if (typingMessage.timestamp != envelope.timestamp) { + OWSFailDebug(@"typingMessage has invalid timestamp."); + return; + } + NSString *localNumber = self.tsAccountManager.localNumber; + if ([localNumber isEqualToString:envelope.source]) { + OWSLogVerbose(@"Ignoring typing indicators from self or linked device."); + return; + } else if ([self.blockingManager isRecipientIdBlocked:envelope.source] + || (typingMessage.hasGroupID && [self.blockingManager isGroupIdBlocked:typingMessage.groupID])) { + NSString *logMessage = [NSString stringWithFormat:@"Ignoring blocked message from sender: %@", envelope.source]; + if (typingMessage.hasGroupID) { + logMessage = [logMessage stringByAppendingFormat:@" in group: %@", typingMessage.groupID]; + } + OWSLogError(@"%@", logMessage); + return; + } + + TSThread *_Nullable thread; + if (typingMessage.hasGroupID) { + TSGroupThread *groupThread = [TSGroupThread threadWithGroupId:typingMessage.groupID transaction:transaction]; + + if (![groupThread isCurrentUserInGroupWithTransaction:transaction]) { + OWSLogInfo(@"Ignoring messages for left group."); + return; + } + + thread = groupThread; + } else { + thread = [TSContactThread getThreadWithContactId:envelope.source transaction:transaction]; + } + + if (!thread) { + // This isn't neccesarily an error. We might not yet know about the thread, + // in which case we don't need to display the typing indicators. + OWSLogWarn(@"Could not locate thread for typingMessage."); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + switch (typingMessage.action) { + case SSKProtoTypingMessageActionStarted: + [self.typingIndicators didReceiveTypingStartedMessageInThread:thread + recipientId:envelope.source + deviceId:envelope.sourceDevice]; + break; + case SSKProtoTypingMessageActionStopped: + [self.typingIndicators didReceiveTypingStoppedMessageInThread:thread + recipientId:envelope.source + deviceId:envelope.sourceDevice]; + break; + default: + OWSFailDebug(@"Typing message has unexpected action."); + break; + } + }); +} + +- (void)handleReceivedGroupAvatarUpdateWithEnvelope:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + TSGroupThread *_Nullable groupThread = + [TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction]; + if (!groupThread) { + OWSFailDebug(@"Missing group for group avatar update"); + return; + } + + TSAttachmentPointer *_Nullable avatarPointer = + [TSAttachmentPointer attachmentPointerFromProto:dataMessage.group.avatar albumMessage:nil]; + + if (!avatarPointer) { + OWSLogWarn(@"received unsupported group avatar envelope"); + return; + } + [self.attachmentDownloads downloadAttachmentPointer:avatarPointer + success:^(NSArray *attachmentStreams) { + OWSAssertDebug(attachmentStreams.count == 1); + TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; + [groupThread updateAvatarWithAttachmentStream:attachmentStream]; + } + failure:^(NSError *error) { + OWSLogError(@"failed to fetch attachments for group avatar sent at: %llu. with error: %@", + envelope.timestamp, + error); + }]; +} + +- (void)handleReceivedMediaWithEnvelope:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction]; + + if (!thread) { + OWSFailDebug(@"Ignoring media message for unknown group."); + return; + } + + TSIncomingMessage *_Nullable message = [self handleReceivedEnvelope:envelope + withDataMessage:dataMessage + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; + + if (!message) { + return; + } + + [message saveWithTransaction:transaction]; + + OWSLogDebug(@"Incoming attachment message: %@.", message.debugDescription); + + [self.attachmentDownloads downloadAttachmentsForMessage:message + transaction:transaction + success:^(NSArray *attachmentStreams) { + OWSLogDebug(@"Successfully fetched attachments: %lu for message: %@.", + (unsigned long)attachmentStreams.count, + message); + } + failure:^(NSError *error) { + OWSLogError(@"Failed to fetch attachments for message: %@ with error: %@.", message, error); + }]; +} + +- (void)throws_handleIncomingEnvelope:(SSKProtoEnvelope *)envelope + withSyncMessage:(SSKProtoSyncMessage *)syncMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction + serverID:(uint64_t)serverID +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!syncMessage) { + OWSFailDebug(@"Missing syncMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + if (![LKSyncMessagesProtocol isValidSyncMessage:envelope transaction:transaction]) { + return; + } + + if (syncMessage.sent) { + OWSIncomingSentMessageTranscript *transcript = + [[OWSIncomingSentMessageTranscript alloc] initWithProto:syncMessage.sent transaction:transaction]; + + SSKProtoDataMessage *_Nullable dataMessage = syncMessage.sent.message; + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + + // Loki: Update profile if needed (i.e. if the sync message came from the master device) + [LKSyncMessagesProtocol updateProfileFromSyncMessageIfNeeded:dataMessage wrappedIn:envelope transaction:transaction]; + + NSString *destination = syncMessage.sent.destination; + if (dataMessage && destination.length > 0 && dataMessage.hasProfileKey) { + // If we observe a linked device sending our profile key to another + // user, we can infer that that user belongs in our profile whitelist. + if (dataMessage.group) { + [self.profileManager addGroupIdToProfileWhitelist:dataMessage.group.id]; + } else { + [self.profileManager addUserToProfileWhitelist:destination]; + } + } + + if ([self isDataMessageGroupAvatarUpdate:syncMessage.sent.message] && !syncMessage.sent.isRecipientUpdate) { + [OWSRecordTranscriptJob + processIncomingSentMessageTranscript:transcript + serverID:0 + serverTimestamp:0 + attachmentHandler:^(NSArray *attachmentStreams) { + OWSAssertDebug(attachmentStreams.count == 1); + TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; + [LKStorage writeSyncWithBlock:^( + YapDatabaseReadWriteTransaction *transaction) { + TSGroupThread *_Nullable groupThread = + [TSGroupThread threadWithGroupId:dataMessage.group.id + transaction:transaction]; + if (!groupThread) { + OWSFailDebug(@"ignoring sync group avatar update for unknown group."); + return; + } + + [groupThread updateAvatarWithAttachmentStream:attachmentStream + transaction:transaction]; + }]; + } + transaction:transaction + ]; + } else { + if (transcript.isGroupUpdate) { + [LKSyncMessagesProtocol handleClosedGroupUpdateSyncMessageIfNeeded:transcript wrappedIn:envelope transaction:transaction]; + } else if (transcript.isGroupQuit) { + [LKSyncMessagesProtocol handleClosedGroupQuitSyncMessageIfNeeded:transcript wrappedIn:envelope transaction:transaction]; + } else { + [OWSRecordTranscriptJob + processIncomingSentMessageTranscript:transcript + serverID:(serverID ?: 0) + serverTimestamp:(uint64_t)envelope.serverTimestamp + attachmentHandler:^(NSArray *attachmentStreams) { + OWSLogDebug(@"successfully fetched transcript attachments: %lu", + (unsigned long)attachmentStreams.count); + } + transaction:transaction]; + } + } + } else if (syncMessage.request) { + if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeContacts) { + // We respond asynchronously because populating the sync message will + // create transactions and it's not practical (due to locking in the OWSIdentityManager) + // to plumb our transaction through. + // + // In rare cases this means we won't respond to the sync request, but that's + // acceptable. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [[self.syncManager syncAllContacts] retainUntilComplete]; + }); + } else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeGroups) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [[self.syncManager syncAllGroups] retainUntilComplete]; + }); + } else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeBlocked) { + OWSLogInfo(@"Received request for block list"); + [self.blockingManager syncBlockList]; + } else if (syncMessage.request.type == SSKProtoSyncMessageRequestTypeConfiguration) { + [SSKEnvironment.shared.syncManager sendConfigurationSyncMessage]; + } else { + OWSLogWarn(@"ignoring unsupported sync request message"); + } + } else if (syncMessage.blocked) { + NSArray *blockedPhoneNumbers = [syncMessage.blocked.numbers copy]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.blockingManager setBlockedPhoneNumbers:blockedPhoneNumbers sendSyncMessage:NO]; + dispatch_async(dispatch_get_main_queue(), ^{ + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.blockedContactsUpdated object:nil]; + }); + }); + } else if (syncMessage.read.count > 0) { + OWSLogInfo(@"Received %lu read receipt(s)", (unsigned long)syncMessage.read.count); + [OWSReadReceiptManager.sharedManager processReadReceiptsFromLinkedDevice:syncMessage.read + readTimestamp:envelope.timestamp + transaction:transaction]; + } else if (syncMessage.verified) { + OWSLogInfo(@"Received verification state for %@", syncMessage.verified.destination); + [self.identityManager throws_processIncomingSyncMessage:syncMessage.verified transaction:transaction]; + } else if (syncMessage.contacts != nil) { + [LKSyncMessagesProtocol handleContactSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction]; + } else if (syncMessage.groups != nil) { + [LKSyncMessagesProtocol handleClosedGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction]; + } else if (syncMessage.openGroups != nil) { + [LKSyncMessagesProtocol handleOpenGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction]; + } else { + OWSLogWarn(@"Ignoring unsupported sync message."); + } +} + +- (void)handleEndSessionMessageWithEnvelope:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction]; + [LKSessionManagementProtocol handleEndSessionMessageReceivedInThread:thread using:transaction]; +} + +- (void)handleExpirationTimerUpdateMessageWithEnvelope:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction]; + if (!thread) { + OWSFailDebug(@"Ignoring expiring messages update for unknown group."); + return; + } + + OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration; + if (dataMessage.hasExpireTimer && dataMessage.expireTimer > 0) { + OWSLogInfo( + @"Expiring messages duration turned to %u for thread %@", (unsigned int)dataMessage.expireTimer, thread); + disappearingMessagesConfiguration = + [[OWSDisappearingMessagesConfiguration alloc] initWithThreadId:thread.uniqueId + enabled:YES + durationSeconds:dataMessage.expireTimer]; + } else { + OWSLogInfo(@"Expiring messages have been turned off for thread %@", thread); + disappearingMessagesConfiguration = [[OWSDisappearingMessagesConfiguration alloc] + initWithThreadId:thread.uniqueId + enabled:NO + durationSeconds:OWSDisappearingMessagesConfigurationDefaultExpirationDuration]; + } + OWSAssertDebug(disappearingMessagesConfiguration); + [disappearingMessagesConfiguration saveWithTransaction:transaction]; + NSString *name = [dataMessage.profile displayName] ?: [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:envelope.source transaction:transaction] ?: envelope.source; + + // MJK TODO - safe to remove senderTimestamp + OWSDisappearingConfigurationUpdateInfoMessage *message = + [[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + thread:thread + configuration:disappearingMessagesConfiguration + createdByRemoteName:name + createdInExistingGroup:NO]; + [message saveWithTransaction:transaction]; +} + +- (void)handleProfileKeyMessageWithEnvelope:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + + NSString *recipientId = envelope.source; + if (!dataMessage.hasProfileKey) { + OWSFailDebug(@"Received a profile key message without a profile key from: %@.", envelopeAddress(envelope)); + return; + } + + NSData *profileKey = dataMessage.profileKey; + if (profileKey.length != kAES256_KeyByteLength) { + OWSFailDebug(@"received profile key of unexpected length: %lu, from: %@", + (unsigned long)profileKey.length, + envelopeAddress(envelope)); + return; + } + + if (dataMessage.profile == nil) { + OWSFailDebug(@"received profile key message without loki profile attached from: %@", envelopeAddress(envelope)); + return; + } + + id profileManager = SSKEnvironment.shared.profileManager; + [profileManager setProfileKeyData:profileKey forRecipientId:recipientId avatarURL:dataMessage.profile.profilePicture]; +} + +- (void)handleReceivedTextMessageWithEnvelope:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + [self handleReceivedEnvelope:envelope + withDataMessage:dataMessage + wasReceivedByUD:wasReceivedByUD + transaction:transaction]; +} + +- (void)handleGroupInfoRequest:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + if (dataMessage.group.type != SSKProtoGroupContextTypeRequestInfo) { + OWSFailDebug(@"Unexpected group message type."); + return; + } + + NSData *groupId = dataMessage.group ? dataMessage.group.id : nil; + if (!groupId) { + OWSFailDebug(@"Group info request is missing group id."); + return; + } + + OWSLogInfo(@"Received 'Request Group Info' message for group: %@ from: %@", groupId, envelope.source); + + TSGroupThread *_Nullable gThread = [TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction]; + if (!gThread) { + OWSLogWarn(@"Unknown group: %@", groupId); + return; + } + + // Ensure sender is in the group. + if (![gThread isUserMemberInGroup:envelope.source transaction:transaction]) { + OWSLogWarn(@"Ignoring 'Request Group Info' message for non-member of group. %@ not in %@", + envelope.source, + gThread.groupModel.groupMemberIds); + return; + } + + // Ensure we are in the group. + if (![gThread isCurrentUserInGroupWithTransaction:transaction]) { + OWSLogWarn(@"Ignoring 'Request Group Info' message for group we no longer belong to."); + return; + } + + NSString *updateGroupInfo = + [gThread.groupModel getInfoStringAboutUpdateTo:gThread.groupModel contactsManager:self.contactsManager]; + + uint32_t expiresInSeconds = [gThread disappearingMessagesDurationWithTransaction:transaction]; + TSOutgoingMessage *message = [TSOutgoingMessage outgoingMessageInThread:gThread + groupMetaMessage:TSGroupMetaMessageUpdate + expiresInSeconds:expiresInSeconds]; + + [message updateWithCustomMessage:updateGroupInfo transaction:transaction]; + // Only send this group update to the requester. + [message updateWithSendingToSingleGroupRecipient:envelope.source transaction:transaction]; + + if (gThread.groupModel.groupImage) { + NSData *_Nullable data = UIImagePNGRepresentation(gThread.groupModel.groupImage); + OWSAssertDebug(data); + if (data) { + DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithData:data fileExtension:@"png"]; + [self.messageSenderJobQueue addMediaMessage:message + dataSource:dataSource + contentType:OWSMimeTypeImagePng + sourceFilename:nil + caption:nil + albumMessageId:nil + isTemporaryAttachment:YES]; + } + } else { + [self.messageSenderJobQueue addMessage:message transaction:transaction]; + } +} + +- (TSIncomingMessage *_Nullable)handleReceivedEnvelope:(SSKProtoEnvelope *)envelope + withDataMessage:(SSKProtoDataMessage *)dataMessage + wasReceivedByUD:(BOOL)wasReceivedByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return nil; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return nil; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return nil; + } + + uint64_t timestamp = envelope.timestamp; + NSString *body = dataMessage.body; + NSData *groupId = dataMessage.group ? dataMessage.group.id : nil; + OWSContact *_Nullable contact = [OWSContacts contactForDataMessage:dataMessage transaction:transaction]; + NSNumber *_Nullable serverTimestamp = (envelope.hasServerTimestamp ? @(envelope.serverTimestamp) : nil); + + if (dataMessage.group.type == SSKProtoGroupContextTypeRequestInfo) { + [self handleGroupInfoRequest:envelope dataMessage:dataMessage transaction:transaction]; + return nil; + } + + /* + // Loki: Update device links in a blocking way + // FIXME: This is horrible for performance + // FIXME: ======== + // The envelope source is set during UD decryption + if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source] && dataMessage.publicChatInfo == nil // Handled in LokiPublicChatPoller for open group messages + && envelope.type != SSKProtoEnvelopeTypeClosedGroupCiphertext) { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + [[LKMultiDeviceProtocol updateDeviceLinksIfNeededForPublicKey:envelope.source transaction:transaction].ensureOn(queue, ^() { + dispatch_semaphore_signal(semaphore); + }).catchOn(queue, ^(NSError *error) { + dispatch_semaphore_signal(semaphore); + }) retainUntilComplete]; + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + } + // FIXME: ======== + */ + + if (groupId.length > 0) { + NSMutableSet *newMemberIds = [NSMutableSet setWithArray:dataMessage.group.members]; + NSMutableSet *removedMemberIds = [NSMutableSet new]; + for (NSString *recipientId in newMemberIds) { + if (![ECKeyPair isValidHexEncodedPublicKeyWithCandidate:recipientId]) { + OWSLogVerbose(@"Incoming group update has invalid group member: %@", [self descriptionForEnvelope:envelope]); + OWSFailDebug(@"Incoming group update has invalid group member"); + return nil; + } + } + + NSString *senderMasterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source); + NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; + NSString *userMasterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userPublicKey in:transaction] ?: userPublicKey); + + // Group messages create the group if it doesn't already exist. + // + // We distinguish between the old group state (if any) and the new group state. + TSGroupThread *_Nullable oldGroupThread = [TSGroupThread threadWithGroupId:groupId transaction:transaction]; + if (oldGroupThread) { + // Loki: Determine removed members + removedMemberIds = [NSMutableSet setWithArray:oldGroupThread.groupModel.groupMemberIds]; + [removedMemberIds minusSet:newMemberIds]; + // TODO: Below is the original code. Is it safe that we modified it like this? + // ======== + // Don't trust other clients; ensure all known group members remain in the + // group unless it is a "quit" message in which case we should only remove + // the quiting member below. +// [newMemberIds addObjectsFromArray:oldGroupThread.groupModel.groupMemberIds]; + // ======== + } + + [LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:senderMasterPublicKey using:dataMessage]; + + [LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:senderMasterPublicKey using:dataMessage transaction:transaction]; + + switch (dataMessage.group.type) { + case SSKProtoGroupContextTypeUpdate: { + if (oldGroupThread != nil && oldGroupThread.groupModel.groupType == closedGroup + && [LKClosedGroupsProtocol shouldIgnoreClosedGroupUpdateMessage:dataMessage inThread:oldGroupThread wrappedIn:envelope]) { + return nil; + } + // Ensures that the thread exists but don't update it. + TSGroupThread *newGroupThread = + [TSGroupThread getOrCreateThreadWithGroupId:groupId groupType:oldGroupThread.groupModel.groupType transaction:transaction]; + + TSGroupModel *newGroupModel = [[TSGroupModel alloc] initWithTitle:dataMessage.group.name + memberIds:newMemberIds.allObjects + image:oldGroupThread.groupModel.groupImage + groupId:dataMessage.group.id + groupType:oldGroupThread.groupModel.groupType + adminIds:dataMessage.group.admins]; + newGroupModel.removedMembers = removedMemberIds; + NSString *updateGroupInfo = [newGroupThread.groupModel getInfoStringAboutUpdateTo:newGroupModel + contactsManager:self.contactsManager]; + + [newGroupThread setGroupModel:newGroupModel withTransaction:transaction]; + + BOOL wasCurrentUserRemovedFromGroup = [removedMemberIds containsObject:userMasterPublicKey]; + if (!wasCurrentUserRemovedFromGroup) { + [LKClosedGroupsProtocol establishSessionsIfNeededWithClosedGroupMembers:newMemberIds.allObjects transaction:transaction]; + } + + [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer + thread:newGroupThread + createdByRemoteRecipientId:nil + createdInExistingGroup:YES + transaction:transaction]; + + // MJK TODO - should be safe to remove senderTimestamp + TSInfoMessage *infoMessage = [[TSInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:newGroupThread + messageType:TSInfoMessageTypeGroupUpdate + customMessage:updateGroupInfo]; + [infoMessage saveWithTransaction:transaction]; + + // If we were the one that was removed then we need to leave the group + if (wasCurrentUserRemovedFromGroup) { + [newGroupThread leaveGroupWithTransaction:transaction]; + } + + return nil; + } + case SSKProtoGroupContextTypeQuit: { + if (!oldGroupThread) { + OWSLogWarn(@"Ignoring quit group message from unknown group."); + return nil; + } + newMemberIds = [NSMutableSet setWithArray:oldGroupThread.groupModel.groupMemberIds]; + [newMemberIds removeObject:senderMasterPublicKey]; + oldGroupThread.groupModel.groupMemberIds = [newMemberIds.allObjects mutableCopy]; + [oldGroupThread saveWithTransaction:transaction]; + + NSString *nameString = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:senderMasterPublicKey transaction:transaction] ?: + [self.contactsManager displayNameForPhoneIdentifier:senderMasterPublicKey transaction:transaction]; + NSString *updateGroupInfo = + [NSString stringWithFormat:NSLocalizedString(@"GROUP_MEMBER_LEFT", @""), nameString]; + // MJK TODO - should be safe to remove senderTimestamp + [[[TSInfoMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:oldGroupThread + messageType:TSInfoMessageTypeGroupUpdate + customMessage:updateGroupInfo] saveWithTransaction:transaction]; + + // If we were the one that quit then we need to leave the group (only relevant for slave + // devices in a multi device context) + // TODO: This needs more documentation + if (![newMemberIds containsObject:userMasterPublicKey]) { + [oldGroupThread leaveGroupWithTransaction:transaction]; + } + + return nil; + } + case SSKProtoGroupContextTypeDeliver: { + if (!oldGroupThread) { + OWSFailDebug(@"Ignoring deliver group message from unknown group."); + return nil; + } + + [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer + thread:oldGroupThread + createdByRemoteRecipientId:senderMasterPublicKey + createdInExistingGroup:NO + transaction:transaction]; + + TSQuotedMessage *_Nullable quotedMessage = [TSQuotedMessage quotedMessageForDataMessage:dataMessage + thread:oldGroupThread + transaction:transaction]; + + NSError *linkPreviewError; + OWSLinkPreview *_Nullable linkPreview = + [OWSLinkPreview buildValidatedLinkPreviewWithDataMessage:dataMessage + body:body + transaction:transaction + error:&linkPreviewError]; + if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { + OWSLogError(@"linkPreviewError: %@", linkPreviewError); + } + + OWSLogDebug(@"Incoming message from: %@ for group: %@ with timestamp: %lu", + envelopeAddress(envelope), + groupId, + (unsigned long)timestamp); + + // Legit usage of senderTimestamp when creating an incoming group message record + TSIncomingMessage *incomingMessage = + [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp + inThread:oldGroupThread + authorId:senderMasterPublicKey + sourceDeviceId:envelope.sourceDevice + messageBody:body + attachmentIds:@[] + expiresInSeconds:dataMessage.expireTimer + quotedMessage:quotedMessage + contactShare:contact + linkPreview:linkPreview + serverTimestamp:serverTimestamp + wasReceivedByUD:wasReceivedByUD]; + + // For open group messages, use the server timestamp as the received timestamp + if (oldGroupThread.isPublicChat) { + [incomingMessage setServerTimestampToReceivedTimestamp:(uint64_t)envelope.serverTimestamp]; + } + + // Loki: Set open group server ID if needed + if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) { + incomingMessage.openGroupServerMessageID = dataMessage.publicChatInfo.serverID; + } + + NSArray *attachmentPointers = + [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments + albumMessage:incomingMessage]; + for (TSAttachmentPointer *pointer in attachmentPointers) { + [pointer saveWithTransaction:transaction]; + [incomingMessage.attachmentIds addObject:pointer.uniqueId]; + } + + if (body.length == 0 && attachmentPointers.count < 1 && !contact) { + OWSLogWarn(@"Ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu.", + senderMasterPublicKey, + groupId, + (unsigned long)timestamp); + return nil; + } + + // Loki: Cache the user public key (for mentions) + dispatch_async(dispatch_get_main_queue(), ^{ + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [LKMentionsManager populateUserPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction]; + [LKMentionsManager cache:incomingMessage.authorId for:oldGroupThread.uniqueId]; + }]; + }); + + [self finalizeIncomingMessage:incomingMessage + thread:oldGroupThread + masterThread:oldGroupThread + envelope:envelope + transaction:transaction]; + + // Loki: Map the message ID to the message server ID if needed + if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) { + [self.primaryStorage setIDForMessageWithServerID:dataMessage.publicChatInfo.serverID to:incomingMessage.uniqueId in:transaction]; + } + + return incomingMessage; + } + default: { + OWSLogWarn(@"Ignoring unknown group message type: %d.", (int)dataMessage.group.type); + return nil; + } + } + } else { + + // Loki: A message from a slave device should appear as if it came from the master device; the underlying + // friend request logic, however, should still be specific to the slave device. + + NSString *publicKey = envelope.source; + NSString *masterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source); + TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:publicKey transaction:transaction]; + TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterPublicKey transaction:transaction]; + + OWSLogDebug(@"Incoming message from: %@ with timestamp: %lu.", publicKey, (unsigned long)timestamp); + + [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer + thread:masterThread + createdByRemoteRecipientId:publicKey + createdInExistingGroup:NO + transaction:transaction]; + + TSQuotedMessage *_Nullable quotedMessage = [TSQuotedMessage quotedMessageForDataMessage:dataMessage + thread:masterThread + transaction:transaction]; + + NSError *linkPreviewError; + OWSLinkPreview *_Nullable linkPreview = + [OWSLinkPreview buildValidatedLinkPreviewWithDataMessage:dataMessage + body:body + transaction:transaction + error:&linkPreviewError]; + if (linkPreviewError && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { + OWSLogError(@"linkPreviewError: %@", linkPreviewError); + } + + // Legit usage of senderTimestamp when creating incoming message from received envelope + TSIncomingMessage *incomingMessage = + [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp + inThread:masterThread + authorId:masterThread.contactIdentifier + sourceDeviceId:envelope.sourceDevice + messageBody:body + attachmentIds:@[] + expiresInSeconds:dataMessage.expireTimer + quotedMessage:quotedMessage + contactShare:contact + linkPreview:linkPreview + serverTimestamp:serverTimestamp + wasReceivedByUD:wasReceivedByUD]; + + [LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:masterPublicKey using:dataMessage]; + + [LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:masterPublicKey using:dataMessage transaction:transaction]; + + NSArray *attachmentPointers = + [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments albumMessage:incomingMessage]; + for (TSAttachmentPointer *pointer in attachmentPointers) { + [pointer saveWithTransaction:transaction]; + [incomingMessage.attachmentIds addObject:pointer.uniqueId]; + } + + if (body.length == 0 && attachmentPointers.count < 1 && !contact) { return nil; } + + [self finalizeIncomingMessage:incomingMessage + thread:thread + masterThread:thread + envelope:envelope + transaction:transaction]; + + return incomingMessage; + } +} + +- (void)finalizeIncomingMessage:(TSIncomingMessage *)incomingMessage + thread:(TSThread *)thread + masterThread:(TSThread *)masterThread + envelope:(SSKProtoEnvelope *)envelope + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return; + } + if (!thread) { + OWSFailDebug(@"Missing thread."); + return; + } + if (!incomingMessage) { + OWSFailDebug(@"Missing incomingMessage."); + return; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return; + } + + [incomingMessage saveWithTransaction:transaction]; + + // Any messages sent from the current user - from this device or another - should be automatically marked as read. + if ([(masterThread.contactIdentifier ?: envelope.source) isEqualToString:self.tsAccountManager.localNumber]) { + // Don't send a read receipt for messages sent by ourselves. + [incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction]; + } + + // Download the "non-message body" attachments. + NSMutableArray *otherAttachmentIds = [incomingMessage.allAttachmentIds mutableCopy]; + if (incomingMessage.attachmentIds) { + [otherAttachmentIds removeObjectsInArray:incomingMessage.attachmentIds]; + } + for (NSString *attachmentId in otherAttachmentIds) { + TSAttachment *_Nullable attachment = + [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; + if (![attachment isKindOfClass:[TSAttachmentPointer class]]) { + OWSLogInfo(@"Skipping attachment stream."); + continue; + } + TSAttachmentPointer *_Nullable attachmentPointer = (TSAttachmentPointer *)attachment; + + OWSLogDebug(@"Downloading attachment for message: %lu", (unsigned long)incomingMessage.timestamp); + + // Use a separate download for each attachment so that: + // + // * We update the message as each comes in. + // * Failures don't interfere with successes. + [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer + success:^(NSArray *attachmentStreams) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + TSAttachmentStream *_Nullable attachmentStream = attachmentStreams.firstObject; + OWSAssertDebug(attachmentStream); + if (attachmentStream && incomingMessage.quotedMessage.thumbnailAttachmentPointerId.length > 0 && + [attachmentStream.uniqueId + isEqualToString:incomingMessage.quotedMessage.thumbnailAttachmentPointerId]) { + [incomingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream]; + [incomingMessage saveWithTransaction:transaction]; + } else { + // We touch the message to trigger redraw of any views displaying it, + // since the attachment might be a contact avatar, etc. + [incomingMessage touchWithTransaction:transaction]; + } + }]; + } + failure:^(NSError *error) { + OWSLogWarn(@"Failed to download attachment for message: %lu with error: %@.", + (unsigned long)incomingMessage.timestamp, + error); + }]; + } + + // In case we already have a read receipt for this new message (this happens sometimes). + [OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage + transaction:transaction]; + + // Update thread preview in inbox + [masterThread touchWithTransaction:transaction]; + + if (CurrentAppContext().isMainApp) { + [SSKEnvironment.shared.notificationsManager notifyUserForIncomingMessage:incomingMessage inThread:masterThread transaction:transaction]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.typingIndicators didReceiveIncomingMessageInThread:masterThread + recipientId:(masterThread.contactIdentifier ?: envelope.source) + deviceId:envelope.sourceDevice]; + }); +} + +#pragma mark - helpers + +- (BOOL)isDataMessageGroupAvatarUpdate:(SSKProtoDataMessage *)dataMessage +{ + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return NO; + } + + return (dataMessage.group != nil && dataMessage.group.type == SSKProtoGroupContextTypeUpdate + && dataMessage.group.avatar != nil); +} + +/** + * @returns + * Group or Contact thread for message, creating a new contact thread if necessary, + * but never creating a new group thread. + */ +- (nullable TSThread *)threadForEnvelope:(SSKProtoEnvelope *)envelope + dataMessage:(SSKProtoDataMessage *)dataMessage + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!envelope) { + OWSFailDebug(@"Missing envelope."); + return nil; + } + if (!dataMessage) { + OWSFailDebug(@"Missing dataMessage."); + return nil; + } + if (!transaction) { + OWSFail(@"Missing transaction."); + return nil; + } + + if (dataMessage.group) { + NSData *groupId = dataMessage.group.id; + OWSAssertDebug(groupId.length > 0); + TSGroupThread *_Nullable groupThread = [TSGroupThread threadWithGroupId:groupId transaction:transaction]; + // This method should only be called from a code path that has already verified + // that this is a "known" group. + OWSAssertDebug(groupThread); + return groupThread; + } else { + return [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction]; + } +} + +#pragma mark - + +- (void)checkForUnknownLinkedDevice:(SSKProtoEnvelope *)envelope + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(envelope); + OWSAssertDebug(transaction); + + NSString *localNumber = self.tsAccountManager.localNumber; + if (![localNumber isEqualToString:envelope.source]) { + return; + } + + // Consult the device list cache we use for message sending + // whether or not we know about this linked device. + SignalRecipient *_Nullable recipient = + [SignalRecipient registeredRecipientForRecipientId:localNumber mustHaveDevices:NO transaction:transaction]; + if (!recipient) { + + } else { + BOOL isRecipientDevice = [recipient.devices containsObject:@(envelope.sourceDevice)]; + if (!isRecipientDevice) { + OWSLogInfo(@"Message received from unknown linked device; adding to local SignalRecipient: %lu.", + (unsigned long) envelope.sourceDevice); + + [recipient updateRegisteredRecipientWithDevicesToAdd:@[ @(envelope.sourceDevice) ] + devicesToRemove:nil + transaction:transaction]; + } + } + + // Consult the device list cache we use for the "linked device" UI + // whether or not we know about this linked device. + NSMutableSet *deviceIdSet = [NSMutableSet new]; + for (OWSDevice *device in [OWSDevice currentDevicesWithTransaction:transaction]) { + [deviceIdSet addObject:@(device.deviceId)]; + } + BOOL isInDeviceList = [deviceIdSet containsObject:@(envelope.sourceDevice)]; + if (!isInDeviceList) { + OWSLogInfo(@"Message received from unknown linked device; refreshing device list: %lu.", + (unsigned long) envelope.sourceDevice); + + [OWSDevicesService refreshDevices]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.profileManager fetchLocalUsersProfile]; + }); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMessageReceiver.h b/SignalUtilitiesKit/OWSMessageReceiver.h new file mode 100644 index 000000000..f8946d0f2 --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageReceiver.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +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 diff --git a/SignalUtilitiesKit/OWSMessageReceiver.m b/SignalUtilitiesKit/OWSMessageReceiver.m new file mode 100644 index 000000000..47332c9a5 --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageReceiver.m @@ -0,0 +1,513 @@ +// +// 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 +#import +#import +#import +#import +#import +#import +#import +#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 diff --git a/SignalUtilitiesKit/OWSMessageSend.swift b/SignalUtilitiesKit/OWSMessageSend.swift new file mode 100644 index 000000000..36dd9241f --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageSend.swift @@ -0,0 +1,96 @@ +// +// 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() + } +} diff --git a/SignalUtilitiesKit/OWSMessageSender.h b/SignalUtilitiesKit/OWSMessageSender.h new file mode 100644 index 000000000..374056f9d --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageSender.h @@ -0,0 +1,121 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "DataSource.h" +#import "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 *)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 *)prepareMessageForSending:(TSOutgoingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +/// Writes attachment to disk and applies original filename to message attributes ++ (void)prepareAttachments:(NSArray *)attachmentInfos + inMessage:(TSOutgoingMessage *)outgoingMessage + completionHandler:(void (^)(NSError *_Nullable error))completionHandler + NS_SWIFT_NAME(prepareAttachments(_:inMessage:completionHandler:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMessageSender.m b/SignalUtilitiesKit/OWSMessageSender.m new file mode 100644 index 000000000..3d6f990c3 --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageSender.m @@ -0,0 +1,1744 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageSender.h" +#import "AppContext.h" +#import "NSData+keyVersionByte.h" +#import "NSData+messagePadding.h" +#import "NSError+MessageSending.h" +#import "OWSBackgroundTask.h" +#import "OWSBlockingManager.h" +#import "OWSContact.h" +#import "OWSDevice.h" +#import "OWSDisappearingMessagesJob.h" +#import "OWSDispatch.h" +#import "OWSEndSessionMessage.h" +#import "OWSError.h" +#import "OWSIdentityManager.h" +#import "OWSMessageServiceParams.h" +#import "OWSOperation.h" +#import "OWSOutgoingSentMessageTranscript.h" +#import "OWSOutgoingSyncMessage.h" +#import "OWSPrimaryStorage+PreKeyStore.h" +#import "OWSPrimaryStorage+SignedPreKeyStore.h" +#import "OWSPrimaryStorage+sessionStore.h" +#import "OWSPrimaryStorage+Loki.h" +#import "OWSPrimaryStorage.h" +#import "OWSRequestFactory.h" +#import "OWSUploadOperation.h" +#import "PreKeyBundle+jsonDict.h" +#import "SSKEnvironment.h" +#import "SignalRecipient.h" +#import "TSAccountManager.h" +#import "TSAttachmentStream.h" +#import "TSContactThread.h" +#import "TSGroupThread.h" +#import "TSIncomingMessage.h" +#import "TSInfoMessage.h" +#import "TSInvalidIdentityKeySendingErrorMessage.h" +#import "TSNetworkManager.h" +#import "TSOutgoingMessage.h" +#import "TSPreKeyManager.h" +#import "TSQuotedMessage.h" +#import "TSRequest.h" +#import "TSSocketManager.h" +#import "TSThread.h" +#import "TSContactThread.h" +#import "LKDeviceLinkMessage.h" +#import "LKUnlinkDeviceMessage.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "SSKAsserts.h" +#import "SignalRecipient.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *NoSessionForTransientMessageException = @"NoSessionForTransientMessageException"; + +const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; + +NSError *SSKEnsureError(NSError *_Nullable error, OWSErrorCode fallbackCode, NSString *fallbackErrorDescription) +{ + if (error) { + return error; + } + OWSCFailDebug(@"Using fallback error."); + return OWSErrorWithCodeDescription(fallbackCode, fallbackErrorDescription); +} + +#pragma mark - + +void AssertIsOnSendingQueue() +{ +#ifdef DEBUG + if (@available(iOS 10.0, *)) { + dispatch_assert_queue([OWSDispatch sendingQueue]); + } // else, skip assert as it's a development convenience. +#endif +} + +#pragma mark - + +@implementation OWSOutgoingAttachmentInfo + +- (instancetype)initWithDataSource:(DataSource *)dataSource + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId +{ + self = [super init]; + if (!self) { + return self; + } + + _dataSource = dataSource; + _contentType = contentType; + _sourceFilename = sourceFilename; + _caption = caption; + _albumMessageId = albumMessageId; + + return self; +} + +@end + +#pragma mark - + +/** + * OWSSendMessageOperation encapsulates all the work associated with sending a message, e.g. uploading attachments, + * getting proper keys, and retrying upon failure. + * + * Used by `OWSMessageSender` to serialize message sending, ensuring that messages are emitted in the order they + * were sent. + */ +@interface OWSSendMessageOperation : OWSOperation + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessage:(TSOutgoingMessage *)message + messageSender:(OWSMessageSender *)messageSender + dbConnection:(YapDatabaseConnection *)dbConnection + success:(void (^)(void))aSuccessHandler + failure:(void (^)(NSError * error))aFailureHandler NS_DESIGNATED_INITIALIZER; + +@end + +#pragma mark - + +@interface OWSMessageSender (OWSSendMessageOperation) + +- (void)sendMessageToService:(TSOutgoingMessage *)message + success:(void (^)(void))successHandler + failure:(RetryableFailureHandler)failureHandler; + +@end + +#pragma mark - + +@interface OWSSendMessageOperation () + +@property (nonatomic, readonly) TSOutgoingMessage *message; +@property (nonatomic, readonly) OWSMessageSender *messageSender; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; +@property (nonatomic, readonly) void (^successHandler)(void); +@property (nonatomic, readonly) void (^failureHandler)(NSError *error); + +@end + +#pragma mark - + +@implementation OWSSendMessageOperation + +- (instancetype)initWithMessage:(TSOutgoingMessage *)message + messageSender:(OWSMessageSender *)messageSender + dbConnection:(YapDatabaseConnection *)dbConnection + success:(void (^)(void))successHandler + failure:(void (^)(NSError * error))failureHandler +{ + self = [super init]; + + if (!self) { + return self; + } + + _message = message; + _messageSender = messageSender; + _dbConnection = dbConnection; + _successHandler = successHandler; + _failureHandler = failureHandler; + + return self; +} + +#pragma mark - OWSOperation overrides + +- (nullable NSError *)checkForPreconditionError +{ + __block NSError *_Nullable error = [super checkForPreconditionError]; + if (error) { return error; } + + if (self.message.hasAttachments) { + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + for (TSAttachment *attachment in [self.message attachmentsWithTransaction:transaction]) { + if (![attachment isKindOfClass:[TSAttachmentStream class]]) { + error = OWSErrorMakeFailedToSendOutgoingMessageError(); + break; + } + + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + OWSAssertDebug(attachmentStream); + OWSAssertDebug(attachmentStream.serverId); + OWSAssertDebug(attachmentStream.isUploaded); + } + }]; + } + + return error; +} + +- (void)run +{ + if (self.message.shouldBeSaved && ![TSOutgoingMessage fetchObjectWithUniqueID:self.message.uniqueId]) { + OWSLogInfo(@"Aborting message send; message deleted."); + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageDeletedBeforeSent, @"Message was deleted before it could be sent."); + error.isFatal = YES; + [self reportError:error]; + return; + } + + [self.messageSender sendMessageToService:self.message + success:^{ + [self reportSuccess]; + } + failure:^(NSError *error) { + [self reportError:error]; + }]; +} + +- (void)didSucceed +{ + if (self.message.messageState != TSOutgoingMessageStateSent) { + [LKLogger print:@"[Loki] Succeeded with sending a message, but the message state isn't TSOutgoingMessageStateSent."]; + } + + self.successHandler(); +} + +- (void)didFailWithError:(NSError *)error +{ + OWSLogError(@"Message failed to send due to error: %@.", error); + self.failureHandler(error); +} + +@end + +#pragma mark - + +NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException"; +NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; + +@interface OWSMessageSender () + +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; +@property (atomic, readonly) NSMutableDictionary *sendingQueueMap; + +@end + +#pragma mark - + +@implementation OWSMessageSender + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _primaryStorage = primaryStorage; + _sendingQueueMap = [NSMutableDictionary new]; + _dbConnection = primaryStorage.newDatabaseConnection; + + OWSSingletonAssert(); + + return self; +} + +#pragma mark - Dependencies + +- (id)contactsManager +{ + OWSAssertDebug(SSKEnvironment.shared.contactsManager); + + return SSKEnvironment.shared.contactsManager; +} + +- (OWSBlockingManager *)blockingManager +{ + OWSAssertDebug(SSKEnvironment.shared.blockingManager); + + return SSKEnvironment.shared.blockingManager; +} + +- (TSNetworkManager *)networkManager +{ + OWSAssertDebug(SSKEnvironment.shared.networkManager); + + return SSKEnvironment.shared.networkManager; +} + +- (id)udManager +{ + OWSAssertDebug(SSKEnvironment.shared.udManager); + + return SSKEnvironment.shared.udManager; +} + +- (TSAccountManager *)tsAccountManager +{ + return TSAccountManager.sharedInstance; +} + +- (OWSIdentityManager *)identityManager +{ + return SSKEnvironment.shared.identityManager; +} + +#pragma mark - + +- (NSOperationQueue *)sendingQueueForMessage:(TSOutgoingMessage *)message +{ + OWSAssertDebug(message); + + + NSString *kDefaultQueueKey = @"kDefaultQueueKey"; + NSString *queueKey = message.uniqueThreadId ?: kDefaultQueueKey; + OWSAssertDebug(queueKey.length > 0); + + if ([kDefaultQueueKey isEqualToString:queueKey]) { + // when do we get here? + OWSLogDebug(@"using default message queue"); + } + + @synchronized(self) + { + NSOperationQueue *sendingQueue = self.sendingQueueMap[queueKey]; + + if (!sendingQueue) { + sendingQueue = [NSOperationQueue new]; + sendingQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated; + sendingQueue.maxConcurrentOperationCount = 1; + sendingQueue.name = [NSString stringWithFormat:@"%@:%@", self.logTag, queueKey]; + self.sendingQueueMap[queueKey] = sendingQueue; + } + + return sendingQueue; + } +} + +- (void)sendMessage:(TSOutgoingMessage *)message + success:(void (^)(void))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(message); + + if (message.body.length > 0) { + OWSAssertDebug([message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold); + } + + if (message.shouldBeSaved && !message.thread.isGroupThread && ![LKSessionMetaProtocol isThreadNoteToSelf:message.thread]) { + // Loki: Not strictly true but nice from a UI point of view + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.calculatingPoW object:[[NSNumber alloc] initWithUnsignedLongLong:message.timestamp]]; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + NSMutableArray *allAttachmentIds = [NSMutableArray new]; + + // This method will use a read/write transaction. This transaction + // will block until any open read/write transactions are complete. + // + // That's key - we don't want to send any messages in response + // to an incoming message until processing of that batch of messages + // is complete. For example, we wouldn't want to auto-reply to a + // group info request before that group info request's batch was + // finished processing. Otherwise, we might receive a delivery + // notice for a group update we hadn't yet saved to the database. + // + // So we're using YDB behavior to ensure this invariant, which is a bit + // unorthodox. + if (message.allAttachmentIds.count > 0) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [allAttachmentIds addObjectsFromArray:[OutgoingMessagePreparer prepareMessageForSending:message transaction:transaction]]; + }]; + } + + NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message]; + + OWSSendMessageOperation *sendMessageOperation = + [[OWSSendMessageOperation alloc] initWithMessage:message + messageSender:self + dbConnection:self.dbConnection + success:successHandler + failure:failureHandler]; + + for (NSString *attachmentId in allAttachmentIds) { + OWSUploadOperation *uploadAttachmentOperation = + [[OWSUploadOperation alloc] initWithAttachmentId:attachmentId + threadID:message.thread.uniqueId + dbConnection:self.dbConnection]; + + [sendMessageOperation addDependency:uploadAttachmentOperation]; + [sendingQueue addOperation:uploadAttachmentOperation]; + } + + [sendingQueue addOperation:sendMessageOperation]; + }); +} + +- (void)sendTemporaryAttachment:(DataSource *)dataSource + contentType:(NSString *)contentType + inMessage:(TSOutgoingMessage *)message + success:(void (^)(void))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(dataSource); + + void (^successWithDeleteHandler)(void) = ^() { + successHandler(); + + OWSLogDebug(@"Removing successful temporary attachment message with attachment ids: %@", message.attachmentIds); + [message remove]; + }; + + void (^failureWithDeleteHandler)(NSError *error) = ^(NSError *error) { + failureHandler(error); + + OWSLogDebug(@"Removing failed temporary attachment message with attachment ids: %@", message.attachmentIds); + [message remove]; + }; + + [self sendAttachment:dataSource + contentType:contentType + sourceFilename:nil + albumMessageId:nil + inMessage:message + success:successWithDeleteHandler + failure:failureWithDeleteHandler]; +} + +- (void)sendAttachment:(DataSource *)dataSource + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + albumMessageId:(nullable NSString *)albumMessageId + inMessage:(TSOutgoingMessage *)message + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(dataSource); + + OWSOutgoingAttachmentInfo *attachmentInfo = [[OWSOutgoingAttachmentInfo alloc] initWithDataSource:dataSource + contentType:contentType + sourceFilename:sourceFilename + caption:nil + albumMessageId:albumMessageId]; + [self sendAttachments:@[ attachmentInfo, ] + inMessage:message + success:success + failure:failure]; +} + +- (void)sendAttachments:(NSArray *)attachmentInfos + inMessage:(TSOutgoingMessage *)message + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + OWSAssertDebug(attachmentInfos.count > 0); + + [OutgoingMessagePreparer prepareAttachments:attachmentInfos + inMessage:message + completionHandler:^(NSError *_Nullable error) { + if (error) { + failure(error); + return; + } + [self sendMessage:message success:success failure:failure]; + }]; +} + +- (void)sendMessageToService:(TSOutgoingMessage *)message + success:(void (^)(void))success + failure:(RetryableFailureHandler)failure +{ + [self.udManager ensureSenderCertificateWithSuccess:^(SMKSenderCertificate *senderCertificate) { + OWSAssertDebug(senderCertificate != nil); + dispatch_async(OWSDispatch.sendingQueue, ^{ + [self sendMessageToService:message senderCertificate:senderCertificate success:success failure:failure]; + }); + } + failure:^(NSError *error) { // Should never occur + dispatch_async(OWSDispatch.sendingQueue, ^{ + [self sendMessageToService:message senderCertificate:nil success:success failure:failure]; + }); + }]; +} + +- (nullable NSArray *)unsentRecipientsForMessage:(TSOutgoingMessage *)message + thread:(nullable TSThread *)thread + error:(NSError **)errorHandle +{ + OWSAssertDebug(message); + OWSAssertDebug(errorHandle); + + NSString *userPublicKey = self.tsAccountManager.localNumber; + + __block NSMutableSet *recipientIds = [NSMutableSet new]; + if ([message isKindOfClass:OWSOutgoingSyncMessage.class]) { + recipientIds = [LKSessionMetaProtocol getDestinationsForOutgoingSyncMessage]; + } else if (thread.isGroupThread) { + TSGroupThread *groupThread = (TSGroupThread *)thread; + recipientIds = [LKSessionMetaProtocol getDestinationsForOutgoingGroupMessage:message inThread:thread]; + __block NSString *userMasterPublicKey; + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + userMasterPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userPublicKey in:transaction] ?: userPublicKey; + }]; + if ([recipientIds containsObject:userMasterPublicKey]) { + OWSFailDebug(@"Message send recipients should not include self."); + } + } else if ([thread isKindOfClass:TSContactThread.class]) { + NSString *recipientContactId = ((TSContactThread *)thread).contactIdentifier; + + // Treat 1:1 sends to blocked contacts as failures. + // If we block a user, don't send 1:1 messages to them. The UI + // should prevent this from occurring, but in some edge cases + // you might, for example, have a pending outgoing message when + // you block them. + OWSAssertDebug(recipientContactId.length > 0); + if ([self.blockingManager isRecipientIdBlocked:recipientContactId]) { + OWSLogInfo(@"Skipping 1:1 send to blocked contact: %@", recipientContactId); + NSError *error = OWSErrorMakeMessageSendFailedDueToBlockListError(); + [error setIsRetryable:NO]; + *errorHandle = error; + return nil; + } + + [recipientIds addObject:recipientContactId]; + } else { + OWSFailDebug(@"Unknown message type: %@", [message class]); + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:NO]; + *errorHandle = error; + return nil; + } + + [recipientIds minusSet:[NSSet setWithArray:self.blockingManager.blockedPhoneNumbers]]; + return recipientIds.allObjects; +} + +- (NSArray *)recipientsForRecipientIds:(NSArray *)recipientIds +{ + OWSAssertDebug(recipientIds.count > 0); + + NSMutableArray *recipients = [NSMutableArray new]; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + for (NSString *recipientId in recipientIds) { + SignalRecipient *recipient = + [SignalRecipient getOrBuildUnsavedRecipientForRecipientId:recipientId transaction:transaction]; + [recipients addObject:recipient]; + } + }]; + return [recipients copy]; +} + +- (AnyPromise *)sendPromiseForRecipients:(NSArray *)recipients + message:(TSOutgoingMessage *)message + thread:(nullable TSThread *)thread + senderCertificate:(nullable SMKSenderCertificate *)senderCertificate + sendErrors:(NSMutableArray *)sendErrors +{ + OWSAssertDebug(recipients.count > 0); + OWSAssertDebug(message); + OWSAssertDebug(sendErrors); + + NSMutableArray *sendPromises = [NSMutableArray array]; + + for (SignalRecipient *recipient in recipients) { + AnyPromise *sendPromise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + NSString *localNumber = self.tsAccountManager.localNumber; + + OWSUDAccess *_Nullable theirUDAccess; + if (senderCertificate != nil && ![recipient.recipientId isEqualToString:localNumber]) { + theirUDAccess = [self.udManager udAccessForRecipientId:recipient.recipientId requireSyncAccess:YES]; + } + + OWSMessageSend *messageSend = [[OWSMessageSend alloc] initWithMessage:message + thread:thread + recipient:recipient + senderCertificate:senderCertificate + udAccess:theirUDAccess + localNumber:self.tsAccountManager.localNumber + success:^{ + // The value doesn't matter, we just need any non-NSError value. + resolve(@(1)); + } + failure:^(NSError *error) { + @synchronized(sendErrors) { + [sendErrors addObject:error]; + } + resolve(error); + }]; + +// NSString *publicKey = recipients.firstObject.recipientId; +// if ([LKMultiDeviceProtocol isMultiDeviceRequiredForMessage:message toPublicKey:publicKey]) { // Avoid the write transaction if possible +// [self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { +// [LKMultiDeviceProtocol sendMessageToDestinationAndLinkedDevices:messageSend transaction:transaction]; +// }]; +// } else { + [self sendMessage:messageSend]; +// } + }]; + [sendPromises addObject:sendPromise]; + } + + // We use PMKJoin(), not PMKWhen(), because we don't want the + // completion promise to execute until _all_ send promises + // have either succeeded or failed. PMKWhen() executes as + // soon as any of its input promises fail. + return PMKJoin(sendPromises); +} + +- (void)sendMessageToService:(TSOutgoingMessage *)message + senderCertificate:(nullable SMKSenderCertificate *)senderCertificate + success:(void (^)(void))successHandlerParam + failure:(RetryableFailureHandler)failureHandlerParam +{ + AssertIsOnSendingQueue(); + OWSAssert(senderCertificate); + + void (^successHandler)(void) = ^() { + dispatch_async(OWSDispatch.sendingQueue, ^{ + [self handleMessageSentLocally:message + success:^{ + successHandlerParam(); + } + failure:^(NSError *error) { + OWSLogError(@"Error sending sync message for message: %@ timestamp: %llu.", + message.class, + message.timestamp); + + failureHandlerParam(error); + }]; + }); + }; + void (^failureHandler)(NSError *) = ^(NSError *error) { + if (message.wasSentToAnyRecipient) { + dispatch_async(OWSDispatch.sendingQueue, ^{ + [self handleMessageSentLocally:message + success:^{ + failureHandlerParam(error); + } + failure:^(NSError *syncError) { + OWSLogError(@"Error sending sync message for message: %@ timestamp: %llu, %@.", + message.class, + message.timestamp, + syncError); + + // Discard the sync message error in favor of the original error + failureHandlerParam(error); + }]; + }); + return; + } + + failureHandlerParam(error); + }; + + TSThread *_Nullable thread = message.thread; + + BOOL isSyncMessage = [message isKindOfClass:[OWSOutgoingSyncMessage class]]; + if (thread == nil && !isSyncMessage) { + + // The thread has been deleted since the message was enqueued. + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients, + NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS", @"Error indicating that an outgoing message had no valid recipients.")); + [error setIsRetryable:NO]; + return failureHandler(error); + } + + // In the "self-send" special case, we ony need to send a sync message with a delivery receipt + // Loki: Take into account multi device + if ([LKSessionMetaProtocol isThreadNoteToSelf:thread] + && !([message isKindOfClass:LKDeviceLinkMessage.class]) && !([message isKindOfClass:SNClosedGroupUpdate.class])) { + // Don't mark self-sent messages as read (or sent) until the sync transcript is sent + successHandler(); + return; + } + + if (thread.isGroupThread) { + [self saveInfoMessageForGroupMessage:message inThread:thread]; + } + + NSError *error; + NSArray *_Nullable recipientIds = [self unsentRecipientsForMessage:message thread:thread error:&error]; + if (error || !recipientIds) { + error = SSKEnsureError( + error, OWSErrorCodeMessageSendNoValidRecipients, @"Couldn't build recipient list for message."); + [error setIsRetryable:NO]; + return failureHandler(error); + } + + // Mark skipped recipients as such. We skip because: + // + // * Recipient is no longer in the group. + // * Recipient is blocked. + // + // Elsewhere, we skip recipient if their Signal account has been deactivated. + NSMutableSet *obsoleteRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds]; + [obsoleteRecipientIds minusSet:[NSSet setWithArray:recipientIds]]; + if (obsoleteRecipientIds.count > 0) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSString *recipientId in obsoleteRecipientIds) { + [message updateWithSkippedRecipient:recipientId transaction:transaction]; + } + }]; + } + + if (recipientIds.count < 1) { + successHandler(); + return; + } + + NSArray *recipients = [self recipientsForRecipientIds:recipientIds]; + + BOOL isGroupSend = (thread && thread.isGroupThread); + NSMutableArray *sendErrors = [NSMutableArray array]; + AnyPromise *sendPromise = [self sendPromiseForRecipients:recipients + message:message + thread:thread + senderCertificate:senderCertificate + sendErrors:sendErrors] + .then(^(id value) { + successHandler(); + }); + + sendPromise.catch(^(id failure) { + NSError *firstRetryableError = nil; + NSError *firstNonRetryableError = nil; + + NSArray *sendErrorsCopy; + @synchronized(sendErrors) { + sendErrorsCopy = [sendErrors copy]; + } + + for (NSError *error in sendErrorsCopy) { + // Some errors should be ignored when sending messages + // to groups. See discussion on + // NSError (OWSMessageSender) category. + if (isGroupSend && error.shouldBeIgnoredForGroups) { + continue; + } + + // Some errors should never be retried, in order to avoid + // hitting rate limits, for example. Unfortunately, since + // group send retry is all-or-nothing, we need to fail + // immediately even if some of the other recipients had + // retryable errors. + if (error.isFatal) { + failureHandler(error); + return; + } + + if ([error isRetryable] && !firstRetryableError) { + firstRetryableError = error; + } else if (![error isRetryable] && !firstNonRetryableError) { + firstNonRetryableError = error; + } + } + + // If any of the send errors are retryable, we want to retry. + // Therefore, prefer to propagate a retryable error. + if (firstRetryableError) { + return failureHandler(firstRetryableError); + } else if (firstNonRetryableError) { + return failureHandler(firstNonRetryableError); + } else { + // If we only received errors that we should ignore, + // consider this send a success, unless the message could + // not be sent to any recipient. + if (message.sentRecipientsCount == 0) { + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients, + NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS", @"Error indicating that an outgoing message had no valid recipients.")); + [error setIsRetryable:NO]; + failureHandler(error); + } else { + successHandler(); + } + } + }); + + [sendPromise retainUntilComplete]; +} + +- (nullable NSArray *)deviceMessagesForMessageSend:(OWSMessageSend *)messageSend + error:(NSError **)errorHandle +{ + OWSAssertDebug(messageSend); + OWSAssertDebug(errorHandle); + AssertIsOnSendingQueue(); + + SignalRecipient *recipient = messageSend.recipient; + + NSArray *deviceMessages; + @try { + deviceMessages = [self throws_deviceMessagesForMessageSend:messageSend]; + } @catch (NSException *exception) { + if ([exception.name isEqualToString:NoSessionForTransientMessageException]) { + // When users re-register, we don't want transient messages (like typing + // indicators) to cause users to hit the prekey fetch rate limit. So + // we silently discard these message if there is no pre-existing session + // for the recipient. + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeNoSessionForTransientMessage, @"No session for transient message."); + [error setIsRetryable:NO]; + [error setIsFatal:YES]; + *errorHandle = error; + return nil; + } else if ([exception.name isEqualToString:UntrustedIdentityKeyException]) { + NSString *localizedErrorDescriptionFormat + = NSLocalizedString(@"FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_KEY", + @"action sheet header when re-sending message which failed because of untrusted identity keys"); + + NSString *localizedErrorDescription = + [NSString stringWithFormat:localizedErrorDescriptionFormat, + [self.contactsManager displayNameForPhoneIdentifier:recipient.recipientId]]; + NSError *error = OWSErrorMakeUntrustedIdentityError(localizedErrorDescription, recipient.recipientId); + + // Key will continue to be unaccepted, so no need to retry. It'll only cause us to hit the Pre-Key request + // rate limit + [error setIsRetryable:NO]; + // Avoid the "Too many failures with this contact" error rate limiting. + [error setIsFatal:YES]; + *errorHandle = error; + + PreKeyBundle *_Nullable newKeyBundle = exception.userInfo[TSInvalidPreKeyBundleKey]; + if (newKeyBundle == nil) { + return nil; + } + + if (![newKeyBundle isKindOfClass:[PreKeyBundle class]]) { + return nil; + } + + NSData *newIdentityKeyWithVersion = newKeyBundle.identityKey; + + if (![newIdentityKeyWithVersion isKindOfClass:[NSData class]]) { + return nil; + } + + // TODO migrate to storing the full 33 byte representation of the identity key. + if (newIdentityKeyWithVersion.length != kIdentityKeyLength) { + return nil; + } + + NSData *newIdentityKey = [newIdentityKeyWithVersion throws_removeKeyType]; + [self.identityManager saveRemoteIdentity:newIdentityKey recipientId:recipient.recipientId]; + + return nil; + } + + if ([exception.name isEqualToString:OWSMessageSenderRateLimitedException]) { + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceRateLimited, + NSLocalizedString(@"FAILED_SENDING_BECAUSE_RATE_LIMIT", + @"action sheet header when re-sending message which failed because of too many attempts")); + // We're already rate-limited. No need to exacerbate the problem. + [error setIsRetryable:NO]; + // Avoid exacerbating the rate limiting. + [error setIsFatal:YES]; + *errorHandle = error; + return nil; + } + + OWSLogWarn(@"Could not build device messages: %@", exception); + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:YES]; + *errorHandle = error; + return nil; + } + + return deviceMessages; +} + +- (void)sendMessage:(OWSMessageSend *)messageSend +{ + OWSAssertDebug(messageSend); + OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]); + NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; + if (!messageSend.isUDSend && ![messageSend.recipient.recipientId isEqual:userPublicKey]) { + [LKLogger print:@"[Loki] Non-UD send"]; + } + + TSOutgoingMessage *message = messageSend.message; + SignalRecipient *recipient = messageSend.recipient; + + BOOL notifyPNServer = ((message.body != nil && message.body.length > 0) || message.hasAttachments); + + OWSLogInfo(@"Attempting to send message: %@, timestamp: %llu, recipient: %@.", + message.class, + message.timestamp, + recipient.uniqueId); + + AssertIsOnSendingQueue(); + + if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) { + // Retry pre key update every time user tries to send a message while the app + // is disabled due to pre key update failures. + // + // Only try to update the signed pre key; updating it is sufficient to + // re-enable message sending. + [TSPreKeyManager + rotateSignedPreKeyWithSuccess:^{ + OWSLogInfo(@"New pre keys registered with server."); + NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError(); + [error setIsRetryable:YES]; + return messageSend.failure(error); + } + failure:^(NSError *error) { + OWSLogWarn(@"Failed to update pre keys with the server due to error: %@.", error); + return messageSend.failure(error); + }]; + } + + if (messageSend.remainingAttempts <= 0) { + // We should always fail with a specific error. + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:YES]; + return messageSend.failure(error); + } + + // Consume an attempt. + messageSend.remainingAttempts = messageSend.remainingAttempts - 1; + + // We need to disable UD for sync messages before we build the device messages, + // since we don't want to build a device message for the local device in the + // non-UD auth case. + if ([message isKindOfClass:[OWSOutgoingSyncMessage class]] + && ![message isKindOfClass:[OWSOutgoingSentMessageTranscript class]]) { + [messageSend disableUD]; + } + + NSError *deviceMessagesError; + NSArray *_Nullable deviceMessages; + if (message.thread.isGroupThread && ((TSGroupThread *)message.thread).isPublicChat) { + deviceMessages = @[]; + } else { + deviceMessages = [self deviceMessagesForMessageSend:messageSend error:&deviceMessagesError]; + + // Loki: Remove this when we have shared sender keys + // ======== + if (deviceMessages.count == 0) { + return messageSend.success(); + } + // ======== + } + + if (deviceMessagesError || !deviceMessages) { + OWSAssertDebug(deviceMessagesError); + return messageSend.failure(deviceMessagesError); + } + + for (NSDictionary *deviceMessage in deviceMessages) { + NSNumber *_Nullable messageType = deviceMessage[@"type"]; + OWSAssertDebug(messageType); + BOOL hasValidMessageType; + if (messageSend.isUDSend) { + hasValidMessageType = [messageType isEqualToNumber:@(TSUnidentifiedSenderMessageType)]; + } else { + NSArray *validMessageTypes = @[ @(TSEncryptedWhisperMessageType), @(TSPreKeyWhisperMessageType), @(TSFallbackMessageType), @(TSClosedGroupCiphertextMessageType) ]; + hasValidMessageType = [validMessageTypes containsObject:messageType]; + } + + if (!hasValidMessageType) { + OWSFailDebug(@"Invalid message type: %@.", messageType); + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:NO]; + return messageSend.failure(error); + } + } + + if (deviceMessages.count == 0 && !(message.thread.isGroupThread && ((TSGroupThread *)message.thread).isPublicChat)) { + // This might happen: + // + // * The first (after upgrading?) time we send a sync message to our linked devices. + // * After unlinking all linked devices. + // * After trying and failing to link a device. + // * The first time we send a message to a user, if they don't have their + // default device. For example, if they have unregistered + // their primary but still have a linked device. Or later, when they re-register. + // + // When we're not sure if we have linked devices, we need to try + // to send self-sync messages even if they have no device messages + // so that we can learn from the service whether or not there are + // linked devices that we don't know about. + OWSLogWarn(@"Sending a message with no device messages."); + + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:NO]; + return messageSend.failure(error); + } + + void (^failedMessageSend)(NSError *error) = ^(NSError *error) { + NSUInteger statusCode = 0; + NSData *_Nullable responseData = nil; + if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) { + statusCode = error.code; + NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey]; + if (underlyingError) { + responseData = underlyingError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; + } else { + OWSFailDebug(@"Missing underlying error: %@.", error); + } + } + [self messageSendDidFail:messageSend deviceMessages:deviceMessages statusCode:statusCode error:error responseData:responseData]; + }; + + __block SNOpenGroup *publicChat; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + publicChat = [LKDatabaseUtilities getPublicChatForThreadID:message.uniqueThreadId transaction: transaction]; + }]; + if (publicChat != nil) { + NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; + NSString *displayName = SSKEnvironment.shared.profileManager.localProfileName; + if (displayName == nil) { displayName = @"Anonymous"; } + TSQuotedMessage *quote = message.quotedMessage; + uint64_t quoteID = quote.timestamp; + NSString *quoteePublicKey = quote.authorId; + __block uint64_t quotedMessageServerID = 0; + if (quoteID != 0) { + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + quotedMessageServerID = [LKDatabaseUtilities getServerIDForQuoteWithID:quoteID quoteeHexEncodedPublicKey:quoteePublicKey threadID:messageSend.thread.uniqueId transaction:transaction]; + }]; + } + NSString *body = (message.body != nil && message.body.length > 0) ? message.body : [NSString stringWithFormat:@"%@", @(message.timestamp)]; // Workaround for the fact that the back-end doesn't accept messages without a body + SNOpenGroupMessage *groupMessage = [[SNOpenGroupMessage alloc] initWithSenderPublicKey:userPublicKey displayName:displayName body:body type:SNOpenGroupAPI.openGroupMessageType + timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteePublicKey:quoteePublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID signatureData:nil signatureVersion:0 serverTimestamp:0]; + OWSLinkPreview *linkPreview = message.linkPreview; + if (linkPreview != nil) { + TSAttachmentStream *attachment = [TSAttachmentStream fetchObjectWithUniqueID:linkPreview.imageAttachmentId]; + if (attachment != nil) { + [groupMessage addAttachmentWithKind:@"preview" server:publicChat.server serverID:attachment.serverId contentType:attachment.contentType size:attachment.byteCount fileName:attachment.sourceFilename flags:0 width:@(attachment.imageSize.width).unsignedIntegerValue height:@(attachment.imageSize.height).unsignedIntegerValue caption:attachment.caption url:attachment.downloadURL linkPreviewURL:linkPreview.urlString linkPreviewTitle:linkPreview.title]; + } + } + for (NSString *attachmentID in message.attachmentIds) { + TSAttachmentStream *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentID]; + if (attachment == nil) { continue; } + NSUInteger width = attachment.shouldHaveImageSize ? @(attachment.imageSize.width).unsignedIntegerValue : 0; + NSUInteger height = attachment.shouldHaveImageSize ? @(attachment.imageSize.height).unsignedIntegerValue : 0; + [groupMessage addAttachmentWithKind:@"attachment" server:publicChat.server serverID:attachment.serverId contentType:attachment.contentType size:attachment.byteCount fileName:attachment.sourceFilename flags:0 width:width height:height caption:attachment.caption url:attachment.downloadURL linkPreviewURL:nil linkPreviewTitle:nil]; + } + message.actualSenderHexEncodedPublicKey = userPublicKey; + [[SNOpenGroupAPI sendMessage:groupMessage toGroup:publicChat.channel onServer:publicChat.server] + .thenOn(OWSDispatch.sendingQueue, ^(SNOpenGroupMessage *groupMessage) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [message saveOpenGroupServerMessageID:groupMessage.serverID in:transaction]; + [self.primaryStorage setIDForMessageWithServerID:groupMessage.serverID to:message.uniqueId in:transaction]; + }]; + [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:messageSend.isUDSend wasSentByWebsocket:false]; + }) + .catchOn(OWSDispatch.sendingQueue, ^(NSError *error) { + failedMessageSend(error); + }) retainUntilComplete]; + } else { + NSString *targetPublicKey = recipient.recipientId; + NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; + __block BOOL isUserLinkedDevice; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + isUserLinkedDevice = [LKDatabaseUtilities isUserLinkedDevice:targetPublicKey in:transaction]; + }]; + BOOL isSSKBasedClosedGroup = [messageSend.thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)messageSend.thread).usesSharedSenderKeys; + if (isSSKBasedClosedGroup) { + [LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to SSK based closed group.", message.class]]; + } else if ([targetPublicKey isEqual:userPublicKey]) { + [LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to self.", message.class]]; + } else if (isUserLinkedDevice) { + [LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to %@ (one of the current user's linked devices).", message.class, recipient.recipientId]]; + } else { + [LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to %@.", message.class, recipient.recipientId]]; + } + NSDictionary *signalMessageInfo = deviceMessages.firstObject; + SSKProtoEnvelopeType type = ((NSNumber *)signalMessageInfo[@"type"]).integerValue; + uint32_t senderDeviceID = (type == SSKProtoEnvelopeTypeUnidentifiedSender) ? 0 : OWSDevicePrimaryDeviceId; + NSString *content = signalMessageInfo[@"content"]; + NSString *recipientID = signalMessageInfo[@"destination"]; + uint64_t ttl = ((NSNumber *)signalMessageInfo[@"ttl"]).unsignedIntegerValue; + BOOL isPing = ((NSNumber *)signalMessageInfo[@"isPing"]).boolValue; + uint64_t timestamp = message.timestamp; + NSString *senderID; + if (type == SSKProtoEnvelopeTypeClosedGroupCiphertext) { + senderID = recipientID; + } else if (type == SSKProtoEnvelopeTypeUnidentifiedSender) { + senderID = @""; + } else { + senderID = userPublicKey; + [LKLogger print:@"[Loki] Non-UD send"]; + } + LKSignalMessage *signalMessage = [[LKSignalMessage alloc] initWithType:type timestamp:timestamp senderID:senderID senderDeviceID:senderDeviceID content:content recipientID:recipientID ttl:ttl isPing:isPing]; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + if (!message.skipSave) { + // Update the PoW calculation status + [message saveIsCalculatingProofOfWork:YES withTransaction:transaction]; + } + }]; + // Convenience + void (^handleError)(NSError *error) = ^(NSError *error) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + if (!message.skipSave) { + // Update the PoW calculation status + [message saveIsCalculatingProofOfWork:NO withTransaction:transaction]; + } + }]; + // Handle the error + failedMessageSend(error); + }; + // Send the message + [[LKSnodeAPI sendSignalMessage:signalMessage] + .thenOn(OWSDispatch.sendingQueue, ^(id result) { + NSSet *promises = (NSSet *)result; + __block BOOL isSuccess = NO; + NSUInteger promiseCount = promises.count; + __block NSUInteger errorCount = 0; + for (AnyPromise *promise in promises) { + [promise + .thenOn(OWSDispatch.sendingQueue, ^(id result) { + if (isSuccess) { return; } // Succeed as soon as the first promise succeeds + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageSent object:[[NSNumber alloc] initWithUnsignedLongLong:signalMessage.timestamp]]; + isSuccess = YES; + if (notifyPNServer) { + [LKPushNotificationManager notifyForMessage:signalMessage]; + } + [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:messageSend.isUDSend wasSentByWebsocket:false]; + }) + .catchOn(OWSDispatch.sendingQueue, ^(NSError *error) { + errorCount += 1; + if (errorCount != promiseCount) { return; } // Only error out if all promises failed + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageFailed object:[[NSNumber alloc] initWithUnsignedLongLong:signalMessage.timestamp]]; + handleError(error); + }) retainUntilComplete]; + } + }) + .catchOn(OWSDispatch.sendingQueue, ^(NSError *error) { + handleError(error); + }) retainUntilComplete]; + } +} + +- (void)messageSendDidSucceed:(OWSMessageSend *)messageSend + deviceMessages:(NSArray *)deviceMessages + wasSentByUD:(BOOL)wasSentByUD + wasSentByWebsocket:(BOOL)wasSentByWebsocket +{ + OWSAssertDebug(messageSend); + OWSAssertDebug(deviceMessages); + + SignalRecipient *recipient = messageSend.recipient; + + OWSLogInfo(@"Successfully sent message: %@ timestamp: %llu, wasSentByUD: %d.", + messageSend.message.class, messageSend.message.timestamp, wasSentByUD); + + if (messageSend.isLocalNumber && deviceMessages.count == 0) { + OWSLogInfo(@"Sent a message with no device messages; clearing 'mayHaveLinkedDevices'."); + // In order to avoid skipping necessary sync messages, the default value + // for mayHaveLinkedDevices is YES. Once we've successfully sent a + // sync message with no device messages (e.g. the service has confirmed + // that we have no linked devices), we can set mayHaveLinkedDevices to NO + // to avoid unnecessary message sends for sync messages until we learn + // of a linked device (e.g. through the device linking UI or by receiving + // a sync message, etc.). + [OWSDeviceManager.sharedManager clearMayHaveLinkedDevices]; + } + + dispatch_async(OWSDispatch.sendingQueue, ^{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [messageSend.message updateWithSentRecipient:messageSend.recipient.uniqueId + wasSentByUD:wasSentByUD + transaction:transaction]; + + // If we've just delivered a message to a user, we know they + // have a valid Signal account. + [SignalRecipient markRecipientAsRegisteredAndGet:recipient.recipientId transaction:transaction]; + }]; + + messageSend.success(); + }); +} + +- (void)messageSendDidFail:(OWSMessageSend *)messageSend + deviceMessages:(NSArray *)deviceMessages + statusCode:(NSInteger)statusCode + error:(NSError *)responseError + responseData:(nullable NSData *)responseData +{ + OWSAssertDebug(messageSend); + OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]); + OWSAssertDebug(deviceMessages); + OWSAssertDebug(responseError); + + TSOutgoingMessage *message = messageSend.message; + SignalRecipient *recipient = messageSend.recipient; + + OWSLogInfo(@"Failed to send message: %@, timestamp: %llu, to recipient: %@.", + message.class, + message.timestamp, + recipient.uniqueId); + + void (^retrySend)(void) = ^void() { + if (messageSend.remainingAttempts <= 0) { + return messageSend.failure(responseError); + } + + dispatch_async(OWSDispatch.sendingQueue, ^{ + OWSLogDebug(@"Retrying: %@.", message.debugDescription); + [self sendMessage:messageSend]; + }); + }; + + switch (statusCode) { + case 0: { // Loki + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + [error setIsRetryable:NO]; + return messageSend.failure(error); + } + case 401: { + OWSLogWarn(@"Unable to send due to invalid credentials. Did the user's client get de-authed by " + @"registering elsewhere?"); + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceFailure, + NSLocalizedString(@"ERROR_DESCRIPTION_SENDING_UNAUTHORIZED", @"Error message when attempting to send message")); + // No need to retry if we've been de-authed. + [error setIsRetryable:NO]; + return messageSend.failure(error); + } + default: + retrySend(); + break; + } +} + +- (void)handleMessageSentLocally:(TSOutgoingMessage *)message + success:(void (^)(void))successParam + failure:(RetryableFailureHandler)failure +{ + dispatch_block_t success = ^{ + // Don't mark self-sent messages as read (or sent) until the sync transcript is sent + // Loki: Take into account multi device + BOOL isNoteToSelf = [LKSessionMetaProtocol isThreadNoteToSelf:message.thread]; + if (isNoteToSelf && !([message isKindOfClass:LKDeviceLinkMessage.class]) + && ![message isKindOfClass:SNClosedGroupUpdate.class]) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSString *recipientId in message.sendingRecipientIds) { + [message updateWithReadRecipientId:recipientId readTimestamp:message.timestamp transaction:transaction]; + } + }]; + } + + successParam(); + }; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:message + expirationStartedAt:[NSDate ows_millisecondTimeStamp] + transaction:transaction]; + }]; + + if (!message.shouldSyncTranscript) { + return success(); + } + + BOOL shouldSendTranscript = [LKSessionMetaProtocol shouldSendTranscriptForMessage:message inThread:message.thread]; + if (!shouldSendTranscript) { + return success(); + } + + BOOL isRecipientUpdate = message.hasSyncedTranscript; + [self + sendSyncTranscriptForMessage:message + isRecipientUpdate:isRecipientUpdate + success:^{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [message updateWithHasSyncedTranscript:YES transaction:transaction]; + }]; + + success(); + } + failure:failure]; +} + +- (void)sendSyncTranscriptForMessage:(TSOutgoingMessage *)message + isRecipientUpdate:(BOOL)isRecipientUpdate + success:(void (^)(void))success + failure:(RetryableFailureHandler)failure +{ + OWSOutgoingSentMessageTranscript *sentMessageTranscript = + [[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message isRecipientUpdate:isRecipientUpdate]; + + NSString *userPublicKey = self.tsAccountManager.localNumber; + + // Loki: Send to the user's other device + __block NSSet *userLinkedDevices; + [self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + userLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userPublicKey in:transaction]; + }]; + NSString *otherUserDevice; + for (NSString *device in userLinkedDevices) { + if (![device isEqual:userPublicKey]) { + otherUserDevice = device; + break; + } + } + + NSString *recipientId = otherUserDevice ?: userPublicKey; + __block SignalRecipient *recipient; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + recipient = [SignalRecipient markRecipientAsRegisteredAndGet:recipientId transaction:transaction]; + }]; + + SMKSenderCertificate *senderCertificate = [self.udManager getSenderCertificate]; + OWSUDAccess *recipientUDAccess = nil; + if (senderCertificate != nil) { + recipientUDAccess = [self.udManager udAccessForRecipientId:recipient.recipientId requireSyncAccess:YES]; + } + + // Loki: If the message was aimed at an SSK based closed group, aim the sync transcript at + // the contact thread with the other device rather than also sending it to the group. + __block TSThread *thread = message.thread; + if ([thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)thread).usesSharedSenderKeys) { + [LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) { + thread = [TSContactThread getThreadWithContactId:otherUserDevice transaction:transaction]; + }]; + } + + OWSMessageSend *messageSend = [[OWSMessageSend alloc] initWithMessage:sentMessageTranscript + thread:thread + recipient:recipient + senderCertificate:senderCertificate + udAccess:recipientUDAccess + localNumber:self.tsAccountManager.localNumber + success:^{ + OWSLogInfo(@"Successfully sent sync transcript."); + + success(); + } + failure:^(NSError *error) { + OWSLogInfo(@"Failed to send sync transcript: %@ (isRetryable: %d).", error, error.isRetryable); + + failure(error); + }]; + + [self sendMessage:messageSend]; +} + +- (NSArray *)throws_deviceMessagesForMessageSend:(OWSMessageSend *)messageSend +{ + // Loki: Multi device is handled elsewhere so just send to the provided recipient ID (Signal used + // to send to each of the recipient's devices here) + OWSAssertDebug(messageSend.message != nil); + OWSAssertDebug(messageSend.recipient != nil); + + SignalRecipient *recipient = messageSend.recipient; + NSMutableArray *messagesArray = [NSMutableArray new]; + + NSData *_Nullable plainText = [messageSend.message buildPlainTextData:recipient]; + if (!plainText) { + OWSRaiseException(InvalidMessageException, @"Failed to build message proto."); + } + OWSLogDebug(@"Built message: %@ plainTextData.length: %lu", [messageSend.message class], (unsigned long)plainText.length); + + NSString *recipientID = recipient.recipientId; + + OWSLogVerbose(@"Building device messages for: %@ %@ (isLocalNumber: %d, isUDSend: %d).", + recipientID, + recipient.devices, + messageSend.isLocalNumber, + messageSend.isUDSend); + + @try { + __block BOOL isSessionRequired; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + isSessionRequired = [LKSessionManagementProtocol isSessionRequiredForMessage:messageSend.message recipientID:recipientID transaction:transaction]; + }]; + if (isSessionRequired) { + BOOL hasSession = [self throws_ensureRecipientHasSessionForMessageSend:messageSend recipientID:recipientID deviceId:@(OWSDevicePrimaryDeviceId)]; + + // Loki: Remove this when shared sender keys has been widely rolled out + // ======== + if (!hasSession && [LKSessionManagementProtocol shouldIgnoreMissingPreKeyBundleExceptionForMessage:messageSend.message to:recipientID]) { + return @[ [NSDictionary new] ]; + } + // ======== + } + + __block NSDictionary *_Nullable messageDict; + __block NSException *encryptionException; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + @try { + messageDict = [self throws_encryptedMessageForMessageSend:messageSend + recipientID:recipientID + plainText:plainText + transaction:transaction]; + } @catch (NSException *exception) { + encryptionException = exception; + } + }]; + + if (encryptionException) { + OWSLogInfo(@"Exception during encryption: %@.", encryptionException); + @throw encryptionException; + } + + if (messageDict) { + [messagesArray addObject:messageDict]; + } else { + OWSRaiseException(InvalidMessageException, @"Failed to encrypt message."); + } + } @catch (NSException *exception) { + if ([exception.name isEqualToString:OWSMessageSenderInvalidDeviceException]) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [recipient updateRegisteredRecipientWithDevicesToAdd:nil + devicesToRemove:@[ @(OWSDevicePrimaryDeviceId) ] + transaction:transaction]; + }]; + } else { + @throw exception; + } + } + + return [messagesArray copy]; +} + +- (BOOL)throws_ensureRecipientHasSessionForMessageSend:(OWSMessageSend *)messageSend recipientID:(NSString *)recipientID deviceId:(NSNumber *)deviceId +{ + OWSAssertDebug(messageSend); + OWSAssertDebug(deviceId); + + OWSPrimaryStorage *storage = self.primaryStorage; + SignalRecipient *recipient = messageSend.recipient; + OWSAssertDebug(recipientID.length > 0); + + // Discard "typing indicator" messages if there is no existing session with the user. + BOOL canSafelyBeDiscarded = messageSend.message.isOnline; + if (canSafelyBeDiscarded) { + OWSRaiseException(NoSessionForTransientMessageException, @"No session for transient message."); + } + + PreKeyBundle *_Nullable bundle = [storage getPreKeyBundleForContact:recipientID]; + __block NSException *exception; + + if (!bundle) { + __block BOOL hasSession; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + hasSession = [storage containsSession:recipientID deviceId:[deviceId intValue] protocolContext:transaction]; + }]; + if (hasSession) { return YES; } + + TSOutgoingMessage *message = messageSend.message; + // Loki: Remove this when we have shared sender keys + // ======== + if ([LKSessionManagementProtocol shouldIgnoreMissingPreKeyBundleExceptionForMessage:message to:recipientID]) { return NO; } + // ======== + NSString *missingPrekeyBundleException = @"missingPrekeyBundleException"; + OWSRaiseException(missingPrekeyBundleException, @"Missing pre key bundle for: %@.", recipientID); + } else { + SessionBuilder *builder = [[SessionBuilder alloc] initWithSessionStore:storage + preKeyStore:storage + signedPreKeyStore:storage + identityKeyStore:self.identityManager + recipientId:recipientID + deviceId:[deviceId intValue]]; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + @try { + [builder throws_processPrekeyBundle:bundle protocolContext:transaction]; + + // Loki: Discard the pre key bundle as the session has now been established + [storage removePreKeyBundleForContact:recipientID transaction:transaction]; + } @catch (NSException *caughtException) { + exception = caughtException; + } + }]; + if (exception) { + if ([exception.name isEqualToString:UntrustedIdentityKeyException]) { + OWSRaiseExceptionWithUserInfo(UntrustedIdentityKeyException, (@{ TSInvalidPreKeyBundleKey : bundle, TSInvalidRecipientKey : recipientID }), @""); + } + @throw exception; + } + return YES; + } +} + +- (nullable NSDictionary *)throws_encryptedMessageForMessageSend:(OWSMessageSend *)messageSend + recipientID:(NSString *)recipientID + plainText:(NSData *)plainText + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(messageSend); + OWSAssertDebug(recipientID); + OWSAssertDebug(plainText); + OWSAssertDebug(transaction); + + OWSPrimaryStorage *storage = self.primaryStorage; + TSOutgoingMessage *message = messageSend.message; + + SessionCipher *cipher = [[SessionCipher alloc] initWithSessionStore:storage + preKeyStore:storage + signedPreKeyStore:storage + identityKeyStore:self.identityManager + recipientId:recipientID + deviceId:@(OWSDevicePrimaryDeviceId).intValue]; + + NSData *_Nullable serializedMessage; + TSWhisperMessageType messageType; + if ([LKSharedSenderKeysImplementation.shared isClosedGroup:recipientID]) { + NSError *error; + serializedMessage = [LKClosedGroupUtilities encryptData:plainText.paddedMessageBody usingGroupPublicKey:recipientID transaction:transaction error:&error]; + + if (error != nil) { + OWSFailDebug(@"Couldn't encrypt message for SSK based closed group due to error: %@.", error); + return nil; + } + + messageType = TSClosedGroupCiphertextMessageType; + + messageSend.udAccess = nil; + } else if (messageSend.isUDSend) { + NSError *error; + SNSessionRestorationImplementation *sessionResetImplementation = [SNSessionRestorationImplementation new]; + + SMKSecretSessionCipher *_Nullable secretCipher = + [[SMKSecretSessionCipher alloc] initWithSessionResetImplementation:sessionResetImplementation + sessionStore:self.primaryStorage + preKeyStore:self.primaryStorage + signedPreKeyStore:self.primaryStorage + identityStore:self.identityManager + error:&error]; + if (error || !secretCipher) { + OWSRaiseException(@"SecretSessionCipherFailure", @"Can't create secret session cipher."); + } + + // Loki: The way this works is: + // • Alice sends a session request (i.e. a pre key bundle) to Bob using fallback encryption. + // • She may send any number of subsequent messages also encrypted using fallback encryption. + // • When Bob receives the session request, he sets up his Signal cipher session locally and sends back a null message, + // now encrypted using Signal encryption. + // • Alice receives this, sets up her Signal cipher session locally, and sends any subsequent messages + // using Signal encryption. + + BOOL shouldUseFallbackEncryption = [LKSessionManagementProtocol shouldUseFallbackEncryptionForMessage:message recipientID:recipientID transaction:transaction]; + + if (shouldUseFallbackEncryption) { + [LKLogger print:@"[Loki] Using fallback encryption"]; + } else { + [LKLogger print:@"[Loki] Using Signal Encryption"]; + } + + serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientPublicKey:recipientID + deviceID:@(OWSDevicePrimaryDeviceId).intValue + paddedPlaintext:plainText.paddedMessageBody + senderCertificate:messageSend.senderCertificate + protocolContext:transaction + useFallbackSessionCipher:shouldUseFallbackEncryption + error:&error]; + + SCKRaiseIfExceptionWrapperError(error); + if (serializedMessage == nil || error != nil) { + OWSFailDebug(@"Error while UD encrypting message: %@.", error); + return nil; + } + messageType = TSUnidentifiedSenderMessageType; + } else { + id encryptedMessage = + [cipher throws_encryptMessage:[plainText paddedMessageBody] protocolContext:transaction]; + serializedMessage = encryptedMessage.serialized; + messageType = [self messageTypeForCipherMessage:encryptedMessage]; + } + + BOOL isSilent = message.isSilent; + BOOL isOnline = message.isOnline; + + OWSMessageServiceParams *messageParams = + [[OWSMessageServiceParams alloc] initWithType:messageType + recipientId:recipientID + device:@(OWSDevicePrimaryDeviceId).intValue + content:serializedMessage + isSilent:isSilent + isOnline:isOnline + registrationId:[cipher throws_remoteRegistrationId:transaction] + ttl:message.ttl + isPing:NO]; + + NSError *error; + NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:messageParams error:&error]; + + if (error != nil) { + return nil; + } + + return jsonDict; +} + +- (TSWhisperMessageType)messageTypeForCipherMessage:(id)cipherMessage +{ + switch (cipherMessage.cipherMessageType) { + case CipherMessageType_Whisper: + return TSEncryptedWhisperMessageType; + case CipherMessageType_Prekey: + return TSPreKeyWhisperMessageType; + default: + return TSUnknownMessageType; + } +} + +- (void)saveInfoMessageForGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread +{ + OWSAssertDebug(message); + OWSAssertDebug(thread); + + if (message.groupMetaMessage == TSGroupMetaMessageDeliver) { + // TODO: Why is this necessary? + [message save]; + } else if (message.groupMetaMessage == TSGroupMetaMessageQuit) { + // MJK TODO - remove senderTimestamp + [[[TSInfoMessage alloc] initWithTimestamp:message.timestamp + inThread:thread + messageType:TSInfoMessageTypeGroupQuit + customMessage:message.customMessage] save]; + } else { + // MJK TODO - remove senderTimestamp + [[[TSInfoMessage alloc] initWithTimestamp:message.timestamp + inThread:thread + messageType:TSInfoMessageTypeGroupUpdate + customMessage:message.customMessage] save]; + } +} + +@end + +@implementation OutgoingMessagePreparer + +#pragma mark - Dependencies + ++ (YapDatabaseConnection *)dbConnection +{ + return SSKEnvironment.shared.primaryStorage.dbReadWriteConnection; +} + +#pragma mark - + ++ (NSArray *)prepareMessageForSending:(TSOutgoingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(message); + OWSAssertDebug(transaction); + + NSMutableArray *attachmentIds = [NSMutableArray new]; + + if (message.attachmentIds) { + [attachmentIds addObjectsFromArray:message.attachmentIds]; + } + + if (message.quotedMessage) { + // Though we currently only ever expect at most one thumbnail, the proto data model + // suggests this could change. The logic is intended to work with multiple, but + // if we ever actually want to send multiple, we should do more testing. + NSArray *quotedThumbnailAttachments = + [message.quotedMessage createThumbnailAttachmentsIfNecessaryWithTransaction:transaction]; + for (TSAttachmentStream *attachment in quotedThumbnailAttachments) { + [attachmentIds addObject:attachment.uniqueId]; + } + } + + if (message.contactShare.avatarAttachmentId != nil) { + TSAttachment *attachment = [message.contactShare avatarAttachmentWithTransaction:transaction]; + if ([attachment isKindOfClass:[TSAttachmentStream class]]) { + [attachmentIds addObject:attachment.uniqueId]; + } else { + OWSFailDebug(@"Unexpected avatarAttachment: %@.", attachment); + } + } + + if (message.linkPreview.imageAttachmentId != nil) { + TSAttachment *attachment = + [TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction]; + if ([attachment isKindOfClass:[TSAttachmentStream class]]) { + [attachmentIds addObject:attachment.uniqueId]; + } else { + OWSFailDebug(@"Unexpected attachment: %@.", attachment); + } + } + + // All outgoing messages should be saved at the time they are enqueued. + [message saveWithTransaction:transaction]; + // When we start a message send, all "failed" recipients should be marked as "sending". + [message updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:transaction]; + + return attachmentIds; +} + ++ (void)prepareAttachments:(NSArray *)attachmentInfos + inMessage:(TSOutgoingMessage *)outgoingMessage + completionHandler:(void (^)(NSError *_Nullable error))completionHandler +{ + OWSAssertDebug(attachmentInfos.count > 0); + OWSAssertDebug(outgoingMessage); + + dispatch_async([OWSDispatch attachmentsQueue], ^{ + NSMutableArray *attachmentStreams = [NSMutableArray new]; + for (OWSOutgoingAttachmentInfo *attachmentInfo in attachmentInfos) { + TSAttachmentStream *attachmentStream = + [[TSAttachmentStream alloc] initWithContentType:attachmentInfo.contentType + byteCount:(UInt32)attachmentInfo.dataSource.dataLength + sourceFilename:attachmentInfo.sourceFilename + caption:attachmentInfo.caption + albumMessageId:attachmentInfo.albumMessageId]; + + if (outgoingMessage.isVoiceMessage) { + attachmentStream.attachmentType = TSAttachmentTypeVoiceMessage; + } + + if (![attachmentStream writeDataSource:attachmentInfo.dataSource]) { + NSError *error = OWSErrorMakeWriteAttachmentDataError(); + completionHandler(error); + return; + } + + [attachmentStreams addObject:attachmentStream]; + } + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + for (TSAttachmentStream *attachmentStream in attachmentStreams) { + [outgoingMessage.attachmentIds addObject:attachmentStream.uniqueId]; + if (attachmentStream.sourceFilename) { + outgoingMessage.attachmentFilenameMap[attachmentStream.uniqueId] = attachmentStream.sourceFilename; + } + } + [outgoingMessage saveWithTransaction:transaction]; + for (TSAttachmentStream *attachmentStream in attachmentStreams) { + [attachmentStream saveWithTransaction:transaction]; + } + }]; + + completionHandler(nil); + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMessageServiceParams.h b/SignalUtilitiesKit/OWSMessageServiceParams.h new file mode 100644 index 000000000..83f00ce3f --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageServiceParams.h @@ -0,0 +1,45 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSConstants.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Contstructs the per-device-message parameters used when submitting a message to + * the Signal Web Service. + * + * See: + * https://github.com/signalapp/libsignal-service-java/blob/master/java/src/main/java/org/whispersystems/signalservice/internal/push/OutgoingPushMessage.java + */ +@interface OWSMessageServiceParams : MTLModel + +@property (nonatomic, readonly) int type; +@property (nonatomic, readonly) NSString *destination; +@property (nonatomic, readonly) int destinationDeviceId; +@property (nonatomic, readonly) int destinationRegistrationId; +@property (nonatomic, readonly) NSString *content; +@property (nonatomic, readonly) BOOL silent; +@property (nonatomic, readonly) BOOL online; + +// Loki: Message ttl +@property (nonatomic, readonly) uint ttl; + +// Loki: Wether this message is a p2p ping +@property (nonatomic, readonly) BOOL isPing; + +- (instancetype)initWithType:(TSWhisperMessageType)type + recipientId:(NSString *)destination + device:(int)deviceId + content:(NSData *)content + isSilent:(BOOL)isSilent + isOnline:(BOOL)isOnline + registrationId:(int)registrationId + ttl:(uint)ttl + isPing:(BOOL)isPing; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMessageServiceParams.m b/SignalUtilitiesKit/OWSMessageServiceParams.m new file mode 100644 index 000000000..6719a6551 --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageServiceParams.m @@ -0,0 +1,49 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageServiceParams.h" +#import "TSConstants.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSMessageServiceParams + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return [NSDictionary mtl_identityPropertyMapWithModel:[self class]]; +} + +- (instancetype)initWithType:(TSWhisperMessageType)type + recipientId:(NSString *)destination + device:(int)deviceId + content:(NSData *)content + isSilent:(BOOL)isSilent + isOnline:(BOOL)isOnline + registrationId:(int)registrationId + ttl:(uint)ttl + isPing:(BOOL)isPing +{ + self = [super init]; + + if (!self) { + return self; + } + + _type = type; + _destination = destination; + _destinationDeviceId = deviceId; + _destinationRegistrationId = registrationId; + _content = [content base64EncodedString]; + _silent = isSilent; + _online = isOnline; + _ttl = ttl; + _isPing = isPing; + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMessageUtils.h b/SignalUtilitiesKit/OWSMessageUtils.h new file mode 100644 index 000000000..ef693b8dc --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageUtils.h @@ -0,0 +1,25 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSMessage; +@class TSThread; +@class YapDatabaseReadTransaction; + +@interface OWSMessageUtils : NSObject + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)sharedManager; + +- (NSUInteger)unreadMessagesCount; +- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread; + +- (void)updateApplicationBadgeCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSMessageUtils.m b/SignalUtilitiesKit/OWSMessageUtils.m new file mode 100644 index 000000000..c95c0ad1f --- /dev/null +++ b/SignalUtilitiesKit/OWSMessageUtils.m @@ -0,0 +1,125 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageUtils.h" +#import "AppContext.h" +#import "MIMETypeUtil.h" +#import "OWSMessageSender.h" +#import "OWSPrimaryStorage.h" +#import "TSAccountManager.h" +#import "TSAttachment.h" +#import "TSAttachmentStream.h" +#import "TSDatabaseView.h" +#import "TSIncomingMessage.h" +#import "TSMessage.h" +#import "TSOutgoingMessage.h" +#import "TSQuotedMessage.h" +#import "TSThread.h" +#import "UIImage+OWS.h" +#import +#import "SSKAsserts.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSMessageUtils () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@end + +#pragma mark - + +@implementation OWSMessageUtils + ++ (instancetype)sharedManager +{ + static OWSMessageUtils *sharedMyManager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedMyManager = [[self alloc] initDefault]; + }); + return sharedMyManager; +} + +- (instancetype)initDefault +{ + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + + return [self initWithPrimaryStorage:primaryStorage]; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + + if (!self) { + return self; + } + + _dbConnection = primaryStorage.newDatabaseConnection; + + OWSSingletonAssert(); + + return self; +} + +- (NSUInteger)unreadMessagesCount +{ + __block NSUInteger count = 0; + + [LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) { + YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName]; + NSArray *allGroups = [unreadMessages allGroups]; + for (NSString *groupID in allGroups) { + [unreadMessages enumerateKeysAndObjectsInGroup:groupID + usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) { + if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { + return; + } + id unread = (id)object; + if (unread.read) { + [LKLogger print:@"Found an already read message in the * unread * messages list."]; + return; + } + count += 1; + }]; + } + }]; + + return count; + + __block NSUInteger numberOfItems; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + numberOfItems = [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInAllGroups]; + }]; + + return numberOfItems; +} + +- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread +{ + __block NSUInteger numberOfItems; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + id databaseView = [transaction ext:TSUnreadDatabaseViewExtensionName]; + OWSAssertDebug(databaseView); + numberOfItems = ([databaseView numberOfItemsInAllGroups] - [databaseView numberOfItemsInGroup:thread.uniqueId]); + }]; + + return numberOfItems; +} + +- (void)updateApplicationBadgeCount +{ + if (!CurrentAppContext().isMainApp) { + return; + } + + NSUInteger numberOfItems = [self unreadMessagesCount]; + [CurrentAppContext() setMainAppBadgeNumber:numberOfItems]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOperation.h b/SignalUtilitiesKit/OWSOperation.h new file mode 100644 index 000000000..ebeeaa53f --- /dev/null +++ b/SignalUtilitiesKit/OWSOperation.h @@ -0,0 +1,88 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, OWSOperationState) { + OWSOperationStateNew, + OWSOperationStateExecuting, + OWSOperationStateFinished +}; + +// A base class for implementing retryable operations. +// To utilize the retryable behavior: +// Set remainingRetries to something greater than 0, and when you're reporting an error, +// set `error.isRetryable = YES`. +// If the failure is one that will not succeed upon retry, set `error.isFatal = YES`. +// +// isRetryable and isFatal are opposites but not redundant. +// +// If a group message send fails, the send will be retried if any of the errors were retryable UNLESS +// any of the errors were fatal. Fatal errors trump retryable errors. +@interface OWSOperation : NSOperation + +@property (readonly, nullable) NSError *failingError; + +// Defaults to 0, set to greater than 0 in init if you'd like the operation to be retryable. +@property NSUInteger remainingRetries; + +#pragma mark - Mandatory Subclass Overrides + +// Called every retry, this is where the bulk of the operation's work should go. +- (void)run; + +#pragma mark - Optional Subclass Overrides + +// Called one time only +- (nullable NSError *)checkForPreconditionError; + +// Called at most one time. +- (void)didSucceed; + +// Called at most one time. +- (void)didCancel; + +// Called zero or more times, retry may be possible +- (void)didReportError:(NSError *)error; + +// Called at most one time, once retry is no longer possible. +- (void)didFailWithError:(NSError *)error NS_SWIFT_NAME(didFail(error:)); + +// How long to wait before retry, if possible +- (NSTimeInterval)retryInterval; + +#pragma mark - Success/Error - Do Not Override + +// Runs now if a retry timer has been set by a previous failure, +// otherwise assumes we're currently running and does nothing. +- (void)runAnyQueuedRetry; + +// Report that the operation completed successfully. +// +// Each invocation of `run` must make exactly one call to one of: `reportSuccess`, `reportCancelled`, or `reportError:` +- (void)reportSuccess; + +// Call this when you abort before completion due to being cancelled. +// +// Each invocation of `run` must make exactly one call to one of: `reportSuccess`, `reportCancelled`, or `reportError:` +- (void)reportCancelled; + +// Report that the operation failed to complete due to an error. +// +// Each invocation of `run` must make exactly one call to one of: `reportSuccess`, `reportCancelled`, or `reportError:` +// You must ensure that `run` cannot succeed after calling `reportError`, e.g. generally you'll write something like +// this: +// +// [self reportError:someError]; +// return; +// +// If the error is terminal, and you want to avoid retry, report an error with `error.isFatal = YES` otherwise the +// operation will retry if possible. +- (void)reportError:(NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOperation.m b/SignalUtilitiesKit/OWSOperation.m new file mode 100644 index 000000000..6bdfc712c --- /dev/null +++ b/SignalUtilitiesKit/OWSOperation.m @@ -0,0 +1,269 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOperation.h" +#import "NSError+MessageSending.h" +#import "NSTimer+OWS.h" +#import "OWSBackgroundTask.h" +#import "OWSError.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSOperationKeyIsExecuting = @"isExecuting"; +NSString *const OWSOperationKeyIsFinished = @"isFinished"; + +@interface OWSOperation () + +@property (nullable) NSError *failingError; +@property (atomic) OWSOperationState operationState; +@property (nonatomic) OWSBackgroundTask *backgroundTask; + +// This property should only be accessed on the main queue. +@property (nonatomic) NSTimer *_Nullable retryTimer; + +@end + +@implementation OWSOperation + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + _operationState = OWSOperationStateNew; + _backgroundTask = [OWSBackgroundTask backgroundTaskWithLabel:self.logTag]; + + // Operations are not retryable by default. + _remainingRetries = 0; + + return self; +} + +- (void)dealloc +{ + OWSLogDebug(@"in dealloc"); +} + +#pragma mark - Subclass Overrides + +// Called one time only +- (nullable NSError *)checkForPreconditionError +{ + // OWSOperation have a notion of failure, which is inferred by the presence of a `failingError`. + // + // By default, any failing dependency cascades that failure to it's dependent. + // If you'd like different behavior, override this method (`checkForPreconditionError`) without calling `super`. + for (NSOperation *dependency in self.dependencies) { + if (![dependency isKindOfClass:[OWSOperation class]]) { + // Native operations, like NSOperation and NSBlockOperation have no notion of "failure". + // So there's no `failingError` to cascade. + continue; + } + + OWSOperation *dependentOperation = (OWSOperation *)dependency; + + // Don't proceed if dependency failed - surface the dependency's error. + NSError *_Nullable dependencyError = dependentOperation.failingError; + if (dependencyError != nil) { + return dependencyError; + } + } + + return nil; +} + +// Called every retry, this is where the bulk of the operation's work should go. +- (void)run +{ + OWSAbstractMethod(); +} + +// Called at most one time. +- (void)didSucceed +{ + // no-op + // Override in subclass if necessary +} + +// Called at most one time. +- (void)didCancel +{ + // no-op + // Override in subclass if necessary +} + +// Called zero or more times, retry may be possible +- (void)didReportError:(NSError *)error +{ + // no-op + // Override in subclass if necessary +} + +// Called at most one time, once retry is no longer possible. +- (void)didFailWithError:(NSError *)error +{ + // no-op + // Override in subclass if necessary +} + +#pragma mark - NSOperation overrides + +// Do not override this method in a subclass instead, override `run` +- (void)main +{ + OWSLogDebug(@"started."); + NSError *_Nullable preconditionError = [self checkForPreconditionError]; + if (preconditionError) { + [self failOperationWithError:preconditionError]; + return; + } + + if (self.isCancelled) { + [self reportCancelled]; + return; + } + + [self run]; +} + +- (void)runAnyQueuedRetry +{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSTimer *_Nullable retryTimer = self.retryTimer; + self.retryTimer = nil; + [retryTimer invalidate]; + + if (retryTimer != nil) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self run]; + }); + } else { + OWSLogVerbose(@"not re-running since operation is already running."); + } + }); +} + +#pragma mark - Public Methods + +// These methods are not intended to be subclassed +- (void)reportSuccess +{ + OWSLogDebug(@"succeeded."); + [self didSucceed]; + [self markAsComplete]; +} + +// These methods are not intended to be subclassed +- (void)reportCancelled +{ + OWSLogDebug(@"cancelled."); + [self didCancel]; + [self markAsComplete]; +} + +- (void)reportError:(NSError *)error +{ + OWSLogDebug(@"reportError: %@, fatal?: %d, retryable?: %d, remainingRetries: %lu", + error, + error.isFatal, + error.isRetryable, + (unsigned long)self.remainingRetries); + + [self didReportError:error]; + + if (error.isFatal) { + [self failOperationWithError:error]; + return; + } + + if (!error.isRetryable) { + [self failOperationWithError:error]; + return; + } + + if (self.remainingRetries == 0) { + [self failOperationWithError:error]; + return; + } + + self.remainingRetries--; + + dispatch_async(dispatch_get_main_queue(), ^{ + OWSAssertDebug(self.retryTimer == nil); + [self.retryTimer invalidate]; + + // The `scheduledTimerWith*` methods add the timer to the current thread's RunLoop. + // Since Operations typically run on a background thread, that would mean the background + // thread's RunLoop. However, the OS can spin down background threads if there's no work + // being done, so we run the risk of the timer's RunLoop being deallocated before it's + // fired. + // + // To ensure the timer's thread sticks around, we schedule it while on the main RunLoop. + self.retryTimer = [NSTimer weakScheduledTimerWithTimeInterval:self.retryInterval + target:self + selector:@selector(runAnyQueuedRetry) + userInfo:nil + repeats:NO]; + }); +} + +// Override in subclass if you want something more sophisticated, e.g. exponential backoff +- (NSTimeInterval)retryInterval +{ + return 0.1; +} + +#pragma mark - Life Cycle + +- (void)failOperationWithError:(NSError *)error +{ + OWSLogDebug(@"failed terminally."); + self.failingError = error; + + [self didFailWithError:error]; + [self markAsComplete]; +} + +- (BOOL)isExecuting +{ + return self.operationState == OWSOperationStateExecuting; +} + +- (BOOL)isFinished +{ + return self.operationState == OWSOperationStateFinished; +} + +- (void)start +{ + [self willChangeValueForKey:OWSOperationKeyIsExecuting]; + self.operationState = OWSOperationStateExecuting; + [self didChangeValueForKey:OWSOperationKeyIsExecuting]; + + [self main]; +} + +- (void)markAsComplete +{ + [self willChangeValueForKey:OWSOperationKeyIsExecuting]; + [self willChangeValueForKey:OWSOperationKeyIsFinished]; + + // Ensure we call the success or failure handler exactly once. + @synchronized(self) + { + OWSAssertDebug(self.operationState != OWSOperationStateFinished); + + self.operationState = OWSOperationStateFinished; + } + + [self didChangeValueForKey:OWSOperationKeyIsExecuting]; + [self didChangeValueForKey:OWSOperationKeyIsFinished]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingCallMessage.h b/SignalUtilitiesKit/OWSOutgoingCallMessage.h new file mode 100644 index 000000000..490f5632e --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingCallMessage.h @@ -0,0 +1,48 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoCallMessageAnswer; +@class SSKProtoCallMessageBusy; +@class SSKProtoCallMessageHangup; +@class SSKProtoCallMessageIceUpdate; +@class SSKProtoCallMessageOffer; +@class TSThread; + +/** + * WebRTC call signaling sent out of band, via the Signal Service + */ +@interface OWSOutgoingCallMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithThread:(TSThread *)thread offerMessage:(SSKProtoCallMessageOffer *)offerMessage; +- (instancetype)initWithThread:(TSThread *)thread answerMessage:(SSKProtoCallMessageAnswer *)answerMessage; +- (instancetype)initWithThread:(TSThread *)thread + iceUpdateMessages:(NSArray *)iceUpdateMessage; +- (instancetype)initWithThread:(TSThread *)thread hangupMessage:(SSKProtoCallMessageHangup *)hangupMessage; +- (instancetype)initWithThread:(TSThread *)thread busyMessage:(SSKProtoCallMessageBusy *)busyMessage; + +@property (nullable, nonatomic, readonly) SSKProtoCallMessageOffer *offerMessage; +@property (nullable, nonatomic, readonly) SSKProtoCallMessageAnswer *answerMessage; +@property (nullable, nonatomic, readonly) NSArray *iceUpdateMessages; +@property (nullable, nonatomic, readonly) SSKProtoCallMessageHangup *hangupMessage; +@property (nullable, nonatomic, readonly) SSKProtoCallMessageBusy *busyMessage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingCallMessage.m b/SignalUtilitiesKit/OWSOutgoingCallMessage.m new file mode 100644 index 000000000..a7a2f6478 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingCallMessage.m @@ -0,0 +1,196 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingCallMessage.h" +#import "ProtoUtils.h" +#import "SignalRecipient.h" +#import "TSContactThread.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSOutgoingCallMessage + +- (instancetype)initWithThread:(TSThread *)thread +{ + // MJK TODO - safe to remove senderTimestamp + self = [super initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + if (!self) { + return self; + } + + return self; +} + +- (instancetype)initWithThread:(TSThread *)thread offerMessage:(SSKProtoCallMessageOffer *)offerMessage +{ + self = [self initWithThread:thread]; + if (!self) { + return self; + } + + _offerMessage = offerMessage; + + return self; +} + +- (instancetype)initWithThread:(TSThread *)thread answerMessage:(SSKProtoCallMessageAnswer *)answerMessage +{ + self = [self initWithThread:thread]; + if (!self) { + return self; + } + + _answerMessage = answerMessage; + + return self; +} + +- (instancetype)initWithThread:(TSThread *)thread + iceUpdateMessages:(NSArray *)iceUpdateMessages +{ + self = [self initWithThread:thread]; + if (!self) { + return self; + } + + _iceUpdateMessages = iceUpdateMessages; + + return self; +} + +- (instancetype)initWithThread:(TSThread *)thread hangupMessage:(SSKProtoCallMessageHangup *)hangupMessage +{ + self = [self initWithThread:thread]; + if (!self) { + return self; + } + + _hangupMessage = hangupMessage; + + return self; +} + +- (instancetype)initWithThread:(TSThread *)thread busyMessage:(SSKProtoCallMessageBusy *)busyMessage +{ + self = [self initWithThread:thread]; + if (!self) { + return self; + } + + _busyMessage = busyMessage; + + return self; +} + +#pragma mark - TSOutgoingMessage overrides + +- (BOOL)shouldSyncTranscript +{ + return NO; +} + +- (BOOL)isSilent +{ + // Avoid "phantom messages" for "outgoing call messages". + + return YES; +} + +- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient +{ + OWSAssertDebug(recipient); + + SSKProtoContentBuilder *builder = [SSKProtoContent builder]; + [builder setCallMessage:[self buildCallMessage:recipient.recipientId]]; + + NSError *error; + NSData *_Nullable data = [builder buildSerializedDataAndReturnError:&error]; + if (error || !data) { + OWSFailDebug(@"could not serialize protobuf: %@", error); + return nil; + } + return data; +} + +- (nullable SSKProtoCallMessage *)buildCallMessage:(NSString *)recipientId +{ + SSKProtoCallMessageBuilder *builder = [SSKProtoCallMessage builder]; + + if (self.offerMessage) { + [builder setOffer:self.offerMessage]; + } + + if (self.answerMessage) { + [builder setAnswer:self.answerMessage]; + } + + if (self.iceUpdateMessages.count > 0) { + [builder setIceUpdate:self.iceUpdateMessages]; + } + + if (self.hangupMessage) { + [builder setHangup:self.hangupMessage]; + } + + if (self.busyMessage) { + [builder setBusy:self.busyMessage]; + } + + [ProtoUtils addLocalProfileKeyIfNecessary:self.thread recipientId:recipientId callMessageBuilder:builder]; + + NSError *error; + SSKProtoCallMessage *_Nullable result = [builder buildAndReturnError:&error]; + if (error || !result) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + return result; +} + +#pragma mark - TSYapDatabaseObject overrides + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeCall]; } + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (NSString *)debugDescription +{ + NSString *className = NSStringFromClass([self class]); + + NSString *payload; + if (self.offerMessage) { + payload = @"offerMessage"; + } else if (self.answerMessage) { + payload = @"answerMessage"; + } else if (self.iceUpdateMessages.count > 0) { + payload = [NSString stringWithFormat:@"iceUpdateMessages: %lu", (unsigned long)self.iceUpdateMessages.count]; + } else if (self.hangupMessage) { + payload = @"hangupMessage"; + } else if (self.busyMessage) { + payload = @"busyMessage"; + } else { + payload = @"none"; + } + + return [NSString stringWithFormat:@"%@ with payload: %@", className, payload]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingNullMessage.h b/SignalUtilitiesKit/OWSOutgoingNullMessage.h new file mode 100644 index 000000000..eb8b1c806 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingNullMessage.h @@ -0,0 +1,31 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSVerificationStateSyncMessage; +@class TSContactThread; + +@interface OWSOutgoingNullMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview; + +- (instancetype)initWithContactThread:(TSContactThread *)contactThread + verificationStateSyncMessage:(OWSVerificationStateSyncMessage *)verificationStateSyncMessage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingNullMessage.m b/SignalUtilitiesKit/OWSOutgoingNullMessage.m new file mode 100644 index 000000000..876757b17 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingNullMessage.m @@ -0,0 +1,107 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingNullMessage.h" +#import "OWSVerificationStateSyncMessage.h" +#import "TSContactThread.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSOutgoingNullMessage () + +@property (nonatomic, readonly) OWSVerificationStateSyncMessage *verificationStateSyncMessage; + +@end + +#pragma mark - + +@implementation OWSOutgoingNullMessage + +- (instancetype)initWithContactThread:(TSContactThread *)contactThread + verificationStateSyncMessage:(OWSVerificationStateSyncMessage *)verificationStateSyncMessage +{ + // MJK TODO - remove senderTimestamp + self = [super initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:contactThread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + if (!self) { + return self; + } + + _verificationStateSyncMessage = verificationStateSyncMessage; + + return self; +} + +#pragma mark - override TSOutgoingMessage + +- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient +{ + SSKProtoNullMessageBuilder *nullMessageBuilder = [SSKProtoNullMessage builder]; + + NSUInteger contentLength; + if (self.verificationStateSyncMessage != nil) { + contentLength = self.verificationStateSyncMessage.unpaddedVerifiedLength; + + OWSAssertDebug(self.verificationStateSyncMessage.paddingBytesLength > 0); + + // We add the same amount of padding in the VerificationStateSync message and it's coresponding NullMessage so that + // the sync message is indistinguishable from an outgoing Sent transcript corresponding to the NullMessage. We pad + // the NullMessage so as to obscure it's content. The sync message (like all sync messages) will be *additionally* + // padded by the superclass while being sent. The end result is we send a NullMessage of a non-distinct size, and a + // verification sync which is ~1-512 bytes larger then that. + contentLength += self.verificationStateSyncMessage.paddingBytesLength; + } else { + contentLength = arc4random_uniform(512); + } + + OWSAssertDebug(contentLength > 0); + + nullMessageBuilder.padding = [Cryptography generateRandomBytes:contentLength]; + + NSError *error; + SSKProtoNullMessage *_Nullable nullMessage = [nullMessageBuilder buildAndReturnError:&error]; + if (error != nil || nullMessage == nil) { + OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error); + return nil; + } + + SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder]; + contentBuilder.nullMessage = nullMessage; + + NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error]; + if (error != nil || contentData == nil) { + OWSFailDebug(@"Couldn't serialize protobuf due to error: %@.", error); + return nil; + } + + return contentData; +} + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeEphemeral]; } + +- (BOOL)shouldSyncTranscript +{ + return NO; +} + +- (BOOL)shouldBeSaved +{ + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingReceiptManager.h b/SignalUtilitiesKit/OWSOutgoingReceiptManager.h new file mode 100644 index 000000000..fe5be23e2 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingReceiptManager.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class SSKProtoEnvelope; + +@interface OWSOutgoingReceiptManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; ++ (instancetype)sharedManager; + +- (void)enqueueDeliveryReceiptForEnvelope:(SSKProtoEnvelope *)envelope; + +- (void)enqueueReadReceiptForEnvelope:(NSString *)messageAuthorId timestamp:(uint64_t)timestamp; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingReceiptManager.m b/SignalUtilitiesKit/OWSOutgoingReceiptManager.m new file mode 100644 index 000000000..64376c7a9 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingReceiptManager.m @@ -0,0 +1,313 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingReceiptManager.h" +#import "AppReadiness.h" +#import "OWSError.h" +#import "OWSMessageSender.h" +#import "OWSPrimaryStorage.h" +#import "OWSReceiptsForSenderMessage.h" +#import "SSKEnvironment.h" +#import "TSContactThread.h" +#import "TSYapDatabaseObject.h" +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, OWSReceiptType) { + OWSReceiptType_Delivery, + OWSReceiptType_Read, +}; + +NSString *const kOutgoingDeliveryReceiptManagerCollection = @"kOutgoingDeliveryReceiptManagerCollection"; +NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptManagerCollection"; + +@interface OWSOutgoingReceiptManager () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@property (nonatomic) Reachability *reachability; + +// This property should only be accessed on the serialQueue. +@property (nonatomic) BOOL isProcessing; + +@end + +#pragma mark - + +@implementation OWSOutgoingReceiptManager + ++ (instancetype)sharedManager +{ + OWSAssert(SSKEnvironment.shared.outgoingReceiptManager); + + return SSKEnvironment.shared.outgoingReceiptManager; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + + if (!self) { + return self; + } + + self.reachability = [Reachability reachabilityForInternetConnection]; + + _dbConnection = primaryStorage.newDatabaseConnection; + + OWSSingletonAssert(); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reachabilityChanged) + name:kReachabilityChangedNotification + object:nil]; + + // Start processing. + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [self process]; + }]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Dependencies + +- (OWSMessageSender *)messageSender +{ + OWSAssertDebug(SSKEnvironment.shared.messageSender); + + return SSKEnvironment.shared.messageSender; +} + +#pragma mark - + +- (dispatch_queue_t)serialQueue +{ + static dispatch_queue_t _serialQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _serialQueue = dispatch_queue_create("org.whispersystems.outgoingReceipts", DISPATCH_QUEUE_SERIAL); + }); + + return _serialQueue; +} + +// Schedules a processing pass, unless one is already scheduled. +- (void)process { + OWSAssertDebug(AppReadiness.isAppReady); + + dispatch_async(self.serialQueue, ^{ + if (self.isProcessing) { + return; + } + + OWSLogVerbose(@"Processing outbound receipts."); + + self.isProcessing = YES; + + if (!self.reachability.isReachable) { + // No network availability; abort. + self.isProcessing = NO; + return; + } + + NSMutableArray *sendPromises = [NSMutableArray array]; + [sendPromises addObjectsFromArray:[self sendReceiptsForReceiptType:OWSReceiptType_Delivery]]; + [sendPromises addObjectsFromArray:[self sendReceiptsForReceiptType:OWSReceiptType_Read]]; + + if (sendPromises.count < 1) { + // No work to do; abort. + self.isProcessing = NO; + return; + } + + AnyPromise *completionPromise = PMKJoin(sendPromises); + completionPromise.ensure(^() { + // Wait N seconds before conducting another pass. + // This allows time for a batch to accumulate. + // + // We want a value high enough to allow us to effectively de-duplicate + // receipts without being so high that we incur so much latency that + // the user notices. + const CGFloat kProcessingFrequencySeconds = 3.f; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kProcessingFrequencySeconds * NSEC_PER_SEC)), + self.serialQueue, + ^{ + self.isProcessing = NO; + + [self process]; + }); + }); + [completionPromise retainUntilComplete]; + }); +} + +- (NSArray *)sendReceiptsForReceiptType:(OWSReceiptType)receiptType { + NSString *collection = [self collectionForReceiptType:receiptType]; + + NSMutableDictionary *> *queuedReceiptMap = [NSMutableDictionary new]; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [transaction enumerateKeysAndObjectsInCollection:collection + usingBlock:^(NSString *key, id object, BOOL *stop) { + NSString *recipientId = key; + NSSet *timestamps = object; + queuedReceiptMap[recipientId] = [timestamps copy]; + }]; + }]; + + NSMutableArray *sendPromises = [NSMutableArray array]; + + for (NSString *recipientId in queuedReceiptMap) { + NSSet *timestamps = queuedReceiptMap[recipientId]; + if (timestamps.count < 1) { + OWSFailDebug(@"Missing timestamps."); + continue; + } + + TSThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId]; + + if (![LKSessionMetaProtocol shouldSendReceiptInThread:thread]) { + continue; + } + + OWSReceiptsForSenderMessage *message; + NSString *receiptName; + switch (receiptType) { + case OWSReceiptType_Delivery: + message = + [OWSReceiptsForSenderMessage deliveryReceiptsForSenderMessageWithThread:thread + messageTimestamps:timestamps.allObjects]; + receiptName = @"Delivery"; + break; + case OWSReceiptType_Read: + message = [OWSReceiptsForSenderMessage readReceiptsForSenderMessageWithThread:thread + messageTimestamps:timestamps.allObjects]; + receiptName = @"Read"; + break; + } + + AnyPromise *sendPromise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [self.messageSender sendMessage:message + success:^{ + OWSLogInfo( + @"Successfully sent %lu %@ receipts to sender.", (unsigned long)timestamps.count, receiptName); + + // DURABLE CLEANUP - we could replace the custom durability logic in this class + // with a durable JobQueue. + [self dequeueReceiptsWithRecipientId:recipientId timestamps:timestamps receiptType:receiptType]; + + // The value doesn't matter, we just need any non-NSError value. + resolve(@(1)); + } + failure:^(NSError *error) { + OWSLogError(@"Failed to send %@ receipts to sender with error: %@", receiptName, error); + + if (error.domain == OWSSignalServiceKitErrorDomain + && error.code == OWSErrorCodeNoSuchSignalRecipient) { + [self dequeueReceiptsWithRecipientId:recipientId timestamps:timestamps receiptType:receiptType]; + } + + resolve(error); + }]; + }]; + [sendPromises addObject:sendPromise]; + } + + return [sendPromises copy]; +} + +- (void)enqueueDeliveryReceiptForEnvelope:(SSKProtoEnvelope *)envelope +{ + [self enqueueReceiptWithRecipientId:envelope.source + timestamp:envelope.timestamp + receiptType:OWSReceiptType_Delivery]; +} + +- (void)enqueueReadReceiptForEnvelope:(NSString *)messageAuthorId timestamp:(uint64_t)timestamp { + [self enqueueReceiptWithRecipientId:messageAuthorId timestamp:timestamp receiptType:OWSReceiptType_Read]; +} + +- (void)enqueueReceiptWithRecipientId:(NSString *)recipientId + timestamp:(uint64_t)timestamp + receiptType:(OWSReceiptType)receiptType { + NSString *collection = [self collectionForReceiptType:receiptType]; + + if (recipientId.length < 1) { + OWSFailDebug(@"Invalid recipient id."); + return; + } + if (timestamp < 1) { + OWSFailDebug(@"Invalid timestamp."); + return; + } + dispatch_async(self.serialQueue, ^{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + NSSet *_Nullable oldTimestamps = [transaction objectForKey:recipientId inCollection:collection]; + NSMutableSet *newTimestamps + = (oldTimestamps ? [oldTimestamps mutableCopy] : [NSMutableSet new]); + [newTimestamps addObject:@(timestamp)]; + + [transaction setObject:newTimestamps forKey:recipientId inCollection:collection]; + }]; + + [self process]; + }); +} + +- (void)dequeueReceiptsWithRecipientId:(NSString *)recipientId + timestamps:(NSSet *)timestamps + receiptType:(OWSReceiptType)receiptType { + NSString *collection = [self collectionForReceiptType:receiptType]; + + if (recipientId.length < 1) { + OWSFailDebug(@"Invalid recipient id."); + return; + } + if (timestamps.count < 1) { + OWSFailDebug(@"Invalid timestamps."); + return; + } + dispatch_async(self.serialQueue, ^{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + NSSet *_Nullable oldTimestamps = [transaction objectForKey:recipientId inCollection:collection]; + NSMutableSet *newTimestamps + = (oldTimestamps ? [oldTimestamps mutableCopy] : [NSMutableSet new]); + [newTimestamps minusSet:timestamps]; + + if (newTimestamps.count > 0) { + [transaction setObject:newTimestamps forKey:recipientId inCollection:collection]; + } else { + [transaction removeObjectForKey:recipientId inCollection:collection]; + } + }]; + }); +} + +- (void)reachabilityChanged +{ + OWSAssertIsOnMainThread(); + + [self process]; +} + +- (NSString *)collectionForReceiptType:(OWSReceiptType)receiptType { + switch (receiptType) { + case OWSReceiptType_Delivery: + return kOutgoingDeliveryReceiptManagerCollection; + case OWSReceiptType_Read: + return kOutgoingReadReceiptManagerCollection; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingSentMessageTranscript.h b/SignalUtilitiesKit/OWSOutgoingSentMessageTranscript.h new file mode 100644 index 000000000..ff47ee6e2 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingSentMessageTranscript.h @@ -0,0 +1,25 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSOutgoingMessage; + +/** + * Notifies your other registered devices (if you have any) that you've sent a message. + * This way the message you just sent can appear on all your devices. + */ +@interface OWSOutgoingSentMessageTranscript : OWSOutgoingSyncMessage + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithOutgoingMessage:(TSOutgoingMessage *)message + isRecipientUpdate:(BOOL)isRecipientUpdate NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingSentMessageTranscript.m b/SignalUtilitiesKit/OWSOutgoingSentMessageTranscript.m new file mode 100644 index 000000000..96b96b9c7 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingSentMessageTranscript.m @@ -0,0 +1,118 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSentMessageTranscript.h" +#import "TSOutgoingMessage.h" +#import "TSThread.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TSOutgoingMessage (OWSOutgoingSentMessageTranscript) + +/** + * Normally this is private, but we need to embed this + * data structure within our own. + * + * recipientId is nil when building "sent" sync messages for messages + * sent to groups. + */ +- (nullable SSKProtoDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId; + +@end + +#pragma mark - + +@interface OWSOutgoingSentMessageTranscript () + +@property (nonatomic, readonly) TSOutgoingMessage *message; + +// sentRecipientId is the recipient of message, for contact thread messages. +// It is used to identify the thread/conversation to desktop. +@property (nonatomic, readonly, nullable) NSString *sentRecipientId; + +@property (nonatomic, readonly) BOOL isRecipientUpdate; + +@end + +#pragma mark - + +@implementation OWSOutgoingSentMessageTranscript + +- (instancetype)initWithOutgoingMessage:(TSOutgoingMessage *)message isRecipientUpdate:(BOOL)isRecipientUpdate +{ + self = [super initWithTimestamp:message.timestamp]; + + if (!self) { + return self; + } + + _message = message; + // This will be nil for groups. + _sentRecipientId = message.thread.contactIdentifier; + _isRecipientUpdate = isRecipientUpdate; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + SSKProtoSyncMessageSentBuilder *sentBuilder = [SSKProtoSyncMessageSent builder]; + [sentBuilder setTimestamp:self.message.timestamp]; + [sentBuilder setDestination:self.sentRecipientId]; + [sentBuilder setIsRecipientUpdate:self.isRecipientUpdate]; + + SSKProtoDataMessage *_Nullable dataMessage = [self.message buildDataMessage:self.sentRecipientId]; + if (!dataMessage) { + OWSFailDebug(@"could not build protobuf."); + return nil; + } + [sentBuilder setMessage:dataMessage]; + [sentBuilder setExpirationStartTimestamp:self.message.timestamp]; + + for (NSString *recipientId in self.message.sentRecipientIds) { + TSOutgoingMessageRecipientState *_Nullable recipientState = + [self.message recipientStateForRecipientId:recipientId]; + if (!recipientState) { + continue; + } + if (recipientState.state != OWSOutgoingMessageRecipientStateSent) { + OWSFailDebug(@"unexpected recipient state for: %@", recipientId); + continue; + } + + NSError *error; + SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder *statusBuilder = + [SSKProtoSyncMessageSentUnidentifiedDeliveryStatus builder]; + [statusBuilder setDestination:recipientId]; + [statusBuilder setUnidentified:recipientState.wasSentByUD]; + SSKProtoSyncMessageSentUnidentifiedDeliveryStatus *_Nullable status = + [statusBuilder buildAndReturnError:&error]; + if (error || !status) { + OWSFailDebug(@"Couldn't build UD status proto: %@", error); + continue; + } + [sentBuilder addUnidentifiedStatus:status]; + } + + NSError *error; + SSKProtoSyncMessageSent *_Nullable sentProto = [sentBuilder buildAndReturnError:&error]; + if (error || !sentProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + + SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder]; + [syncMessageBuilder setSent:sentProto]; + return syncMessageBuilder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingSyncMessage.h b/SignalUtilitiesKit/OWSOutgoingSyncMessage.h new file mode 100644 index 000000000..926a5fa87 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingSyncMessage.h @@ -0,0 +1,35 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Abstract base class used for the family of sync messages which take care + * of keeping your multiple registered devices consistent. E.g. sharing contacts, sharing groups, + * notifiying your devices of sent messages, and "read" receipts. + */ +@interface OWSOutgoingSyncMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp; + +- (instancetype)init NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSOutgoingSyncMessage.m b/SignalUtilitiesKit/OWSOutgoingSyncMessage.m new file mode 100644 index 000000000..38a643e96 --- /dev/null +++ b/SignalUtilitiesKit/OWSOutgoingSyncMessage.m @@ -0,0 +1,125 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" +#import "ProtoUtils.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSOutgoingSyncMessage + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (instancetype)init +{ + // MJK TODO - remove SenderTimestamp + self = [super initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:nil + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + + if (!self) { + return self; + } + + return self; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp +{ + self = [super initOutgoingMessageWithTimestamp:timestamp + inThread:nil + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + + if (!self) { + return self; + } + + return self; +} + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeSync]; } + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (BOOL)shouldSyncTranscript +{ + return NO; +} + +// This method should not be overridden, since we want to add random padding to *every* sync message +- (nullable SSKProtoSyncMessage *)buildSyncMessage +{ + SSKProtoSyncMessageBuilder *_Nullable builder = [self syncMessageBuilder]; + if (!builder) { + return nil; + } + + // Add a random 1-512 bytes to obscure sync message type + size_t paddingBytesLength = arc4random_uniform(512) + 1; + builder.padding = [Cryptography generateRandomBytes:paddingBytesLength]; + + NSError *error; + SSKProtoSyncMessage *_Nullable proto = [builder buildAndReturnError:&error]; + if (error || !proto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + return proto; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + OWSAbstractMethod(); + + return [SSKProtoSyncMessage builder]; +} + +- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient +{ + SSKProtoSyncMessage *_Nullable syncMessage = [self buildSyncMessage]; + if (!syncMessage) { + return nil; + } + + SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder]; + [contentBuilder setSyncMessage:syncMessage]; + + NSError *error; + NSData *_Nullable data = [contentBuilder buildSerializedDataAndReturnError:&error]; + if (error || !data) { + OWSFailDebug(@"could not serialize protobuf: %@", error); + return nil; + } + + return data; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+Calling.h b/SignalUtilitiesKit/OWSPrimaryStorage+Calling.h new file mode 100644 index 000000000..10172082e --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+Calling.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSPrimaryStorage (Calling) + +// phoneNumber is an e164 formatted phone number. +// +// callKitId is expected to have CallKitCallManager.kAnonymousCallHandlePrefix. +- (void)setPhoneNumber:(NSString *)phoneNumber forCallKitId:(NSString *)callKitId; + +// returns an e164 formatted phone number or nil if no +// record can be found. +// +// callKitId is expected to have CallKitCallManager.kAnonymousCallHandlePrefix. +- (NSString *)phoneNumberForCallKitId:(NSString *)callKitId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+Calling.m b/SignalUtilitiesKit/OWSPrimaryStorage+Calling.m new file mode 100644 index 000000000..fd96acd7f --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+Calling.m @@ -0,0 +1,35 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage+Calling.h" +#import "YapDatabaseConnection+OWS.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSPrimaryStorageCallKitIdToPhoneNumberCollection = @"TSStorageManagerCallKitIdToPhoneNumberCollection"; + +@implementation OWSPrimaryStorage (Calling) + +- (void)setPhoneNumber:(NSString *)phoneNumber forCallKitId:(NSString *)callKitId +{ + OWSAssertDebug(phoneNumber.length > 0); + OWSAssertDebug(callKitId.length > 0); + + [self.dbReadWriteConnection setObject:phoneNumber + forKey:callKitId + inCollection:OWSPrimaryStorageCallKitIdToPhoneNumberCollection]; +} + +- (NSString *)phoneNumberForCallKitId:(NSString *)callKitId +{ + OWSAssertDebug(callKitId.length > 0); + + return + [self.dbReadConnection objectForKey:callKitId inCollection:OWSPrimaryStorageCallKitIdToPhoneNumberCollection]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+Loki.h b/SignalUtilitiesKit/OWSPrimaryStorage+Loki.h new file mode 100644 index 000000000..b8a1b5625 --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+Loki.h @@ -0,0 +1,50 @@ +#import "OWSPrimaryStorage.h" + +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSPrimaryStorage (Loki) + +# pragma mark - Pre Key Record Management + +- (BOOL)hasPreKeyRecordForContact:(NSString *)hexEncodedPublicKey; +- (PreKeyRecord *_Nullable)getPreKeyRecordForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction; +- (PreKeyRecord *)getOrCreatePreKeyRecordForContact:(NSString *)hexEncodedPublicKey; + +# pragma mark - Pre Key Bundle Management + +/** + * Generates a pre key bundle for the given contact. Doesn't store the pre key bundle (pre key bundles are supposed to be sent without ever being stored). + */ +- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)hexEncodedPublicKey; +- (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)hexEncodedPublicKey; +- (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)removePreKeyBundleForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction; + +# pragma mark - Last Message Hash + +/** + * Gets the last message hash and removes it if its `expiresAt` has already passed. + */ +- (NSString *_Nullable)getLastMessageHashForSnode:(NSString *)snode transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)setLastMessageHashForSnode:(NSString *)snode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setLastMessageHash(forSnode:hash:expiresAt:transaction:)); + +# pragma mark - Open Groups + +- (void)setIDForMessageWithServerID:(NSUInteger)serverID to:(NSString *)messageID in:(YapDatabaseReadWriteTransaction *)transaction; +- (NSString *_Nullable)getIDForMessageWithServerID:(NSUInteger)serverID in:(YapDatabaseReadTransaction *)transaction; +- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(updateMessageIDCollectionByPruningMessagesWithIDs(_:in:)); + +# pragma mark - Restoration from Seed + +- (void)setRestorationTime:(NSTimeInterval)time; +- (NSTimeInterval)getRestorationTime; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+Loki.m b/SignalUtilitiesKit/OWSPrimaryStorage+Loki.m new file mode 100644 index 000000000..0f2d09569 --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+Loki.m @@ -0,0 +1,194 @@ +#import "OWSPrimaryStorage+Loki.h" +#import "OWSPrimaryStorage+PreKeyStore.h" +#import "OWSPrimaryStorage+SignedPreKeyStore.h" +#import "OWSPrimaryStorage+keyFromIntLong.h" +#import "OWSDevice.h" +#import "OWSIdentityManager.h" +#import "NSDate+OWS.h" +#import "TSAccountManager.h" +#import "TSPreKeyManager.h" +#import "YapDatabaseConnection+OWS.h" +#import "YapDatabaseTransaction+OWS.h" +#import +#import "NSObject+Casting.h" +#import + +@implementation OWSPrimaryStorage (Loki) + +# pragma mark - Convenience + +- (OWSIdentityManager *)identityManager { + return OWSIdentityManager.sharedManager; +} + +- (TSAccountManager *)accountManager { + return TSAccountManager.sharedInstance; +} + +# pragma mark - Pre Key Record Management + +#define LKPreKeyContactCollection @"LKPreKeyContactCollection" +#define OWSPrimaryStoragePreKeyStoreCollection @"TSStorageManagerPreKeyStoreCollection" + +- (BOOL)hasPreKeyRecordForContact:(NSString *)hexEncodedPublicKey { + int preKeyId = [self.dbReadWriteConnection intForKey:hexEncodedPublicKey inCollection:LKPreKeyContactCollection]; + return preKeyId > 0; +} + +- (PreKeyRecord *_Nullable)getPreKeyRecordForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction { + OWSAssertDebug(hexEncodedPublicKey.length > 0); + int preKeyID = [transaction intForKey:hexEncodedPublicKey inCollection:LKPreKeyContactCollection]; + + if (preKeyID <= 0) { return nil; } + + // throws_loadPreKey doesn't allow us to pass transaction + // FIXME: This seems like it could be a pretty big issue? + return [transaction preKeyRecordForKey:[self keyFromInt:preKeyID] inCollection:OWSPrimaryStoragePreKeyStoreCollection]; +} + +- (PreKeyRecord *)getOrCreatePreKeyRecordForContact:(NSString *)hexEncodedPublicKey { + OWSAssertDebug(hexEncodedPublicKey.length > 0); + int preKeyID = [self.dbReadWriteConnection intForKey:hexEncodedPublicKey inCollection:LKPreKeyContactCollection]; + + // If we don't have an ID then generate and store a new one + if (preKeyID <= 0) { + return [self generateAndStorePreKeyRecordForContact:hexEncodedPublicKey]; + } + + // Load existing pre key record if possible; generate a new one otherwise + @try { + return [self throws_loadPreKey:preKeyID]; + } @catch (NSException *exception) { + return [self generateAndStorePreKeyRecordForContact:hexEncodedPublicKey]; + } +} + +- (PreKeyRecord *)generateAndStorePreKeyRecordForContact:(NSString *)hexEncodedPublicKey { + [LKLogger print:[NSString stringWithFormat:@"[Loki] Generating new pre key record for: %@.", hexEncodedPublicKey]]; + OWSAssertDebug(hexEncodedPublicKey.length > 0); + + NSArray *records = [self generatePreKeyRecords:1]; + OWSAssertDebug(records.count > 0); + [self storePreKeyRecords:records]; + + PreKeyRecord *record = records.firstObject; + [self.dbReadWriteConnection setInt:record.Id forKey:hexEncodedPublicKey inCollection:LKPreKeyContactCollection]; + + return record; +} + +# pragma mark - Pre Key Bundle Management + +#define LKPreKeyBundleCollection @"LKPreKeyBundleCollection" + +- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)hexEncodedPublicKey forceClean:(BOOL)forceClean { + // Refresh signed pre key if needed + [TSPreKeyManager checkPreKeys]; + + ECKeyPair *_Nullable keyPair = self.identityManager.identityKeyPair; + OWSAssertDebug(keyPair); + + // Refresh signed pre key if needed + if (self.currentSignedPreKey == nil || forceClean) { // TODO: Is the self.currentSignedPreKey == nil check needed? + SignedPreKeyRecord *signedPreKeyRecord = [self generateRandomSignedRecord]; + [signedPreKeyRecord markAsAcceptedByService]; + [self storeSignedPreKey:signedPreKeyRecord.Id signedPreKeyRecord:signedPreKeyRecord]; + [self setCurrentSignedPrekeyId:signedPreKeyRecord.Id]; + [LKLogger print:@"[Loki] Signed pre key refreshed successfully."]; + } + + SignedPreKeyRecord *_Nullable signedPreKey = self.currentSignedPreKey; + if (signedPreKey == nil) { + OWSFailDebug(@"Signed pre key is nil."); + } + + PreKeyRecord *preKey = [self getOrCreatePreKeyRecordForContact:hexEncodedPublicKey]; + uint32_t registrationID = [self.accountManager getOrGenerateRegistrationId]; + + PreKeyBundle *bundle = [[PreKeyBundle alloc] initWithRegistrationId:registrationID + deviceId:OWSDevicePrimaryDeviceId + preKeyId:preKey.Id + preKeyPublic:preKey.keyPair.publicKey.prependKeyType + signedPreKeyPublic:signedPreKey.keyPair.publicKey.prependKeyType + signedPreKeyId:signedPreKey.Id + signedPreKeySignature:signedPreKey.signature + identityKey:keyPair.publicKey.prependKeyType]; + return bundle; +} + +- (PreKeyBundle *)generatePreKeyBundleForContact:(NSString *)hexEncodedPublicKey { + NSInteger failureCount = 0; + BOOL forceClean = NO; + while (failureCount < 3) { + @try { + PreKeyBundle *preKeyBundle = [self generatePreKeyBundleForContact:hexEncodedPublicKey forceClean:forceClean]; + if (![Ed25519 verifySignature:preKeyBundle.signedPreKeySignature + publicKey:preKeyBundle.identityKey.throws_removeKeyType + data:preKeyBundle.signedPreKeyPublic]) { + @throw [NSException exceptionWithName:InvalidKeyException reason:@"KeyIsNotValidlySigned" userInfo:nil]; + } + [LKLogger print:[NSString stringWithFormat:@"[Loki] Generated a new pre key bundle for: %@.", hexEncodedPublicKey]]; + return preKeyBundle; + } @catch (NSException *exception) { + failureCount += 1; + forceClean = YES; + } + } + [LKLogger print:[NSString stringWithFormat:@"[Loki] Failed to generate a valid pre key bundle for: %@.", hexEncodedPublicKey]]; + return nil; +} + +- (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)hexEncodedPublicKey { + return [self.dbReadConnection preKeyBundleForKey:hexEncodedPublicKey inCollection:LKPreKeyBundleCollection]; +} + +- (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction { + [transaction setObject:bundle forKey:hexEncodedPublicKey inCollection:LKPreKeyBundleCollection]; + [LKLogger print:[NSString stringWithFormat:@"[Loki] Stored pre key bundle from: %@.", hexEncodedPublicKey]]; + // FIXME: I don't think the line below is good for anything + [transaction.connection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{ }]; +} + +- (void)removePreKeyBundleForContact:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *)transaction { + [transaction removeObjectForKey:hexEncodedPublicKey inCollection:LKPreKeyBundleCollection]; + [LKLogger print:[NSString stringWithFormat:@"[Loki] Removed pre key bundle from: %@.", hexEncodedPublicKey]]; +} + +# pragma mark - Open Groups + +#define LKMessageIDCollection @"LKMessageIDCollection" + +- (void)setIDForMessageWithServerID:(NSUInteger)serverID to:(NSString *)messageID in:(YapDatabaseReadWriteTransaction *)transaction { + NSString *key = [NSString stringWithFormat:@"%@", @(serverID)]; + [transaction setObject:messageID forKey:key inCollection:LKMessageIDCollection]; +} + +- (NSString *_Nullable)getIDForMessageWithServerID:(NSUInteger)serverID in:(YapDatabaseReadTransaction *)transaction { + NSString *key = [NSString stringWithFormat:@"%@", @(serverID)]; + return [transaction objectForKey:key inCollection:LKMessageIDCollection]; +} + +- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction { + NSMutableArray *serverIDs = [NSMutableArray new]; + [transaction enumerateRowsInCollection:LKMessageIDCollection usingBlock:^(NSString *key, id object, id metadata, BOOL *stop) { + if (![object isKindOfClass:NSString.class]) { return; } + NSString *messageID = (NSString *)object; + if (![targetMessageIDs containsObject:messageID]) { return; } + [serverIDs addObject:key]; + }]; + [transaction removeObjectsForKeys:serverIDs inCollection:LKMessageIDCollection]; +} + +# pragma mark - Restoration from Seed + +#define LKGeneralCollection @"Loki" + +- (void)setRestorationTime:(NSTimeInterval)time { + [self.dbReadWriteConnection setDouble:time forKey:@"restoration_time" inCollection:LKGeneralCollection]; +} + +- (NSTimeInterval)getRestorationTime { + return [self.dbReadConnection doubleForKey:@"restoration_time" inCollection:LKGeneralCollection defaultValue:0]; +} + +@end diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+Loki.swift b/SignalUtilitiesKit/OWSPrimaryStorage+Loki.swift new file mode 100644 index 000000000..d9fb7896f --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+Loki.swift @@ -0,0 +1,89 @@ + +// TODO: Make this strongly typed like LKUserDefaults + +public extension OWSPrimaryStorage { + + // MARK: Snode Pool + public func setSnodePool(_ snodePool: Set, in transaction: YapDatabaseReadWriteTransaction) { + clearSnodePool(in: transaction) + snodePool.forEach { snode in + transaction.setObject(snode, forKey: snode.description, inCollection: Storage.snodePoolCollection) + } + } + + public func clearSnodePool(in transaction: YapDatabaseReadWriteTransaction) { + transaction.removeAllObjects(inCollection: Storage.snodePoolCollection) + } + + public func getSnodePool(in transaction: YapDatabaseReadTransaction) -> Set { + var result: Set = [] + transaction.enumerateKeysAndObjects(inCollection: Storage.snodePoolCollection) { _, object, _ in + guard let snode = object as? Snode else { return } + result.insert(snode) + } + return result + } + + public func dropSnodeFromSnodePool(_ snode: Snode, in transaction: YapDatabaseReadWriteTransaction) { + transaction.removeObject(forKey: snode.description, inCollection: Storage.snodePoolCollection) + } + + // MARK: Swarm + public func setSwarm(_ swarm: [Snode], for publicKey: String, in transaction: YapDatabaseReadWriteTransaction) { + print("[Loki] Caching swarm for: \(publicKey == getUserHexEncodedPublicKey() ? "self" : publicKey).") + clearSwarm(for: publicKey, in: transaction) + let collection = Storage.getSwarmCollection(for: publicKey) + swarm.forEach { snode in + transaction.setObject(snode, forKey: snode.description, inCollection: collection) + } + } + + public func clearSwarm(for publicKey: String, in transaction: YapDatabaseReadWriteTransaction) { + let collection = Storage.getSwarmCollection(for: publicKey) + transaction.removeAllObjects(inCollection: collection) + } + + public func getSwarm(for publicKey: String, in transaction: YapDatabaseReadTransaction) -> [Snode] { + var result: [Snode] = [] + let collection = Storage.getSwarmCollection(for: publicKey) + transaction.enumerateKeysAndObjects(inCollection: collection) { _, object, _ in + guard let snode = object as? Snode else { return } + result.append(snode) + } + return result + } + + // MARK: Session Requests + public func setSessionRequestTimestamp(for publicKey: String, to timestamp: Date, in transaction: YapDatabaseReadWriteTransaction) { + transaction.setDate(timestamp, forKey: publicKey, inCollection: Storage.sessionRequestTimestampCollection) + } + + public func getSessionRequestTimestamp(for publicKey: String, in transaction: YapDatabaseReadTransaction) -> Date? { + transaction.date(forKey: publicKey, inCollection: Storage.sessionRequestTimestampCollection) + } + + // MARK: Multi Device + public func setDeviceLinks(_ deviceLinks: Set) { } + public func addDeviceLink(_ deviceLink: DeviceLink) { } + public func removeDeviceLink(_ deviceLink: DeviceLink) { } + public func getDeviceLinks(for masterHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set { return [] } + public func getDeviceLink(for slaveHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> DeviceLink? { return nil } + public func getMasterHexEncodedPublicKey(for slaveHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> String? { return nil } + + // MARK: Open Groups + public func getUserCount(for publicChat: OpenGroup, in transaction: YapDatabaseReadTransaction) -> Int? { + return transaction.object(forKey: publicChat.id, inCollection: Storage.openGroupUserCountCollection) as? Int + } + + public func setUserCount(_ userCount: Int, forPublicChatWithID publicChatID: String, in transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(userCount, forKey: publicChatID, inCollection: Storage.openGroupUserCountCollection) + } + + public func getProfilePictureURL(forPublicChatWithID publicChatID: String, in transaction: YapDatabaseReadTransaction) -> String? { + return transaction.object(forKey: publicChatID, inCollection: Storage.openGroupProfilePictureURLCollection) as? String + } + + public func setProfilePictureURL(_ profilePictureURL: String?, forPublicChatWithID publicChatID: String, in transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(profilePictureURL, forKey: publicChatID, inCollection: Storage.openGroupProfilePictureURLCollection) + } +} diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+PreKeyStore.h b/SignalUtilitiesKit/OWSPrimaryStorage+PreKeyStore.h new file mode 100644 index 000000000..fead08374 --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+PreKeyStore.h @@ -0,0 +1,18 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSPrimaryStorage (PreKeyStore) + +- (NSArray *)generatePreKeyRecords; +- (NSArray *)generatePreKeyRecords:(int)batchSize; +- (void)storePreKeyRecords:(NSArray *)preKeyRecords NS_SWIFT_NAME(storePreKeyRecords(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+PreKeyStore.m b/SignalUtilitiesKit/OWSPrimaryStorage+PreKeyStore.m new file mode 100644 index 000000000..20f345f27 --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+PreKeyStore.m @@ -0,0 +1,112 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage+PreKeyStore.h" +#import "OWSPrimaryStorage+keyFromIntLong.h" +#import "TSStorageKeys.h" +#import "YapDatabaseConnection+OWS.h" +#import + +#define OWSPrimaryStoragePreKeyStoreCollection @"TSStorageManagerPreKeyStoreCollection" +#define TSNextPrekeyIdKey @"TSStorageInternalSettingsNextPreKeyId" +#define BATCH_SIZE 100 + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSPrimaryStorage (PreKeyStore) + +- (NSArray *)generatePreKeyRecords +{ + return [self generatePreKeyRecords:BATCH_SIZE]; +} + +- (NSArray *)generatePreKeyRecords:(int)batchSize +{ + NSMutableArray *preKeyRecords = [NSMutableArray array]; + + @synchronized(self) + { + int preKeyId = [self nextPreKeyId:batchSize]; + + OWSLogInfo(@"building %d new preKeys starting from preKeyId: %d", batchSize, preKeyId); + for (int i = 0; i < batchSize; i++) { + ECKeyPair *keyPair = [Curve25519 generateKeyPair]; + PreKeyRecord *record = [[PreKeyRecord alloc] initWithId:preKeyId keyPair:keyPair]; + + [preKeyRecords addObject:record]; + preKeyId++; + } + + [self.dbReadWriteConnection setInt:preKeyId + forKey:TSNextPrekeyIdKey + inCollection:TSStorageInternalSettingsCollection]; + } + return preKeyRecords; +} + +- (void)storePreKeyRecords:(NSArray *)preKeyRecords +{ + for (PreKeyRecord *record in preKeyRecords) { + [self.dbReadWriteConnection setObject:record + forKey:[self keyFromInt:record.Id] + inCollection:OWSPrimaryStoragePreKeyStoreCollection]; + } +} + +- (PreKeyRecord *)throws_loadPreKey:(int)preKeyId +{ + PreKeyRecord *preKeyRecord = [self.dbReadConnection preKeyRecordForKey:[self keyFromInt:preKeyId] + inCollection:OWSPrimaryStoragePreKeyStoreCollection]; + + if (!preKeyRecord) { + OWSRaiseException(InvalidKeyIdException, @"No pre key found matching key id"); + } else { + return preKeyRecord; + } +} + +- (void)storePreKey:(int)preKeyId preKeyRecord:(PreKeyRecord *)record +{ + [self.dbReadWriteConnection setObject:record + forKey:[self keyFromInt:preKeyId] + inCollection:OWSPrimaryStoragePreKeyStoreCollection]; +} + +- (BOOL)containsPreKey:(int)preKeyId +{ + PreKeyRecord *preKeyRecord = [self.dbReadConnection preKeyRecordForKey:[self keyFromInt:preKeyId] + inCollection:OWSPrimaryStoragePreKeyStoreCollection]; + return (preKeyRecord != nil); +} + +- (void)removePreKey:(int)preKeyId protocolContext:(nullable id)protocolContext +{ + if ([protocolContext isKindOfClass:YapDatabaseReadWriteTransaction.class]) { + [(YapDatabaseReadWriteTransaction *)protocolContext removeObjectForKey:[self keyFromInt:preKeyId] inCollection:OWSPrimaryStoragePreKeyStoreCollection]; + } else { + [self.dbReadWriteConnection removeObjectForKey:[self keyFromInt:preKeyId] inCollection:OWSPrimaryStoragePreKeyStoreCollection]; + } +} + +- (int)nextPreKeyId:(int)batchSize +{ + int lastPreKeyId = + [self.dbReadConnection intForKey:TSNextPrekeyIdKey inCollection:TSStorageInternalSettingsCollection]; + + if (lastPreKeyId < 1) { + // One-time prekey ids must be > 0 and < kPreKeyOfLastResortId. + lastPreKeyId = 1 + arc4random_uniform(kPreKeyOfLastResortId - (batchSize + 1)); + } else if (lastPreKeyId > kPreKeyOfLastResortId - batchSize) { + // We want to "overflow" to 1 when we reach the "prekey of last resort" id + // to avoid biasing towards higher values. + lastPreKeyId = 1; + } + OWSCAssertDebug(lastPreKeyId > 0 && lastPreKeyId < kPreKeyOfLastResortId); + + return lastPreKeyId; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h b/SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h new file mode 100644 index 000000000..e7713c987 --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h @@ -0,0 +1,27 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSPrimaryStorage (SessionStore) + +- (void)archiveAllSessionsForContact:(NSString *)contactIdentifier protocolContext:(nullable id)protocolContext; + +#pragma mark - Debug + +- (void)resetSessionStore:(YapDatabaseReadWriteTransaction *)transaction; + +#if DEBUG +- (void)snapshotSessionStore:(YapDatabaseReadWriteTransaction *)transaction; +- (void)restoreSessionStore:(YapDatabaseReadWriteTransaction *)transaction; +#endif + +- (void)printAllSessions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.m b/SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.m new file mode 100644 index 000000000..8501b263a --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.m @@ -0,0 +1,273 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSFileSystem.h" +#import "SSKEnvironment.h" +#import "YapDatabaseConnection+OWS.h" +#import "YapDatabaseTransaction+OWS.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSPrimaryStorageSessionStoreCollection = @"TSStorageManagerSessionStoreCollection"; +NSString *const kSessionStoreDBConnectionKey = @"kSessionStoreDBConnectionKey"; + +@implementation OWSPrimaryStorage (SessionStore) + +/** + * Special purpose dbConnection which disables the object cache to better enforce transaction semantics on the store. + * Note that it's still technically possible to access this collection from a different collection, + * but that should be considered a bug. + */ ++ (YapDatabaseConnection *)sessionStoreDBConnection +{ + return SSKEnvironment.shared.sessionStoreDBConnection; +} + +- (YapDatabaseConnection *)sessionStoreDBConnection +{ + return [[self class] sessionStoreDBConnection]; +} + +#pragma mark - SessionStore + +- (SessionRecord *)loadSession:(NSString *)contactIdentifier + deviceId:(int)deviceId + protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(contactIdentifier.length > 0); + OWSAssertDebug(deviceId >= 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + NSDictionary *_Nullable dictionary = + [transaction objectForKey:contactIdentifier inCollection:OWSPrimaryStorageSessionStoreCollection]; + + SessionRecord *record; + + if (dictionary) { + record = [dictionary objectForKey:@(deviceId)]; + } + + if (!record) { + return [SessionRecord new]; + } + + return record; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (NSArray *)subDevicesSessions:(NSString *)contactIdentifier protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(contactIdentifier.length > 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + // Deprecated. We aren't currently using this anywhere, but it's "required" by the SessionStore protocol. + // If we are going to start using it I'd want to re-verify it works as intended. + OWSFailDebug(@"subDevicesSessions is deprecated"); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + NSDictionary *_Nullable dictionary = + [transaction objectForKey:contactIdentifier inCollection:OWSPrimaryStorageSessionStoreCollection]; + + return dictionary ? dictionary.allKeys : @[]; +} +#pragma clang diagnostic pop + +- (void)storeSession:(NSString *)contactIdentifier + deviceId:(int)deviceId + session:(SessionRecord *)session + protocolContext:protocolContext +{ + OWSAssertDebug(contactIdentifier.length > 0); + OWSAssertDebug(deviceId >= 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + // We need to ensure subsequent usage of this SessionRecord does not consider this session as "fresh". Normally this + // is achieved by marking things as "not fresh" at the point of deserialization - when we fetch a SessionRecord from + // YapDB (initWithCoder:). However, because YapDB has an object cache, rather than fetching/deserializing, it's + // possible we'd get back *this* exact instance of the object (which, at this point, is still potentially "fresh"), + // thus we explicitly mark this instance as "unfresh", any time we save. + // NOTE: this may no longer be necessary now that we have a non-caching session db connection. + [session markAsUnFresh]; + + NSDictionary *immutableDictionary = + [transaction objectForKey:contactIdentifier inCollection:OWSPrimaryStorageSessionStoreCollection]; + + NSMutableDictionary *dictionary + = (immutableDictionary ? [immutableDictionary mutableCopy] : [NSMutableDictionary new]); + + [dictionary setObject:session forKey:@(deviceId)]; + + [transaction setObject:[dictionary copy] + forKey:contactIdentifier + inCollection:OWSPrimaryStorageSessionStoreCollection]; +} + +- (BOOL)containsSession:(NSString *)contactIdentifier + deviceId:(int)deviceId + protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(contactIdentifier.length > 0); + OWSAssertDebug(deviceId >= 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + return [self loadSession:contactIdentifier deviceId:deviceId protocolContext:protocolContext] + .sessionState.hasSenderChain; +} + +- (void)deleteSessionForContact:(NSString *)contactIdentifier + deviceId:(int)deviceId + protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(contactIdentifier.length > 0); + OWSAssertDebug(deviceId >= 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + OWSLogInfo( + @"[OWSPrimaryStorage (SessionStore)] deleting session for contact: %@ device: %d", contactIdentifier, deviceId); + + NSDictionary *immutableDictionary = + [transaction objectForKey:contactIdentifier inCollection:OWSPrimaryStorageSessionStoreCollection]; + + NSMutableDictionary *dictionary + = (immutableDictionary ? [immutableDictionary mutableCopy] : [NSMutableDictionary new]); + + [dictionary removeObjectForKey:@(deviceId)]; + + [transaction setObject:[dictionary copy] + forKey:contactIdentifier + inCollection:OWSPrimaryStorageSessionStoreCollection]; +} + +- (void)deleteAllSessionsForContact:(NSString *)contactIdentifier protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(contactIdentifier.length > 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + OWSLogInfo(@"[OWSPrimaryStorage (SessionStore)] deleting all sessions for contact:%@", contactIdentifier); + + [transaction removeObjectForKey:contactIdentifier inCollection:OWSPrimaryStorageSessionStoreCollection]; +} + +- (void)archiveAllSessionsForContact:(NSString *)contactIdentifier protocolContext:(nullable id)protocolContext +{ + OWSAssertDebug(contactIdentifier.length > 0); + OWSAssertDebug([protocolContext isKindOfClass:[YapDatabaseReadWriteTransaction class]]); + + YapDatabaseReadWriteTransaction *transaction = protocolContext; + + OWSLogInfo(@"[OWSPrimaryStorage (SessionStore)] archiving all sessions for contact: %@", contactIdentifier); + + __block NSDictionary *sessionRecords = + [transaction objectForKey:contactIdentifier inCollection:OWSPrimaryStorageSessionStoreCollection]; + + for (id deviceId in sessionRecords) { + id object = sessionRecords[deviceId]; + if (![object isKindOfClass:[SessionRecord class]]) { + OWSFailDebug(@"Unexpected object in session dict: %@", [object class]); + continue; + } + + SessionRecord *sessionRecord = (SessionRecord *)object; + [sessionRecord archiveCurrentState]; + } + + [transaction setObject:sessionRecords + forKey:contactIdentifier + inCollection:OWSPrimaryStorageSessionStoreCollection]; +} + +#pragma mark - debug + +- (void)resetSessionStore:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + OWSLogWarn(@"resetting session store"); + + [transaction removeAllObjectsInCollection:OWSPrimaryStorageSessionStoreCollection]; +} + +- (void)printAllSessions +{ + NSString *tag = @"[OWSPrimaryStorage (SessionStore)]"; + [self.sessionStoreDBConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + OWSLogDebug(@"%@ All Sessions:", tag); + [transaction + enumerateKeysAndObjectsInCollection:OWSPrimaryStorageSessionStoreCollection + usingBlock:^(NSString *_Nonnull key, + id _Nonnull deviceSessionsObject, + BOOL *_Nonnull stop) { + if (![deviceSessionsObject isKindOfClass:[NSDictionary class]]) { + OWSFailDebug(@"%@ Unexpected type: %@ in collection.", + tag, + [deviceSessionsObject class]); + return; + } + NSDictionary *deviceSessions = (NSDictionary *)deviceSessionsObject; + + OWSLogDebug(@"%@ Sessions for recipient: %@", tag, key); + [deviceSessions enumerateKeysAndObjectsUsingBlock:^( + id _Nonnull key, id _Nonnull sessionRecordObject, BOOL *_Nonnull stop) { + if (![sessionRecordObject isKindOfClass:[SessionRecord class]]) { + OWSFailDebug(@"%@ Unexpected type: %@ in collection.", + tag, + [sessionRecordObject class]); + return; + } + SessionRecord *sessionRecord = (SessionRecord *)sessionRecordObject; + SessionState *activeState = [sessionRecord sessionState]; + NSArray *previousStates = + [sessionRecord previousSessionStates]; + OWSLogDebug(@"%@ Device: %@ SessionRecord: %@ activeSessionState: " + @"%@ previousSessionStates: %@", + tag, + key, + sessionRecord, + activeState, + previousStates); + }]; + }]; + }]; +} + +#if DEBUG +- (NSString *)snapshotFilePath +{ + // Prefix name with period "." so that backups will ignore these snapshots. + NSString *dirPath = [OWSFileSystem appDocumentDirectoryPath]; + return [dirPath stringByAppendingPathComponent:@".session-snapshot"]; +} + +- (void)snapshotSessionStore:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [transaction snapshotCollection:OWSPrimaryStorageSessionStoreCollection snapshotFilePath:self.snapshotFilePath]; +} + +- (void)restoreSessionStore:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [transaction restoreSnapshotOfCollection:OWSPrimaryStorageSessionStoreCollection + snapshotFilePath:self.snapshotFilePath]; +} +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+SignedPreKeyStore.h b/SignalUtilitiesKit/OWSPrimaryStorage+SignedPreKeyStore.h new file mode 100644 index 000000000..fc95e6792 --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+SignedPreKeyStore.h @@ -0,0 +1,40 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +// Used for testing +extern NSString *const OWSPrimaryStorageSignedPreKeyStoreCollection; + +@interface OWSPrimaryStorage (SignedPreKeyStore) + +- (SignedPreKeyRecord *)generateRandomSignedRecord; + +- (nullable SignedPreKeyRecord *)loadSignedPrekeyOrNil:(int)signedPreKeyId; + +// Returns nil if no current signed prekey id is found. +- (nullable NSNumber *)currentSignedPrekeyId; +- (void)setCurrentSignedPrekeyId:(int)value; +- (nullable SignedPreKeyRecord *)currentSignedPreKey; + +#pragma mark - Prekey update failures + +- (int)prekeyUpdateFailureCount; +- (void)clearPrekeyUpdateFailureCount; +- (int)incrementPrekeyUpdateFailureCount; + +- (nullable NSDate *)firstPrekeyUpdateFailureDate; +- (void)setFirstPrekeyUpdateFailureDate:(nonnull NSDate *)value; +- (void)clearFirstPrekeyUpdateFailureDate; + +#pragma mark - Debugging + +- (void)logSignedPreKeyReport; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSPrimaryStorage+SignedPreKeyStore.m b/SignalUtilitiesKit/OWSPrimaryStorage+SignedPreKeyStore.m new file mode 100644 index 000000000..541257db3 --- /dev/null +++ b/SignalUtilitiesKit/OWSPrimaryStorage+SignedPreKeyStore.m @@ -0,0 +1,224 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSPrimaryStorage+SignedPreKeyStore.h" +#import "OWSIdentityManager.h" +#import "OWSPrimaryStorage+PreKeyStore.h" +#import "OWSPrimaryStorage+keyFromIntLong.h" +#import "YapDatabaseConnection+OWS.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSPrimaryStorageSignedPreKeyStoreCollection = @"TSStorageManagerSignedPreKeyStoreCollection"; +NSString *const OWSPrimaryStorageSignedPreKeyMetadataCollection = @"TSStorageManagerSignedPreKeyMetadataCollection"; +NSString *const OWSPrimaryStorageKeyPrekeyUpdateFailureCount = @"prekeyUpdateFailureCount"; +NSString *const OWSPrimaryStorageKeyFirstPrekeyUpdateFailureDate = @"firstPrekeyUpdateFailureDate"; +NSString *const OWSPrimaryStorageKeyPrekeyCurrentSignedPrekeyId = @"currentSignedPrekeyId"; + +@implementation OWSPrimaryStorage (SignedPreKeyStore) + +- (SignedPreKeyRecord *)generateRandomSignedRecord +{ + ECKeyPair *keyPair = [Curve25519 generateKeyPair]; + + // Signed prekey ids must be > 0. + int preKeyId = 1 + arc4random_uniform(INT32_MAX - 1); + ECKeyPair *_Nullable identityKeyPair = [[OWSIdentityManager sharedManager] identityKeyPair]; + OWSAssert(identityKeyPair); + + @try { + NSData *signature = [Ed25519 sign:keyPair.publicKey.prependKeyType withKeyPair:identityKeyPair]; + return [[SignedPreKeyRecord alloc] initWithId:preKeyId + keyPair:keyPair + signature:signature + generatedAt:[NSDate date]]; + } @catch (NSException *exception) { + // throws_sign only throws when the data to sign is empty or `keyPair` is nil. + // Neither of which should happen. + OWSFail(@"exception: %@", exception); + return nil; + } +} + +- (SignedPreKeyRecord *)throws_loadSignedPrekey:(int)signedPreKeyId +{ + SignedPreKeyRecord *preKeyRecord = + [self.dbReadConnection signedPreKeyRecordForKey:[self keyFromInt:signedPreKeyId] + inCollection:OWSPrimaryStorageSignedPreKeyStoreCollection]; + + if (!preKeyRecord) { + OWSRaiseException(InvalidKeyIdException, @"No signed pre key found matching key id"); + } else { + return preKeyRecord; + } +} + +- (nullable SignedPreKeyRecord *)loadSignedPrekeyOrNil:(int)signedPreKeyId +{ + return [self.dbReadConnection signedPreKeyRecordForKey:[self keyFromInt:signedPreKeyId] + inCollection:OWSPrimaryStorageSignedPreKeyStoreCollection]; +} + +- (NSArray *)loadSignedPreKeys +{ + NSMutableArray *signedPreKeyRecords = [NSMutableArray array]; + + YapDatabaseConnection *conn = [self newDatabaseConnection]; + + [conn readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [transaction enumerateRowsInCollection:OWSPrimaryStorageSignedPreKeyStoreCollection + usingBlock:^(NSString *key, id object, id metadata, BOOL *stop) { + [signedPreKeyRecords addObject:object]; + }]; + }]; + + return signedPreKeyRecords; +} + +- (void)storeSignedPreKey:(int)signedPreKeyId signedPreKeyRecord:(SignedPreKeyRecord *)signedPreKeyRecord +{ + [self.dbReadWriteConnection setObject:signedPreKeyRecord + forKey:[self keyFromInt:signedPreKeyId] + inCollection:OWSPrimaryStorageSignedPreKeyStoreCollection]; +} + +- (BOOL)containsSignedPreKey:(int)signedPreKeyId +{ + PreKeyRecord *preKeyRecord = + [self.dbReadConnection signedPreKeyRecordForKey:[self keyFromInt:signedPreKeyId] + inCollection:OWSPrimaryStorageSignedPreKeyStoreCollection]; + return (preKeyRecord != nil); +} + +- (void)removeSignedPreKey:(int)signedPrekeyId +{ + [self.dbReadWriteConnection removeObjectForKey:[self keyFromInt:signedPrekeyId] + inCollection:OWSPrimaryStorageSignedPreKeyStoreCollection]; +} + +- (nullable NSNumber *)currentSignedPrekeyId +{ + return [self.dbReadConnection objectForKey:OWSPrimaryStorageKeyPrekeyCurrentSignedPrekeyId + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; +} + +- (void)setCurrentSignedPrekeyId:(int)value +{ + [self.dbReadWriteConnection setObject:@(value) + forKey:OWSPrimaryStorageKeyPrekeyCurrentSignedPrekeyId + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; +} + +- (nullable SignedPreKeyRecord *)currentSignedPreKey +{ + __block SignedPreKeyRecord *_Nullable currentRecord; + + [self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + NSNumber *_Nullable preKeyId = [transaction objectForKey:OWSPrimaryStorageKeyPrekeyCurrentSignedPrekeyId + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; + + if (preKeyId == nil) { + return; + } + + currentRecord = + [transaction objectForKey:preKeyId.stringValue inCollection:OWSPrimaryStorageSignedPreKeyStoreCollection]; + }]; + + return currentRecord; +} + +#pragma mark - Prekey update failures + +- (int)prekeyUpdateFailureCount +{ + NSNumber *_Nullable value = [self.dbReadConnection objectForKey:OWSPrimaryStorageKeyPrekeyUpdateFailureCount + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; + // Will default to zero. + return [value intValue]; +} + +- (void)clearPrekeyUpdateFailureCount +{ + [self.dbReadWriteConnection removeObjectForKey:OWSPrimaryStorageKeyPrekeyUpdateFailureCount + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; +} + +- (int)incrementPrekeyUpdateFailureCount +{ + return [self.dbReadWriteConnection incrementIntForKey:OWSPrimaryStorageKeyPrekeyUpdateFailureCount + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; +} + +- (nullable NSDate *)firstPrekeyUpdateFailureDate +{ + return [self.dbReadConnection dateForKey:OWSPrimaryStorageKeyFirstPrekeyUpdateFailureDate + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; +} + +- (void)setFirstPrekeyUpdateFailureDate:(nonnull NSDate *)value +{ + [self.dbReadWriteConnection setDate:value + forKey:OWSPrimaryStorageKeyFirstPrekeyUpdateFailureDate + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; +} + +- (void)clearFirstPrekeyUpdateFailureDate +{ + [self.dbReadWriteConnection removeObjectForKey:OWSPrimaryStorageKeyFirstPrekeyUpdateFailureDate + inCollection:OWSPrimaryStorageSignedPreKeyMetadataCollection]; +} + +#pragma mark - Debugging + +- (void)logSignedPreKeyReport +{ + NSString *tag = @"[OWSPrimaryStorage (SignedPreKeyStore)]"; + + NSNumber *currentId = [self currentSignedPrekeyId]; + NSDate *firstPrekeyUpdateFailureDate = [self firstPrekeyUpdateFailureDate]; + NSUInteger prekeyUpdateFailureCount = [self prekeyUpdateFailureCount]; + + [self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + __block int i = 0; + + OWSLogInfo(@"%@ SignedPreKeys Report:", tag); + OWSLogInfo(@"%@ currentId: %@", tag, currentId); + OWSLogInfo(@"%@ firstPrekeyUpdateFailureDate: %@", tag, firstPrekeyUpdateFailureDate); + OWSLogInfo(@"%@ prekeyUpdateFailureCount: %lu", tag, (unsigned long)prekeyUpdateFailureCount); + + NSUInteger count = [transaction numberOfKeysInCollection:OWSPrimaryStorageSignedPreKeyStoreCollection]; + OWSLogInfo(@"%@ All Keys (count: %lu):", tag, (unsigned long)count); + + [transaction + enumerateKeysAndObjectsInCollection:OWSPrimaryStorageSignedPreKeyStoreCollection + usingBlock:^( + NSString *_Nonnull key, id _Nonnull signedPreKeyObject, BOOL *_Nonnull stop) { + i++; + if (![signedPreKeyObject isKindOfClass:[SignedPreKeyRecord class]]) { + OWSFailDebug(@"%@ Was expecting SignedPreKeyRecord, but found: %@", + tag, + [signedPreKeyObject class]); + return; + } + SignedPreKeyRecord *signedPreKeyRecord + = (SignedPreKeyRecord *)signedPreKeyObject; + OWSLogInfo(@"%@ #%d +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSUIDatabaseConnectionWillUpdateNotification = @"OWSUIDatabaseConnectionWillUpdateNotification"; +NSString *const OWSUIDatabaseConnectionDidUpdateNotification = @"OWSUIDatabaseConnectionDidUpdateNotification"; +NSString *const OWSUIDatabaseConnectionWillUpdateExternallyNotification = @"OWSUIDatabaseConnectionWillUpdateExternallyNotification"; +NSString *const OWSUIDatabaseConnectionDidUpdateExternallyNotification = @"OWSUIDatabaseConnectionDidUpdateExternallyNotification"; + +NSString *const OWSUIDatabaseConnectionNotificationsKey = @"OWSUIDatabaseConnectionNotificationsKey"; + +void VerifyRegistrationsForPrimaryStorage(OWSStorage *storage) +{ + OWSCAssertDebug(storage); + + [[storage newDatabaseConnection] asyncReadWithBlock:^(YapDatabaseReadTransaction *transaction) { + for (NSString *extensionName in storage.registeredExtensionNames) { + OWSLogVerbose(@"Verifying database extension: %@", extensionName); + YapDatabaseViewTransaction *_Nullable viewTransaction = [transaction ext:extensionName]; + if (!viewTransaction) { + OWSCFailDebug(@"VerifyRegistrationsForPrimaryStorage missing database extension: %@", extensionName); + + [OWSStorage incrementVersionOfDatabaseExtension:extensionName]; + } + } + }]; +} + +#pragma mark - + +@interface OWSPrimaryStorage () + +@property (atomic) BOOL areAsyncRegistrationsComplete; +@property (atomic) BOOL areSyncRegistrationsComplete; +@property (nonatomic, readonly) YapDatabaseConnectionPool *dbReadPool; + +@end + +#pragma mark - + +@implementation OWSPrimaryStorage + +@synthesize uiDatabaseConnection = _uiDatabaseConnection; + ++ (instancetype)sharedManager +{ + OWSAssertDebug(SSKEnvironment.shared.primaryStorage); + + return SSKEnvironment.shared.primaryStorage; +} + +- (instancetype)initStorage +{ + self = [super initStorage]; + + if (self) { + [self loadDatabase]; + + _dbReadPool = [[YapDatabaseConnectionPool alloc] initWithDatabase:self.database]; + _dbReadWriteConnection = [self newDatabaseConnection]; + _uiDatabaseConnection = [self newDatabaseConnection]; + + // Increase object cache limit. Default is 250. + _uiDatabaseConnection.objectCacheLimit = 500; + [_uiDatabaseConnection beginLongLivedReadTransaction]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModified:) + name:YapDatabaseModifiedNotification + object:self.dbNotificationObject]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModifiedExternally:) + name:YapDatabaseModifiedExternallyNotification + object:nil]; + + OWSSingletonAssert(); + } + + return self; +} + +- (void)dealloc +{ + // Surface memory leaks by logging the deallocation of this class. + OWSLogVerbose(@"Dealloc: %@", self.class); + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)yapDatabaseModifiedExternally:(NSNotification *)notification +{ + // Notify observers we're about to update the database connection + [[NSNotificationCenter defaultCenter] postNotificationName:OWSUIDatabaseConnectionWillUpdateExternallyNotification object:self.dbNotificationObject]; + + // Move uiDatabaseConnection to the latest commit. + // Do so atomically, and fetch all the notifications for each commit we jump. + NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction]; + + // Notify observers that the uiDatabaseConnection was updated + NSDictionary *userInfo = @{ OWSUIDatabaseConnectionNotificationsKey: notifications }; + [[NSNotificationCenter defaultCenter] postNotificationName:OWSUIDatabaseConnectionDidUpdateExternallyNotification + object:self.dbNotificationObject + userInfo:userInfo]; +} + +- (void)yapDatabaseModified:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + OWSLogVerbose(@""); + [self updateUIDatabaseConnectionToLatest]; +} + +- (void)updateUIDatabaseConnectionToLatest +{ + OWSAssertIsOnMainThread(); + + // Notify observers we're about to update the database connection + [[NSNotificationCenter defaultCenter] postNotificationName:OWSUIDatabaseConnectionWillUpdateNotification object:self.dbNotificationObject]; + + // Move uiDatabaseConnection to the latest commit. + // Do so atomically, and fetch all the notifications for each commit we jump. + NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction]; + + // Notify observers that the uiDatabaseConnection was updated + NSDictionary *userInfo = @{ OWSUIDatabaseConnectionNotificationsKey: notifications }; + [[NSNotificationCenter defaultCenter] postNotificationName:OWSUIDatabaseConnectionDidUpdateNotification + object:self.dbNotificationObject + userInfo:userInfo]; +} + +- (YapDatabaseConnection *)uiDatabaseConnection +{ + OWSAssertIsOnMainThread(); + return _uiDatabaseConnection; +} + +- (void)resetStorage +{ + _dbReadPool = nil; + _uiDatabaseConnection = nil; + _dbReadWriteConnection = nil; + + [super resetStorage]; +} + +- (void)runSyncRegistrations +{ + // Synchronously register extensions which are essential for views. + [TSDatabaseView registerCrossProcessNotifier:self]; + + // See comments on OWSDatabaseConnection. + // + // In the absence of finding documentation that can shed light on the issue we've been + // seeing, this issue only seems to affect sync and not async registrations. We've always + // been opening write transactions before the async registrations complete without negative + // consequences. + OWSAssertDebug(!self.areSyncRegistrationsComplete); + self.areSyncRegistrationsComplete = YES; +} + +- (void)runAsyncRegistrationsWithCompletion:(void (^_Nonnull)(void))completion +{ + OWSAssertDebug(completion); + OWSAssertDebug(self.database); + + OWSLogVerbose(@"async registrations enqueuing."); + + // Asynchronously register other extensions. + // + // All sync registrations must be done before all async registrations, + // or the sync registrations will block on the async registrations. + [TSDatabaseView asyncRegisterLegacyThreadInteractionsDatabaseView:self]; + [TSDatabaseView asyncRegisterThreadInteractionsDatabaseView:self]; + [TSDatabaseView asyncRegisterThreadDatabaseView:self]; + [TSDatabaseView asyncRegisterUnreadDatabaseView:self]; + [self asyncRegisterExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] + withName:[TSDatabaseSecondaryIndexes registerTimeStampIndexExtensionName]]; + + [OWSMessageReceiver asyncRegisterDatabaseExtension:self]; + [OWSBatchMessageProcessor asyncRegisterDatabaseExtension:self]; + + [TSDatabaseView asyncRegisterUnseenDatabaseView:self]; + [TSDatabaseView asyncRegisterThreadOutgoingMessagesDatabaseView:self]; + [TSDatabaseView asyncRegisterThreadSpecialMessagesDatabaseView:self]; + + [FullTextSearchFinder asyncRegisterDatabaseExtensionWithStorage:self]; + [OWSIncomingMessageFinder asyncRegisterExtensionWithPrimaryStorage:self]; + [TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView:self]; + [OWSDisappearingMessagesFinder asyncRegisterDatabaseExtensions:self]; + [OWSFailedMessagesJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:self]; + [OWSIncompleteCallsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:self]; + [OWSFailedAttachmentDownloadsJob asyncRegisterDatabaseExtensionsWithPrimaryStorage:self]; + [OWSMediaGalleryFinder asyncRegisterDatabaseExtensionsWithPrimaryStorage:self]; + [TSDatabaseView asyncRegisterLazyRestoreAttachmentsDatabaseView:self]; + [SSKJobRecordFinder asyncRegisterDatabaseExtensionObjCWithStorage:self]; + + // Loki + [LKDeviceLinkIndex asyncRegisterDatabaseExtensions:self]; + + [self.database + flushExtensionRequestsWithCompletionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + completionBlock:^{ + OWSAssertDebug(!self.areAsyncRegistrationsComplete); + OWSLogVerbose(@"async registrations complete."); + + self.areAsyncRegistrationsComplete = YES; + + completion(); + + [self verifyDatabaseViews]; + }]; +} + +- (void)verifyDatabaseViews +{ + VerifyRegistrationsForPrimaryStorage(self); +} + ++ (void)protectFiles +{ + OWSLogInfo(@"Database file size: %@", [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath]); + OWSLogInfo(@"\t SHM file size: %@", [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath_SHM]); + OWSLogInfo(@"\t WAL file size: %@", [OWSFileSystem fileSizeOfPath:self.sharedDataDatabaseFilePath_WAL]); + + // Protect the entire new database directory. + [OWSFileSystem protectFileOrFolderAtPath:self.sharedDataDatabaseDirPath]; +} + ++ (NSString *)legacyDatabaseDirPath +{ + return [OWSFileSystem appDocumentDirectoryPath]; +} + ++ (NSString *)sharedDataDatabaseDirPath +{ + NSString *databaseDirPath = [[OWSFileSystem appSharedDataDirectoryPath] stringByAppendingPathComponent:@"database"]; + + if (![OWSFileSystem ensureDirectoryExists:databaseDirPath]) { + OWSFail(@"Could not create new database directory"); + } + return databaseDirPath; +} + ++ (NSString *)databaseFilename +{ + return @"Signal.sqlite"; +} + ++ (NSString *)databaseFilename_SHM +{ + return [self.databaseFilename stringByAppendingString:@"-shm"]; +} + ++ (NSString *)databaseFilename_WAL +{ + return [self.databaseFilename stringByAppendingString:@"-wal"]; +} + ++ (NSString *)legacyDatabaseFilePath +{ + return [self.legacyDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename]; +} + ++ (NSString *)legacyDatabaseFilePath_SHM +{ + return [self.legacyDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_SHM]; +} + ++ (NSString *)legacyDatabaseFilePath_WAL +{ + return [self.legacyDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_WAL]; +} + ++ (NSString *)sharedDataDatabaseFilePath +{ + return [self.sharedDataDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename]; +} + ++ (NSString *)sharedDataDatabaseFilePath_SHM +{ + return [self.sharedDataDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_SHM]; +} + ++ (NSString *)sharedDataDatabaseFilePath_WAL +{ + return [self.sharedDataDatabaseDirPath stringByAppendingPathComponent:self.databaseFilename_WAL]; +} + ++ (nullable NSError *)migrateToSharedData +{ + OWSLogInfo(@""); + + // Given how sensitive this migration is, we verbosely + // log the contents of all involved paths before and after. + NSArray *paths = @[ + self.legacyDatabaseFilePath, + self.legacyDatabaseFilePath_SHM, + self.legacyDatabaseFilePath_WAL, + self.sharedDataDatabaseFilePath, + self.sharedDataDatabaseFilePath_SHM, + self.sharedDataDatabaseFilePath_WAL, + ]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + for (NSString *path in paths) { + if ([fileManager fileExistsAtPath:path]) { + OWSLogInfo(@"before migrateToSharedData: %@, %@", path, [OWSFileSystem fileSizeOfPath:path]); + } + } + + // We protect the db files here, which is somewhat redundant with what will happen in + // `moveAppFilePath:` which also ensures file protection. + // However that method dispatches async, since it can take a while with large attachment directories. + // + // Since we only have three files here it'll be quick to do it sync, and we want to make + // sure it happens as part of the migration. + // + // FileProtection attributes move with the file, so we do it on the legacy files before moving + // them. + [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath]; + [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_SHM]; + [OWSFileSystem protectFileOrFolderAtPath:self.legacyDatabaseFilePath_WAL]; + + NSError *_Nullable error = nil; + if ([fileManager fileExistsAtPath:self.legacyDatabaseFilePath] && + [fileManager fileExistsAtPath:self.sharedDataDatabaseFilePath]) { + // In the case that we have a "database conflict" (i.e. database files + // in the src and dst locations), ensure database integrity by renaming + // all of the dst database files. + for (NSString *filePath in @[ + self.sharedDataDatabaseFilePath, + self.sharedDataDatabaseFilePath_SHM, + self.sharedDataDatabaseFilePath_WAL, + ]) { + error = [OWSFileSystem renameFilePathUsingRandomExtension:filePath]; + if (error) { + return error; + } + } + } + + error = + [OWSFileSystem moveAppFilePath:self.legacyDatabaseFilePath sharedDataFilePath:self.sharedDataDatabaseFilePath]; + if (error) { + return error; + } + error = [OWSFileSystem moveAppFilePath:self.legacyDatabaseFilePath_SHM + sharedDataFilePath:self.sharedDataDatabaseFilePath_SHM]; + if (error) { + return error; + } + error = [OWSFileSystem moveAppFilePath:self.legacyDatabaseFilePath_WAL + sharedDataFilePath:self.sharedDataDatabaseFilePath_WAL]; + if (error) { + return error; + } + + for (NSString *path in paths) { + if ([fileManager fileExistsAtPath:path]) { + OWSLogInfo(@"after migrateToSharedData: %@, %@", path, [OWSFileSystem fileSizeOfPath:path]); + } + } + + return nil; +} + ++ (NSString *)databaseFilePath +{ + OWSLogVerbose(@"databasePath: %@", OWSPrimaryStorage.sharedDataDatabaseFilePath); + + return self.sharedDataDatabaseFilePath; +} + ++ (NSString *)databaseFilePath_SHM +{ + return self.sharedDataDatabaseFilePath_SHM; +} + ++ (NSString *)databaseFilePath_WAL +{ + return self.sharedDataDatabaseFilePath_WAL; +} + +- (NSString *)databaseFilePath +{ + return OWSPrimaryStorage.databaseFilePath; +} + +- (NSString *)databaseFilePath_SHM +{ + return OWSPrimaryStorage.databaseFilePath_SHM; +} + +- (NSString *)databaseFilePath_WAL +{ + return OWSPrimaryStorage.databaseFilePath_WAL; +} + +- (NSString *)databaseFilename_SHM +{ + return OWSPrimaryStorage.databaseFilename_SHM; +} + +- (NSString *)databaseFilename_WAL +{ + return OWSPrimaryStorage.databaseFilename_WAL; +} + ++ (YapDatabaseConnection *)dbReadConnection +{ + return OWSPrimaryStorage.sharedManager.dbReadConnection; +} + +- (YapDatabaseConnection *)dbReadConnection +{ + return self.dbReadPool.connection; +} + ++ (YapDatabaseConnection *)dbReadWriteConnection +{ + return OWSPrimaryStorage.sharedManager.dbReadWriteConnection; +} + +#pragma mark - Misc. + +- (void)touchDbAsync +{ + OWSLogInfo(@""); + + // There appears to be a bug in YapDatabase that sometimes delays modifications + // made in another process (e.g. the SAE) from showing up in other processes. + // There's a simple workaround: a trivial write to the database flushes changes + // made from other processes. + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction setObject:[NSUUID UUID].UUIDString forKey:@"conversation_view_noop_mod" inCollection:@"temp"]; + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSProfileKeyMessage.h b/SignalUtilitiesKit/OWSProfileKeyMessage.h new file mode 100644 index 000000000..c0d04bc83 --- /dev/null +++ b/SignalUtilitiesKit/OWSProfileKeyMessage.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSProfileKeyMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSProfileKeyMessage.m b/SignalUtilitiesKit/OWSProfileKeyMessage.m new file mode 100644 index 000000000..8682f01f4 --- /dev/null +++ b/SignalUtilitiesKit/OWSProfileKeyMessage.m @@ -0,0 +1,78 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSProfileKeyMessage.h" +#import "ProfileManagerProtocol.h" +#import "ProtoUtils.h" +#import "SSKEnvironment.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSProfileKeyMessage + +- (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread +{ + return [super initOutgoingMessageWithTimestamp:timestamp + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeProfileKey]; } + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (BOOL)shouldSyncTranscript +{ + return NO; +} + +- (nullable SSKProtoDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId +{ + OWSAssertDebug(self.thread); + + SSKProtoDataMessageBuilder *_Nullable builder = [self dataMessageBuilder]; + if (!builder) { + OWSFailDebug(@"could not build protobuf."); + return nil; + } + [builder setTimestamp:self.timestamp]; + [ProtoUtils addLocalProfileKeyToDataMessageBuilder:builder]; + [builder setFlags:SSKProtoDataMessageFlagsProfileKeyUpdate]; + + if (recipientId.length > 0) { + // Once we've shared our profile key with a user (perhaps due to being + // a member of a whitelisted group), make sure they're whitelisted. + id profileManager = SSKEnvironment.shared.profileManager; + [profileManager addUserToProfileWhitelist:recipientId]; + } + + NSError *error; + SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error]; + if (error || !dataProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + return dataProto; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSProvisioningCipher.h b/SignalUtilitiesKit/OWSProvisioningCipher.h new file mode 100644 index 000000000..ede2142af --- /dev/null +++ b/SignalUtilitiesKit/OWSProvisioningCipher.h @@ -0,0 +1,18 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSProvisioningCipher : NSObject + +@property (nonatomic, readonly) NSData *ourPublicKey; + +- (instancetype)initWithTheirPublicKey:(NSData *)theirPublicKey; +- (nullable NSData *)encrypt:(NSData *)plainText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSProvisioningCipher.m b/SignalUtilitiesKit/OWSProvisioningCipher.m new file mode 100644 index 000000000..b63074b85 --- /dev/null +++ b/SignalUtilitiesKit/OWSProvisioningCipher.m @@ -0,0 +1,157 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSProvisioningCipher.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSProvisioningCipher () + +@property (nonatomic, readonly) NSData *theirPublicKey; +@property (nonatomic, readonly) ECKeyPair *ourKeyPair; +@property (nonatomic, readonly) NSData *initializationVector; + +@end + +#pragma mark - + +@implementation OWSProvisioningCipher + +- (instancetype)initWithTheirPublicKey:(NSData *)theirPublicKey +{ + return [self initWithTheirPublicKey:theirPublicKey + ourKeyPair:[Curve25519 generateKeyPair] + initializationVector:[Cryptography generateRandomBytes:kCCBlockSizeAES128]]; +} + +// Private method which exposes dependencies for testing +- (instancetype)initWithTheirPublicKey:(NSData *)theirPublicKey + ourKeyPair:(ECKeyPair *)ourKeyPair + initializationVector:(NSData *)initializationVector +{ + self = [super init]; + if (!self) { + return self; + } + + _theirPublicKey = theirPublicKey; + _ourKeyPair = ourKeyPair; + _initializationVector = initializationVector; + + return self; +} + +- (NSData *)ourPublicKey +{ + return self.ourKeyPair.publicKey; +} + +- (nullable NSData *)encrypt:(NSData *)dataToEncrypt +{ + @try { + return [self throws_encryptWithData:dataToEncrypt]; + } @catch (NSException *exception) { + OWSFailDebug(@"exception: %@ of type: %@ with reason: %@, user info: %@.", + exception.description, + exception.name, + exception.reason, + exception.userInfo); + return nil; + } +} + +- (nullable NSData *)throws_encryptWithData:(NSData *)dataToEncrypt +{ + NSData *sharedSecret = + [Curve25519 generateSharedSecretFromPublicKey:self.theirPublicKey andKeyPair:self.ourKeyPair]; + + NSData *infoData = [@"TextSecure Provisioning Message" dataUsingEncoding:NSASCIIStringEncoding]; + NSData *nullSalt = [[NSMutableData dataWithLength:32] copy]; + NSData *derivedSecret = [HKDFKit deriveKey:sharedSecret info:infoData salt:nullSalt outputSize:64]; + NSData *cipherKey = [derivedSecret subdataWithRange:NSMakeRange(0, 32)]; + NSData *macKey = [derivedSecret subdataWithRange:NSMakeRange(32, 32)]; + if (cipherKey.length != 32) { + OWSFailDebug(@"Cipher Key must be 32 bytes"); + return nil; + } + if (macKey.length != 32) { + OWSFailDebug(@"Mac Key must be 32 bytes"); + return nil; + } + + u_int8_t versionByte[] = { 0x01 }; + NSMutableData *message = [NSMutableData dataWithBytes:&versionByte length:1]; + + NSData *_Nullable cipherText = [self encrypt:dataToEncrypt withKey:cipherKey]; + if (cipherText == nil) { + OWSFailDebug(@"Provisioning cipher failed."); + return nil; + } + + [message appendData:cipherText]; + + NSData *_Nullable mac = [self macForMessage:message withKey:macKey]; + if (mac == nil) { + OWSFailDebug(@"mac failed."); + return nil; + } + [message appendData:mac]; + + return [message copy]; +} + +- (nullable NSData *)encrypt:(NSData *)dataToEncrypt withKey:(NSData *)cipherKey +{ + NSData *iv = self.initializationVector; + if (iv.length != kCCBlockSizeAES128) { + OWSFailDebug(@"Unexpected length for iv"); + return nil; + } + if (dataToEncrypt.length >= SIZE_MAX - (kCCBlockSizeAES128 + iv.length)) { + OWSFailDebug(@"data is too long to encrypt."); + return nil; + } + + // allow space for message + padding any incomplete block. PKCS7 padding will always add at least one byte. + size_t ciphertextBufferSize = dataToEncrypt.length + kCCBlockSizeAES128; + + NSMutableData *ciphertextData = [[NSMutableData alloc] initWithLength:ciphertextBufferSize]; + + size_t bytesEncrypted = 0; + CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, + kCCAlgorithmAES, + kCCOptionPKCS7Padding, + cipherKey.bytes, + cipherKey.length, + iv.bytes, + dataToEncrypt.bytes, + dataToEncrypt.length, + ciphertextData.mutableBytes, + ciphertextBufferSize, + &bytesEncrypted); + + if (cryptStatus != kCCSuccess) { + OWSFailDebug(@"Encryption failed with status: %d", cryptStatus); + return nil; + } + + // message format is (iv || ciphertext) + NSMutableData *encryptedMessage = [NSMutableData new]; + [encryptedMessage appendData:iv]; + [encryptedMessage appendData:[ciphertextData subdataWithRange:NSMakeRange(0, bytesEncrypted)]]; + return [encryptedMessage copy]; +} + +- (nullable NSData *)macForMessage:(NSData *)message withKey:(NSData *)macKey +{ + return [Cryptography computeSHA256HMAC:message withHMACKey:macKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSProvisioningMessage.h b/SignalUtilitiesKit/OWSProvisioningMessage.h new file mode 100644 index 000000000..e4b22748f --- /dev/null +++ b/SignalUtilitiesKit/OWSProvisioningMessage.h @@ -0,0 +1,23 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSProvisioningMessage : NSObject + +- (instancetype)initWithMyPublicKey:(NSData *)myPublicKey + myPrivateKey:(NSData *)myPrivateKey + theirPublicKey:(NSData *)theirPublicKey + accountIdentifier:(NSString *)accountIdentifier + profileKey:(NSData *)profileKey + readReceiptsEnabled:(BOOL)areReadReceiptsEnabled + provisioningCode:(NSString *)provisioningCode; + +- (nullable NSData *)buildEncryptedMessageBody; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSProvisioningMessage.m b/SignalUtilitiesKit/OWSProvisioningMessage.m new file mode 100644 index 000000000..91af10564 --- /dev/null +++ b/SignalUtilitiesKit/OWSProvisioningMessage.m @@ -0,0 +1,93 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSProvisioningMessage.h" +#import "OWSProvisioningCipher.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSProvisioningMessage () + +@property (nonatomic, readonly) NSData *myPublicKey; +@property (nonatomic, readonly) NSData *myPrivateKey; +@property (nonatomic, readonly) NSString *accountIdentifier; +@property (nonatomic, readonly) NSData *theirPublicKey; +@property (nonatomic, readonly) NSData *profileKey; +@property (nonatomic, readonly) BOOL areReadReceiptsEnabled; +@property (nonatomic, readonly) NSString *provisioningCode; + +@end + +@implementation OWSProvisioningMessage + +- (instancetype)initWithMyPublicKey:(NSData *)myPublicKey + myPrivateKey:(NSData *)myPrivateKey + theirPublicKey:(NSData *)theirPublicKey + accountIdentifier:(NSString *)accountIdentifier + profileKey:(NSData *)profileKey + readReceiptsEnabled:(BOOL)areReadReceiptsEnabled + provisioningCode:(NSString *)provisioningCode +{ + self = [super init]; + if (!self) { + return self; + } + + _myPublicKey = myPublicKey; + _myPrivateKey = myPrivateKey; + _theirPublicKey = theirPublicKey; + _accountIdentifier = accountIdentifier; + _profileKey = profileKey; + _areReadReceiptsEnabled = areReadReceiptsEnabled; + _provisioningCode = provisioningCode; + + return self; +} + +- (nullable NSData *)buildEncryptedMessageBody +{ + ProvisioningProtoProvisionMessageBuilder *messageBuilder = + [ProvisioningProtoProvisionMessage builderWithIdentityKeyPublic:self.myPublicKey + identityKeyPrivate:self.myPrivateKey + number:self.accountIdentifier + provisioningCode:self.provisioningCode + userAgent:@"OWI" + profileKey:self.profileKey + readReceipts:self.areReadReceiptsEnabled]; + + NSError *error; + NSData *_Nullable plainTextProvisionMessage = [messageBuilder buildSerializedDataAndReturnError:&error]; + if (!plainTextProvisionMessage || error) { + OWSFailDebug(@"could not serialize proto: %@.", error); + return nil; + } + + OWSProvisioningCipher *cipher = [[OWSProvisioningCipher alloc] initWithTheirPublicKey:self.theirPublicKey]; + NSData *_Nullable encryptedProvisionMessage = [cipher encrypt:plainTextProvisionMessage]; + if (encryptedProvisionMessage == nil) { + OWSFailDebug(@"Failed to encrypt provision message"); + return nil; + } + + // Note that this is a one-time-use *cipher* public key, not our Signal *identity* public key + ProvisioningProtoProvisionEnvelopeBuilder *envelopeBuilder = + [ProvisioningProtoProvisionEnvelope builderWithPublicKey:[cipher.ourPublicKey prependKeyType] + body:encryptedProvisionMessage]; + + NSData *_Nullable envelopeData = [envelopeBuilder buildSerializedDataAndReturnError:&error]; + if (!envelopeData || error) { + OWSFailDebug(@"could not serialize proto: %@.", error); + return nil; + } + + return envelopeData; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSQueues.h b/SignalUtilitiesKit/OWSQueues.h new file mode 100644 index 000000000..5ca99712a --- /dev/null +++ b/SignalUtilitiesKit/OWSQueues.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#ifdef DEBUG + +#define AssertOnDispatchQueue(queue) \ + { \ + if (@available(iOS 10.0, *)) { \ + dispatch_assert_queue(queue); \ + } else { \ + _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") \ + OWSAssertDebug(dispatch_get_current_queue() == queue); \ + _Pragma("clang diagnostic pop") \ + } \ + } + +#else + +#define AssertOnDispatchQueue(queue) + +#endif + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSReadReceiptManager.h b/SignalUtilitiesKit/OWSReadReceiptManager.h new file mode 100644 index 000000000..a01f46e29 --- /dev/null +++ b/SignalUtilitiesKit/OWSReadReceiptManager.h @@ -0,0 +1,88 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class SSKProtoSyncMessageRead; +@class TSIncomingMessage; +@class TSOutgoingMessage; +@class TSThread; +@class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; + +extern NSString *const kIncomingMessageMarkedAsReadNotification; + +// There are four kinds of read receipts: +// +// * Read receipts that this client sends to linked +// devices to inform them that a message has been read. +// * Read receipts that this client receives from linked +// devices that inform this client that a message has been read. +// * These read receipts are saved so that they can be applied +// if they arrive before the corresponding message. +// * Read receipts that this client sends to other users +// to inform them that a message has been read. +// * Read receipts that this client receives from other users +// that inform this client that a message has been read. +// * These read receipts are saved so that they can be applied +// if they arrive before the corresponding message. +// +// This manager is responsible for handling and emitting all four kinds. +@interface OWSReadReceiptManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; ++ (instancetype)sharedManager; + +#pragma mark - Sender/Recipient Read Receipts + +// This method should be called when we receive a read receipt +// from a user to whom we have sent a message. +// +// This method can be called from any thread. +- (void)processReadReceiptsFromRecipientId:(NSString *)recipientId + sentTimestamps:(NSArray *)sentTimestamps + readTimestamp:(uint64_t)readTimestamp; + +- (void)applyEarlyReadReceiptsForOutgoingMessageFromLinkedDevice:(TSOutgoingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark - Linked Device Read Receipts + +- (void)processReadReceiptsFromLinkedDevice:(NSArray *)readReceiptProtos + readTimestamp:(uint64_t)readTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)applyEarlyReadReceiptsForIncomingMessage:(TSIncomingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark - Locally Read + +// This method cues this manager: +// +// * ...to inform the sender that this message was read (if read receipts +// are enabled). +// * ...to inform the local user's other devices that this message was read. +// +// Both types of messages are deduplicated. +// +// This method can be called from any thread. +- (void)messageWasReadLocally:(TSIncomingMessage *)message; + +- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread; + +#pragma mark - Settings + +- (void)prepareCachedValues; + +- (BOOL)areReadReceiptsEnabled; +- (BOOL)areReadReceiptsEnabledWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (void)setAreReadReceiptsEnabled:(BOOL)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSReadReceiptManager.m b/SignalUtilitiesKit/OWSReadReceiptManager.m new file mode 100644 index 000000000..934ce270a --- /dev/null +++ b/SignalUtilitiesKit/OWSReadReceiptManager.m @@ -0,0 +1,552 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSReadReceiptManager.h" +#import "AppReadiness.h" +#import "OWSLinkedDeviceReadReceipt.h" +#import "OWSMessageSender.h" +#import "OWSOutgoingReceiptManager.h" +#import "OWSPrimaryStorage.h" +#import "OWSReadReceiptsForLinkedDevicesMessage.h" +#import "OWSReceiptsForSenderMessage.h" +#import "OWSStorage.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSContactThread.h" +#import "TSDatabaseView.h" +#import "TSIncomingMessage.h" +#import "YapDatabaseConnection+OWS.h" +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kIncomingMessageMarkedAsReadNotification = @"kIncomingMessageMarkedAsReadNotification"; + +@interface TSRecipientReadReceipt : TSYapDatabaseObject + +@property (nonatomic, readonly) uint64_t sentTimestamp; +// Map of "recipient id"-to-"read timestamp". +@property (nonatomic, readonly) NSDictionary *recipientMap; + +@end + +#pragma mark - + +@implementation TSRecipientReadReceipt + ++ (NSString *)collection +{ + return @"TSRecipientReadReceipt2"; +} + +- (instancetype)initWithSentTimestamp:(uint64_t)sentTimestamp +{ + OWSAssertDebug(sentTimestamp > 0); + + self = [super initWithUniqueId:[TSRecipientReadReceipt uniqueIdForSentTimestamp:sentTimestamp]]; + + if (self) { + _sentTimestamp = sentTimestamp; + _recipientMap = [NSDictionary new]; + } + + return self; +} + ++ (NSString *)uniqueIdForSentTimestamp:(uint64_t)timestamp +{ + return [NSString stringWithFormat:@"%llu", timestamp]; +} + +- (void)addRecipientId:(NSString *)recipientId timestamp:(uint64_t)timestamp +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(timestamp > 0); + + NSMutableDictionary *recipientMapCopy = [self.recipientMap mutableCopy]; + recipientMapCopy[recipientId] = @(timestamp); + _recipientMap = [recipientMapCopy copy]; +} + ++ (void)addRecipientId:(NSString *)recipientId + sentTimestamp:(uint64_t)sentTimestamp + readTimestamp:(uint64_t)readTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + TSRecipientReadReceipt *_Nullable recipientReadReceipt = + [transaction objectForKey:[self uniqueIdForSentTimestamp:sentTimestamp] inCollection:[self collection]]; + if (!recipientReadReceipt) { + recipientReadReceipt = [[TSRecipientReadReceipt alloc] initWithSentTimestamp:sentTimestamp]; + } + [recipientReadReceipt addRecipientId:recipientId timestamp:readTimestamp]; + [recipientReadReceipt saveWithTransaction:transaction]; +} + ++ (nullable NSDictionary *)recipientMapForSentTimestamp:(uint64_t)sentTimestamp + transaction: + (YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + TSRecipientReadReceipt *_Nullable recipientReadReceipt = + [transaction objectForKey:[self uniqueIdForSentTimestamp:sentTimestamp] inCollection:[self collection]]; + return recipientReadReceipt.recipientMap; +} + ++ (void)removeRecipientIdsForTimestamp:(uint64_t)sentTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [transaction removeObjectForKey:[self uniqueIdForSentTimestamp:sentTimestamp] inCollection:[self collection]]; +} + +@end + +#pragma mark - + +NSString *const OWSReadReceiptManagerCollection = @"OWSReadReceiptManagerCollection"; +NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsEnabled"; + +@interface OWSReadReceiptManager () + +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +// A map of "thread unique id"-to-"read receipt" for read receipts that +// we will send to our linked devices. +// +// Should only be accessed while synchronized on the OWSReadReceiptManager. +@property (nonatomic, readonly) NSMutableDictionary *toLinkedDevicesReadReceiptMap; + +// Should only be accessed while synchronized on the OWSReadReceiptManager. +@property (nonatomic) BOOL isProcessing; + +@property (atomic) NSNumber *areReadReceiptsEnabledCached; + +@end + +#pragma mark - + +@implementation OWSReadReceiptManager + ++ (instancetype)sharedManager +{ + OWSAssert(SSKEnvironment.shared.readReceiptManager); + + return SSKEnvironment.shared.readReceiptManager; +} + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + + if (!self) { + return self; + } + + _dbConnection = primaryStorage.newDatabaseConnection; + + _toLinkedDevicesReadReceiptMap = [NSMutableDictionary new]; + + OWSSingletonAssert(); + + // Start processing. + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [self scheduleProcessing]; + }]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Dependencies + +- (SSKMessageSenderJobQueue *)messageSenderJobQueue +{ + return SSKEnvironment.shared.messageSenderJobQueue; +} + +- (OWSOutgoingReceiptManager *)outgoingReceiptManager +{ + OWSAssertDebug(SSKEnvironment.shared.outgoingReceiptManager); + + return SSKEnvironment.shared.outgoingReceiptManager; +} + +#pragma mark - + +// Schedules a processing pass, unless one is already scheduled. +- (void)scheduleProcessing +{ + OWSAssertDebug(AppReadiness.isAppReady); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + @synchronized(self) + { + if (self.isProcessing) { + return; + } + + self.isProcessing = YES; + + [self process]; + } + }); +} + +- (void)process +{ + @synchronized(self) + { + OWSLogVerbose(@"Processing read receipts."); + + NSArray *readReceiptsForLinkedDevices = + [self.toLinkedDevicesReadReceiptMap allValues]; + [self.toLinkedDevicesReadReceiptMap removeAllObjects]; + if (readReceiptsForLinkedDevices.count > 0) { + OWSReadReceiptsForLinkedDevicesMessage *message = + [[OWSReadReceiptsForLinkedDevicesMessage alloc] initWithReadReceipts:readReceiptsForLinkedDevices]; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self.messageSenderJobQueue addMessage:message transaction:transaction]; + }]; + } + + BOOL didWork = readReceiptsForLinkedDevices.count > 0; + + if (didWork) { + // Wait N seconds before processing read receipts again. + // This allows time for a batch to accumulate. + // + // We want a value high enough to allow us to effectively de-duplicate, + // read receipts without being so high that we risk not sending read + // receipts due to app exit. + const CGFloat kProcessingFrequencySeconds = 3.f; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kProcessingFrequencySeconds * NSEC_PER_SEC)), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + [self process]; + }); + } else { + self.isProcessing = NO; + } + } +} + +#pragma mark - Mark as Read Locally + +- (void)markAsReadLocallyBeforeSortId:(uint64_t)sortId thread:(TSThread *)thread +{ + OWSAssertDebug(thread); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self markAsReadBeforeSortId:sortId + thread:thread + readTimestamp:[NSDate ows_millisecondTimeStamp] + wasLocal:YES + transaction:transaction]; + }]; + }); +} + +- (void)messageWasReadLocally:(TSIncomingMessage *)message +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + @synchronized(self) + { + NSString *threadUniqueId = message.uniqueThreadId; + OWSAssertDebug(threadUniqueId.length > 0); + + NSString *messageAuthorId = message.authorId; + OWSAssertDebug(messageAuthorId.length > 0); + + OWSLinkedDeviceReadReceipt *newReadReceipt = + [[OWSLinkedDeviceReadReceipt alloc] initWithSenderId:messageAuthorId + messageIdTimestamp:message.timestamp + readTimestamp:[NSDate ows_millisecondTimeStamp]]; + + OWSLinkedDeviceReadReceipt *_Nullable oldReadReceipt = self.toLinkedDevicesReadReceiptMap[threadUniqueId]; + if (oldReadReceipt && oldReadReceipt.messageIdTimestamp > newReadReceipt.messageIdTimestamp) { + // If there's an existing "linked device" read receipt for the same thread with + // a newer timestamp, discard this "linked device" read receipt. + OWSLogVerbose(@"Ignoring redundant read receipt for linked devices."); + } else { + OWSLogVerbose(@"Enqueuing read receipt for linked devices."); + self.toLinkedDevicesReadReceiptMap[threadUniqueId] = newReadReceipt; + } + + if (![LKSessionMetaProtocol shouldSendReceiptInThread:message.thread]) { + return; + } + + if ([self areReadReceiptsEnabled]) { + OWSLogVerbose(@"Enqueuing read receipt for sender."); + [self.outgoingReceiptManager enqueueReadReceiptForEnvelope:messageAuthorId timestamp:message.timestamp]; + } + + [self scheduleProcessing]; + } + }); +} + +#pragma mark - Read Receipts From Recipient + +- (void)processReadReceiptsFromRecipientId:(NSString *)recipientId + sentTimestamps:(NSArray *)sentTimestamps + readTimestamp:(uint64_t)readTimestamp +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(sentTimestamps); + + if (![self areReadReceiptsEnabled]) { + OWSLogInfo(@"Ignoring incoming receipt message as read receipts are disabled."); + return; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSNumber *nsSentTimestamp in sentTimestamps) { + UInt64 sentTimestamp = [nsSentTimestamp unsignedLongLongValue]; + + NSArray *messages + = (NSArray *)[TSInteraction interactionsWithTimestamp:sentTimestamp + ofClass:[TSOutgoingMessage class] + withTransaction:transaction]; + if (messages.count > 1) { + OWSLogError(@"More than one matching message with timestamp: %llu.", sentTimestamp); + } + if (messages.count > 0) { + // TODO: We might also need to "mark as read by recipient" any older messages + // from us in that thread. Or maybe this state should hang on the thread? + for (TSOutgoingMessage *message in messages) { + [message updateWithReadRecipientId:recipientId + readTimestamp:readTimestamp + transaction:transaction]; + } + } else { + // Persist the read receipts so that we can apply them to outgoing messages + // that we learn about later through sync messages. + [TSRecipientReadReceipt addRecipientId:recipientId + sentTimestamp:sentTimestamp + readTimestamp:readTimestamp + transaction:transaction]; + } + } + }]; + }); +} + +- (void)applyEarlyReadReceiptsForOutgoingMessageFromLinkedDevice:(TSOutgoingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(message); + OWSAssertDebug(transaction); + + uint64_t sentTimestamp = message.timestamp; + NSDictionary *recipientMap = + [TSRecipientReadReceipt recipientMapForSentTimestamp:sentTimestamp transaction:transaction]; + if (!recipientMap) { + return; + } + OWSAssertDebug(recipientMap.count > 0); + for (NSString *recipientId in recipientMap) { + NSNumber *nsReadTimestamp = recipientMap[recipientId]; + OWSAssertDebug(nsReadTimestamp); + uint64_t readTimestamp = [nsReadTimestamp unsignedLongLongValue]; + + [message updateWithReadRecipientId:recipientId readTimestamp:readTimestamp transaction:transaction]; + } + [TSRecipientReadReceipt removeRecipientIdsForTimestamp:message.timestamp transaction:transaction]; +} + +#pragma mark - Linked Device Read Receipts + +- (void)applyEarlyReadReceiptsForIncomingMessage:(TSIncomingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(message); + OWSAssertDebug(transaction); + + NSString *senderId = message.authorId; + uint64_t timestamp = message.timestamp; + if (senderId.length < 1 || timestamp < 1) { + OWSFailDebug(@"Invalid incoming message: %@ %llu", senderId, timestamp); + return; + } + + OWSLinkedDeviceReadReceipt *_Nullable readReceipt = + [OWSLinkedDeviceReadReceipt findLinkedDeviceReadReceiptWithSenderId:senderId + messageIdTimestamp:timestamp + transaction:transaction]; + if (!readReceipt) { + return; + } + + [message markAsReadAtTimestamp:readReceipt.readTimestamp sendReadReceipt:NO transaction:transaction]; + [readReceipt removeWithTransaction:transaction]; +} + +- (void)processReadReceiptsFromLinkedDevice:(NSArray *)readReceiptProtos + readTimestamp:(uint64_t)readTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(readReceiptProtos); + OWSAssertDebug(transaction); + + for (SSKProtoSyncMessageRead *readReceiptProto in readReceiptProtos) { + NSString *_Nullable senderId = readReceiptProto.sender; + uint64_t messageIdTimestamp = readReceiptProto.timestamp; + + if (senderId.length == 0) { + OWSFailDebug(@"senderId was unexpectedly nil"); + continue; + } + + if (messageIdTimestamp == 0) { + OWSFailDebug(@"messageIdTimestamp was unexpectedly 0"); + continue; + } + + NSArray *messages + = (NSArray *)[TSInteraction interactionsWithTimestamp:messageIdTimestamp + ofClass:[TSIncomingMessage class] + withTransaction:transaction]; + if (messages.count > 0) { + for (TSIncomingMessage *message in messages) { + NSTimeInterval secondsSinceRead = [NSDate new].timeIntervalSince1970 - readTimestamp / 1000; + OWSAssertDebug([message isKindOfClass:[TSIncomingMessage class]]); + OWSLogDebug(@"read on linked device %f seconds ago", secondsSinceRead); + [self markAsReadOnLinkedDevice:message readTimestamp:readTimestamp transaction:transaction]; + } + } else { + // Received read receipt for unknown incoming message. + // Persist in case we receive the incoming message later. + OWSLinkedDeviceReadReceipt *readReceipt = + [[OWSLinkedDeviceReadReceipt alloc] initWithSenderId:senderId + messageIdTimestamp:messageIdTimestamp + readTimestamp:readTimestamp]; + [readReceipt saveWithTransaction:transaction]; + } + } +} + +- (void)markAsReadOnLinkedDevice:(TSIncomingMessage *)message + readTimestamp:(uint64_t)readTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(message); + OWSAssertDebug(transaction); + + // Always re-mark the message as read to ensure any earlier read time is applied to disappearing messages. + [message markAsReadAtTimestamp:readTimestamp sendReadReceipt:NO transaction:transaction]; + + // Also mark any unread messages appearing earlier in the thread as read as well. + [self markAsReadBeforeSortId:message.sortId + thread:[message threadWithTransaction:transaction] + readTimestamp:readTimestamp + wasLocal:NO + transaction:transaction]; +} + +#pragma mark - Mark As Read + +- (void)markAsReadBeforeSortId:(uint64_t)sortId + thread:(TSThread *)thread + readTimestamp:(uint64_t)readTimestamp + wasLocal:(BOOL)wasLocal + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(sortId > 0); + OWSAssertDebug(thread); + OWSAssertDebug(transaction); + + NSMutableArray> *newlyReadList = [NSMutableArray new]; + + [[TSDatabaseView unseenDatabaseViewExtension:transaction] + enumerateKeysAndObjectsInGroup:thread.uniqueId + usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) { + if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { + OWSFailDebug( + @"Expected to conform to OWSReadTracking: object with class: %@ collection: %@ " + @"key: %@", + [object class], + collection, + key); + return; + } + id possiblyRead = (id)object; + if (possiblyRead.sortId > sortId) { + *stop = YES; + return; + } + + // Under normal circumstances !possiblyRead.read should always evaluate to true at this point, but + // there is a bug that can somehow cause it to be false leading to conversations permanently being + // stuck with "unread" messages. + + OWSAssertDebug(possiblyRead.expireStartedAt == 0); + if (!possiblyRead.read) { + [newlyReadList addObject:possiblyRead]; + } + }]; + + if (newlyReadList.count < 1) { + return; + } + + if (wasLocal) { + OWSLogError(@"Marking %lu messages as read locally.", (unsigned long)newlyReadList.count); + } else { + OWSLogError(@"Marking %lu messages as read by linked device.", (unsigned long)newlyReadList.count); + } + for (id readItem in newlyReadList) { + [readItem markAsReadAtTimestamp:readTimestamp sendReadReceipt:wasLocal transaction:transaction]; + } +} + +#pragma mark - Settings + +- (void)prepareCachedValues +{ + [self areReadReceiptsEnabled]; +} + +- (BOOL)areReadReceiptsEnabled +{ + // We don't need to worry about races around this cached value. + if (!self.areReadReceiptsEnabledCached) { + self.areReadReceiptsEnabledCached = @([self.dbConnection boolForKey:OWSReadReceiptManagerAreReadReceiptsEnabled + inCollection:OWSReadReceiptManagerCollection + defaultValue:NO]); + } + + return [self.areReadReceiptsEnabledCached boolValue]; +} + +- (void)setAreReadReceiptsEnabled:(BOOL)value +{ + OWSLogInfo(@"setAreReadReceiptsEnabled: %d.", value); + + [self.dbConnection setBool:value + forKey:OWSReadReceiptManagerAreReadReceiptsEnabled + inCollection:OWSReadReceiptManagerCollection]; + + [SSKEnvironment.shared.syncManager sendConfigurationSyncMessage]; + + self.areReadReceiptsEnabledCached = @(value); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSReadReceiptsForLinkedDevicesMessage.h b/SignalUtilitiesKit/OWSReadReceiptsForLinkedDevicesMessage.h new file mode 100644 index 000000000..5e2165be7 --- /dev/null +++ b/SignalUtilitiesKit/OWSReadReceiptsForLinkedDevicesMessage.h @@ -0,0 +1,20 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSLinkedDeviceReadReceipt; + +@interface OWSReadReceiptsForLinkedDevicesMessage : OWSOutgoingSyncMessage + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithReadReceipts:(NSArray *)readReceipts NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSReadReceiptsForLinkedDevicesMessage.m b/SignalUtilitiesKit/OWSReadReceiptsForLinkedDevicesMessage.m new file mode 100644 index 000000000..72bc0dec8 --- /dev/null +++ b/SignalUtilitiesKit/OWSReadReceiptsForLinkedDevicesMessage.m @@ -0,0 +1,56 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSReadReceiptsForLinkedDevicesMessage.h" +#import "OWSLinkedDeviceReadReceipt.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSReadReceiptsForLinkedDevicesMessage () + +@property (nonatomic, readonly) NSArray *readReceipts; + +@end + +@implementation OWSReadReceiptsForLinkedDevicesMessage + +- (instancetype)initWithReadReceipts:(NSArray *)readReceipts +{ + self = [super init]; + if (!self) { + return self; + } + + _readReceipts = [readReceipts copy]; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder]; + for (OWSLinkedDeviceReadReceipt *readReceipt in self.readReceipts) { + SSKProtoSyncMessageReadBuilder *readProtoBuilder = + [SSKProtoSyncMessageRead builderWithSender:readReceipt.senderId timestamp:readReceipt.messageIdTimestamp]; + + NSError *error; + SSKProtoSyncMessageRead *_Nullable readProto = [readProtoBuilder buildAndReturnError:&error]; + if (error || !readProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + [syncMessageBuilder addRead:readProto]; + } + return syncMessageBuilder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSReadTracking.h b/SignalUtilitiesKit/OWSReadTracking.h new file mode 100644 index 000000000..aa7b90fd5 --- /dev/null +++ b/SignalUtilitiesKit/OWSReadTracking.h @@ -0,0 +1,38 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class YapDatabaseReadWriteTransaction; + +/** + * Some interactions track read/unread status. + * e.g. incoming messages and call notifications + */ +@protocol OWSReadTracking + +/** + * Has the local user seen the interaction? + */ +@property (nonatomic, readonly, getter=wasRead) BOOL read; + +@property (nonatomic, readonly) uint64_t expireStartedAt; +@property (nonatomic, readonly) uint64_t sortId; +@property (nonatomic, readonly) NSString *uniqueThreadId; + + +- (BOOL)shouldAffectUnreadCounts; + +/** + * Used both for *responding* to a remote read receipt and in response to the local user's activity. + */ +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSReceiptsForSenderMessage.h b/SignalUtilitiesKit/OWSReceiptsForSenderMessage.h new file mode 100644 index 000000000..df4108af1 --- /dev/null +++ b/SignalUtilitiesKit/OWSReceiptsForSenderMessage.h @@ -0,0 +1,33 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSDeliveryReceipt; + +@interface OWSReceiptsForSenderMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + ++ (OWSReceiptsForSenderMessage *)deliveryReceiptsForSenderMessageWithThread:(nullable TSThread *)thread + messageTimestamps:(NSArray *)messageTimestamps; + ++ (OWSReceiptsForSenderMessage *)readReceiptsForSenderMessageWithThread:(nullable TSThread *)thread + messageTimestamps:(NSArray *)messageTimestamps; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSReceiptsForSenderMessage.m b/SignalUtilitiesKit/OWSReceiptsForSenderMessage.m new file mode 100644 index 000000000..1466e8ee6 --- /dev/null +++ b/SignalUtilitiesKit/OWSReceiptsForSenderMessage.m @@ -0,0 +1,139 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSReceiptsForSenderMessage.h" +#import "SignalRecipient.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSReceiptsForSenderMessage () + +@property (nonatomic, readonly) NSArray *messageTimestamps; + +@property (nonatomic, readonly) SSKProtoReceiptMessageType receiptType; + +@end + +#pragma mark - + +@implementation OWSReceiptsForSenderMessage + ++ (OWSReceiptsForSenderMessage *)deliveryReceiptsForSenderMessageWithThread:(nullable TSThread *)thread + messageTimestamps:(NSArray *)messageTimestamps +{ + return [[OWSReceiptsForSenderMessage alloc] initWithThread:thread + messageTimestamps:messageTimestamps + receiptType:SSKProtoReceiptMessageTypeDelivery]; +} + ++ (OWSReceiptsForSenderMessage *)readReceiptsForSenderMessageWithThread:(nullable TSThread *)thread + messageTimestamps:(NSArray *)messageTimestamps +{ + return [[OWSReceiptsForSenderMessage alloc] initWithThread:thread + messageTimestamps:messageTimestamps + receiptType:SSKProtoReceiptMessageTypeRead]; +} + +- (instancetype)initWithThread:(nullable TSThread *)thread + messageTimestamps:(NSArray *)messageTimestamps + receiptType:(SSKProtoReceiptMessageType)receiptType +{ + // MJK TODO - remove senderTimestamp + self = [super initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + if (!self) { + return self; + } + + _messageTimestamps = [messageTimestamps copy]; + _receiptType = receiptType; + + return self; +} + +#pragma mark - TSOutgoingMessage overrides + +- (BOOL)shouldSyncTranscript +{ + return NO; +} + +- (BOOL)isSilent +{ + // Avoid "phantom messages" for "recipient read receipts". + + return YES; +} + +- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient +{ + OWSAssertDebug(recipient); + + SSKProtoReceiptMessage *_Nullable receiptMessage = [self buildReceiptMessage:recipient.recipientId]; + if (!receiptMessage) { + OWSFailDebug(@"could not build protobuf."); + return nil; + } + + SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder]; + [contentBuilder setReceiptMessage:receiptMessage]; + + NSError *error; + NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error]; + if (error || !contentData) { + OWSFailDebug(@"could not serialize protobuf: %@", error); + return nil; + } + return contentData; +} + +- (nullable SSKProtoReceiptMessage *)buildReceiptMessage:(NSString *)recipientId +{ + SSKProtoReceiptMessageBuilder *builder = [SSKProtoReceiptMessage builderWithType:self.receiptType]; + + OWSAssertDebug(self.messageTimestamps.count > 0); + for (NSNumber *messageTimestamp in self.messageTimestamps) { + [builder addTimestamp:[messageTimestamp unsignedLongLongValue]]; + } + + NSError *error; + SSKProtoReceiptMessage *_Nullable receiptMessage = [builder buildAndReturnError:&error]; + if (error || !receiptMessage) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + return receiptMessage; +} + +#pragma mark - TSYapDatabaseObject overrides + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (NSString *)debugDescription +{ + return [NSString + stringWithFormat:@"%@ with message timestamps: %lu", self.logTag, (unsigned long)self.messageTimestamps.count]; +} + +#pragma mark - Other + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeReceipt]; } + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRecipientIdentity.h b/SignalUtilitiesKit/OWSRecipientIdentity.h new file mode 100644 index 000000000..a209b3854 --- /dev/null +++ b/SignalUtilitiesKit/OWSRecipientIdentity.h @@ -0,0 +1,55 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, OWSVerificationState) { + OWSVerificationStateDefault, + OWSVerificationStateVerified, + OWSVerificationStateNoLongerVerified, +}; + +@class SSKProtoVerified; + +NSString *OWSVerificationStateToString(OWSVerificationState verificationState); +SSKProtoVerified *_Nullable BuildVerifiedProtoWithRecipientId(NSString *destinationRecipientId, + NSData *identityKey, + OWSVerificationState verificationState, + NSUInteger paddingBytesLength); + +@interface OWSRecipientIdentity : TSYapDatabaseObject + +@property (nonatomic, readonly) NSString *recipientId; +@property (nonatomic, readonly) NSData *identityKey; +@property (nonatomic, readonly) NSDate *createdAt; +@property (nonatomic, readonly) BOOL isFirstKnownKey; + +#pragma mark - Verification State + +@property (atomic, readonly) OWSVerificationState verificationState; + +- (void)updateWithVerificationState:(OWSVerificationState)verificationState + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark - Initializers + +- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId NS_UNAVAILABLE; + +- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithRecipientId:(NSString *)recipientId + identityKey:(NSData *)identityKey + isFirstKnownKey:(BOOL)isFirstKnownKey + createdAt:(NSDate *)createdAt + verificationState:(OWSVerificationState)verificationState NS_DESIGNATED_INITIALIZER; + +#pragma mark - debug + ++ (void)printAllIdentities; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRecipientIdentity.m b/SignalUtilitiesKit/OWSRecipientIdentity.m new file mode 100644 index 000000000..d37b99527 --- /dev/null +++ b/SignalUtilitiesKit/OWSRecipientIdentity.m @@ -0,0 +1,184 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSRecipientIdentity.h" +#import "OWSIdentityManager.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSPrimaryStorage.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *OWSVerificationStateToString(OWSVerificationState verificationState) +{ + switch (verificationState) { + case OWSVerificationStateDefault: + return @"OWSVerificationStateDefault"; + case OWSVerificationStateVerified: + return @"OWSVerificationStateVerified"; + case OWSVerificationStateNoLongerVerified: + return @"OWSVerificationStateNoLongerVerified"; + } +} + +SSKProtoVerifiedState OWSVerificationStateToProtoState(OWSVerificationState verificationState) +{ + switch (verificationState) { + case OWSVerificationStateDefault: + return SSKProtoVerifiedStateDefault; + case OWSVerificationStateVerified: + return SSKProtoVerifiedStateVerified; + case OWSVerificationStateNoLongerVerified: + return SSKProtoVerifiedStateUnverified; + } +} + +SSKProtoVerified *_Nullable BuildVerifiedProtoWithRecipientId(NSString *destinationRecipientId, + NSData *identityKey, + OWSVerificationState verificationState, + NSUInteger paddingBytesLength) +{ + OWSCAssertDebug(identityKey.length == kIdentityKeyLength); + OWSCAssertDebug(destinationRecipientId.length > 0); + // we only sync user's marking as un/verified. Never sync the conflicted state, the sibling device + // will figure that out on it's own. + OWSCAssertDebug(verificationState != OWSVerificationStateNoLongerVerified); + + SSKProtoVerifiedBuilder *verifiedBuilder = [SSKProtoVerified builderWithDestination:destinationRecipientId]; + verifiedBuilder.identityKey = identityKey; + verifiedBuilder.state = OWSVerificationStateToProtoState(verificationState); + + if (paddingBytesLength > 0) { + // We add the same amount of padding in the VerificationStateSync message and it's coresponding NullMessage so + // that the sync message is indistinguishable from an outgoing Sent transcript corresponding to the NullMessage. + // We pad the NullMessage so as to obscure it's content. The sync message (like all sync messages) will be + // *additionally* padded by the superclass while being sent. The end result is we send a NullMessage of a + // non-distinct size, and a verification sync which is ~1-512 bytes larger then that. + verifiedBuilder.nullMessage = [Cryptography generateRandomBytes:paddingBytesLength]; + } + + NSError *error; + SSKProtoVerified *_Nullable verifiedProto = [verifiedBuilder buildAndReturnError:&error]; + if (error || !verifiedProto) { + OWSCFailDebug(@"%@ could not build protobuf: %@", @"[BuildVerifiedProtoWithRecipientId]", error); + return nil; + } + return verifiedProto; +} + +@interface OWSRecipientIdentity () + +@property (atomic) OWSVerificationState verificationState; + +@end + +/** + * Record for a recipients identity key and some meta data around it used to make trust decisions. + * + * NOTE: Instances of this class MUST only be retrieved/persisted via it's internal `dbConnection`, + * which makes some special accomodations to enforce consistency. + */ +@implementation OWSRecipientIdentity + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + + if (self) { + if (![coder decodeObjectForKey:@"verificationState"]) { + _verificationState = OWSVerificationStateDefault; + } + } + + return self; +} + +- (instancetype)initWithRecipientId:(NSString *)recipientId + identityKey:(NSData *)identityKey + isFirstKnownKey:(BOOL)isFirstKnownKey + createdAt:(NSDate *)createdAt + verificationState:(OWSVerificationState)verificationState +{ + self = [super initWithUniqueId:recipientId]; + if (!self) { + return self; + } + + _recipientId = recipientId; + _identityKey = identityKey; + _isFirstKnownKey = isFirstKnownKey; + _createdAt = createdAt; + _verificationState = verificationState; + + return self; +} + +- (void)updateWithVerificationState:(OWSVerificationState)verificationState + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + // Ensure changes are persisted without clobbering any work done on another thread or instance. + [self updateWithChangeBlock:^(OWSRecipientIdentity *_Nonnull obj) { + obj.verificationState = verificationState; + } + transaction:transaction]; +} + +- (void)updateWithChangeBlock:(void (^)(OWSRecipientIdentity *obj))changeBlock + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + changeBlock(self); + + OWSRecipientIdentity *latest = [[self class] fetchObjectWithUniqueID:self.uniqueId transaction:transaction]; + if (latest == nil) { + [self saveWithTransaction:transaction]; + return; + } + + changeBlock(latest); + [latest saveWithTransaction:transaction]; +} + +- (void)updateWithChangeBlock:(void (^)(OWSRecipientIdentity *obj))changeBlock +{ + changeBlock(self); + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + OWSRecipientIdentity *latest = [[self class] fetchObjectWithUniqueID:self.uniqueId transaction:transaction]; + if (latest == nil) { + [self saveWithTransaction:transaction]; + return; + } + + changeBlock(latest); + [latest saveWithTransaction:transaction]; + }]; +} + +#pragma mark - debug + ++ (void)printAllIdentities +{ + OWSLogInfo(@"### All Recipient Identities ###"); + __block int count = 0; + [self enumerateCollectionObjectsUsingBlock:^(id obj, BOOL *stop) { + count++; + if (![obj isKindOfClass:[self class]]) { + OWSFailDebug(@"unexpected object in collection: %@", obj); + return; + } + OWSRecipientIdentity *recipientIdentity = (OWSRecipientIdentity *)obj; + + OWSLogInfo(@"Identity %d: %@", count, recipientIdentity.debugDescription); + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRecordTranscriptJob.h b/SignalUtilitiesKit/OWSRecordTranscriptJob.h new file mode 100644 index 000000000..11e5df9b0 --- /dev/null +++ b/SignalUtilitiesKit/OWSRecordTranscriptJob.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSIncomingSentMessageTranscript; +@class SSKProtoSyncMessageSentUpdate; +@class TSAttachmentStream; +@class YapDatabaseReadWriteTransaction; + +// This job is used to process "outgoing message" notifications from linked devices. +@interface OWSRecordTranscriptJob : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript + serverID:(uint64_t)serverID + serverTimestamp:(uint64_t)serverTimestamp + attachmentHandler:(void (^)( + NSArray *attachmentStreams))attachmentHandler + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRecordTranscriptJob.m b/SignalUtilitiesKit/OWSRecordTranscriptJob.m new file mode 100644 index 000000000..6308e76ca --- /dev/null +++ b/SignalUtilitiesKit/OWSRecordTranscriptJob.m @@ -0,0 +1,291 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSRecordTranscriptJob.h" +#import "OWSAttachmentDownloads.h" +#import "OWSDisappearingMessagesJob.h" +#import "OWSIncomingSentMessageTranscript.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSReadReceiptManager.h" +#import "SSKEnvironment.h" +#import "TSAttachmentPointer.h" +#import "TSGroupThread.h" +#import "TSInfoMessage.h" +#import "TSNetworkManager.h" +#import "TSOutgoingMessage.h" +#import "TSQuotedMessage.h" +#import "TSThread.h" +#import +#import "OWSPrimaryStorage+Loki.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSRecordTranscriptJob + +#pragma mark - Dependencies + ++ (OWSPrimaryStorage *)primaryStorage +{ + OWSAssertDebug(SSKEnvironment.shared.primaryStorage); + + return SSKEnvironment.shared.primaryStorage; +} + ++ (TSNetworkManager *)networkManager +{ + OWSAssertDebug(SSKEnvironment.shared.networkManager); + + return SSKEnvironment.shared.networkManager; +} + ++ (OWSReadReceiptManager *)readReceiptManager +{ + OWSAssert(SSKEnvironment.shared.readReceiptManager); + + return SSKEnvironment.shared.readReceiptManager; +} + ++ (id)contactsManager +{ + OWSAssertDebug(SSKEnvironment.shared.contactsManager); + + return SSKEnvironment.shared.contactsManager; +} + ++ (OWSAttachmentDownloads *)attachmentDownloads +{ + return SSKEnvironment.shared.attachmentDownloads; +} + +#pragma mark - + ++ (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)transcript + serverID:(uint64_t)serverID + serverTimestamp:(uint64_t)serverTimestamp + attachmentHandler:(void (^)( + NSArray *attachmentStreams))attachmentHandler + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transcript); + OWSAssertDebug(transaction); + + if (transcript.isRecipientUpdate) { + // "Recipient updates" are processed completely separately in order + // to avoid resurrecting threads or messages. + [self processRecipientUpdateWithTranscript:transcript transaction:transaction]; + return; + } + + OWSLogInfo(@"Recording transcript in thread: %@ timestamp: %llu", transcript.thread.uniqueId, transcript.timestamp); + + if (transcript.isEndSessionMessage) { + OWSLogInfo(@"EndSession was sent to recipient: %@.", transcript.recipientId); + [self.primaryStorage deleteAllSessionsForContact:transcript.recipientId protocolContext:transaction]; + + // MJK TODO - we don't use this timestamp, safe to remove + [[[TSInfoMessage alloc] initWithTimestamp:transcript.timestamp + inThread:transcript.thread + messageType:TSInfoMessageTypeSessionDidEnd] saveWithTransaction:transaction]; + + // Don't continue processing lest we print a bubble for the session reset. + return; + } + + TSOutgoingMessage *outgoingMessage = + [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:transcript.timestamp + inThread:transcript.thread + messageBody:transcript.body + attachmentIds:[NSMutableArray new] + expiresInSeconds:transcript.expirationDuration + expireStartedAt:transcript.expirationStartedAt + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:transcript.quotedMessage + contactShare:transcript.contact + linkPreview:transcript.linkPreview]; + + + if (transcript.thread.isGroupThread) { + TSGroupThread *thread = (TSGroupThread *)transcript.thread; + if (thread.isPublicChat) { + [outgoingMessage setServerTimestampToReceivedTimestamp:serverTimestamp]; + } + } + + if (serverID != 0) { + outgoingMessage.openGroupServerMessageID = serverID; + } + + NSArray *attachmentPointers = + [TSAttachmentPointer attachmentPointersFromProtos:transcript.attachmentPointerProtos + albumMessage:outgoingMessage]; + for (TSAttachmentPointer *pointer in attachmentPointers) { + [pointer saveWithTransaction:transaction]; + [outgoingMessage.attachmentIds addObject:pointer.uniqueId]; + } + + TSQuotedMessage *_Nullable quotedMessage = transcript.quotedMessage; + if (quotedMessage && quotedMessage.thumbnailAttachmentPointerId) { + // We weren't able to derive a local thumbnail, so we'll fetch the referenced attachment. + TSAttachmentPointer *attachmentPointer = + [TSAttachmentPointer fetchObjectWithUniqueID:quotedMessage.thumbnailAttachmentPointerId + transaction:transaction]; + + if ([attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) { + OWSLogDebug(@"downloading attachments for transcript: %lu", (unsigned long)transcript.timestamp); + + [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer + success:^(NSArray *attachmentStreams) { + OWSAssertDebug(attachmentStreams.count == 1); + TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [outgoingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream]; + [outgoingMessage saveWithTransaction:transaction]; + if (serverID != 0) { + [OWSPrimaryStorage.sharedManager setIDForMessageWithServerID:serverID to:outgoingMessage.uniqueId in:transaction]; + } + }]; + } + failure:^(NSError *error) { + OWSLogWarn(@"failed to fetch thumbnail for transcript: %lu with error: %@", + (unsigned long)transcript.timestamp, + error); + }]; + } + } + + [[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:outgoingMessage.expiresInSeconds + thread:transcript.thread + createdByRemoteRecipientId:nil + createdInExistingGroup:NO + transaction:transaction]; + + if (transcript.isExpirationTimerUpdate) { + // early return to avoid saving an empty incoming message. + OWSAssertDebug(transcript.body.length == 0); + OWSAssertDebug(outgoingMessage.attachmentIds.count == 0); + + return; + } + + if (outgoingMessage.body.length < 1 && outgoingMessage.attachmentIds.count < 1 && !outgoingMessage.contactShare) { + OWSFailDebug(@"Ignoring message transcript for empty message."); + return; + } + + [outgoingMessage saveWithTransaction:transaction]; + [outgoingMessage updateWithWasSentFromLinkedDeviceWithUDRecipientIds:transcript.udRecipientIds + nonUdRecipientIds:transcript.nonUdRecipientIds + isSentUpdate:NO + transaction:transaction]; + [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:outgoingMessage + expirationStartedAt:transcript.expirationStartedAt + transaction:transaction]; + [self.readReceiptManager applyEarlyReadReceiptsForOutgoingMessageFromLinkedDevice:outgoingMessage + transaction:transaction]; + + if (outgoingMessage.hasAttachments) { + [self.attachmentDownloads + downloadAttachmentsForMessage:outgoingMessage + transaction:transaction + success:attachmentHandler + failure:^(NSError *error) { + OWSLogError( + @"failed to fetch transcripts attachments for message: %@", outgoingMessage); + }]; + } +} + +#pragma mark - + ++ (void)processRecipientUpdateWithTranscript:(OWSIncomingSentMessageTranscript *)transcript + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transcript); + OWSAssertDebug(transaction); + + if (!AreRecipientUpdatesEnabled()) { + OWSFailDebug(@"Ignoring 'recipient update' transcript; disabled."); + return; + } + + if (transcript.udRecipientIds.count < 1 && transcript.nonUdRecipientIds.count < 1) { + OWSFailDebug(@"Ignoring empty 'recipient update' transcript."); + return; + } + + uint64_t timestamp = transcript.timestamp; + if (timestamp < 1) { + OWSFailDebug(@"'recipient update' transcript has invalid timestamp."); + return; + } + + if (!transcript.thread.isGroupThread) { + OWSFailDebug(@"'recipient update' has missing or invalid thread."); + return; + } + TSGroupThread *groupThread = (TSGroupThread *)transcript.thread; + NSData *groupId = groupThread.groupModel.groupId; + if (groupId.length < 1) { + OWSFailDebug(@"'recipient update' transcript has invalid groupId."); + return; + } + + NSArray *messages + = (NSArray *)[TSInteraction interactionsWithTimestamp:timestamp + ofClass:[TSOutgoingMessage class] + withTransaction:transaction]; + if (messages.count < 1) { + // This message may have disappeared. + OWSLogError(@"No matching message with timestamp: %llu.", timestamp); + return; + } + + BOOL messageFound = NO; + for (TSOutgoingMessage *message in messages) { + if (!message.isFromLinkedDevice) { + // isFromLinkedDevice isn't always set for very old linked messages, but: + // + // a) We should never receive a "sent update" for a very old message. + // b) It's safe to discard suspicious "sent updates." + continue; + } + TSThread *thread = [message threadWithTransaction:transaction]; + if (!thread.isGroupThread) { + continue; + } + TSGroupThread *groupThread = (TSGroupThread *)thread; + if (![groupThread.groupModel.groupId isEqual:groupId]) { + continue; + } + + if (!message.isFromLinkedDevice) { + OWSFailDebug(@"Ignoring 'recipient update' for message which was sent locally."); + continue; + } + + OWSLogInfo(@"Processing 'recipient update' transcript in thread: %@, timestamp: %llu, nonUdRecipientIds: %d, " + @"udRecipientIds: %d.", + thread.uniqueId, + timestamp, + (int)transcript.nonUdRecipientIds.count, + (int)transcript.udRecipientIds.count); + + [message updateWithWasSentFromLinkedDeviceWithUDRecipientIds:transcript.udRecipientIds + nonUdRecipientIds:transcript.nonUdRecipientIds + isSentUpdate:YES + transaction:transaction]; + + messageFound = YES; + } + + if (!messageFound) { + // This message may have disappeared. + OWSLogError(@"No matching message with timestamp: %llu.", timestamp); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRequestBuilder.h b/SignalUtilitiesKit/OWSRequestBuilder.h new file mode 100644 index 000000000..b4879dc55 --- /dev/null +++ b/SignalUtilitiesKit/OWSRequestBuilder.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSRequest; + +@interface OWSRequestBuilder : NSObject + ++ (TSRequest *)profileNameSetRequestWithEncryptedPaddedName:(nullable NSData *)encryptedPaddedName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRequestBuilder.m b/SignalUtilitiesKit/OWSRequestBuilder.m new file mode 100644 index 000000000..74830bff1 --- /dev/null +++ b/SignalUtilitiesKit/OWSRequestBuilder.m @@ -0,0 +1,43 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSRequestBuilder.h" +#import "TSConstants.h" +#import "TSRequest.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +const NSUInteger kEncodedNameLength = 72; + +@implementation OWSRequestBuilder + ++ (TSRequest *)profileNameSetRequestWithEncryptedPaddedName:(nullable NSData *)encryptedPaddedName +{ + NSString *urlString; + + NSString *base64EncodedName = [encryptedPaddedName base64EncodedString]; + // name length must match exactly + if (base64EncodedName.length == kEncodedNameLength) { + // Remove any "/" in the base64 (all other base64 chars are URL safe. + // Apples built-in `stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URL*]]` doesn't offer a + // flavor for encoding "/". + NSString *urlEncodedName = [base64EncodedName stringByReplacingOccurrencesOfString:@"/" withString:@"%2F"]; + urlString = [NSString stringWithFormat:textSecureSetProfileNameAPIFormat, urlEncodedName]; + } else { + // if name length doesn't match exactly, assume blank name + OWSAssertDebug(encryptedPaddedName == nil); + urlString = [NSString stringWithFormat:textSecureSetProfileNameAPIFormat, @""]; + } + + NSURL *url = [NSURL URLWithString:urlString]; + TSRequest *request = [[TSRequest alloc] initWithURL:url]; + request.HTTPMethod = @"PUT"; + + return request; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRequestFactory.h b/SignalUtilitiesKit/OWSRequestFactory.h new file mode 100644 index 000000000..b3c6a28c7 --- /dev/null +++ b/SignalUtilitiesKit/OWSRequestFactory.h @@ -0,0 +1,117 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ECKeyPair; +@class OWSDevice; +@class PreKeyRecord; +@class SMKUDAccessKey; +@class SignedPreKeyRecord; +@class TSRequest; + +typedef NS_ENUM(NSUInteger, TSVerificationTransport) { TSVerificationTransportVoice = 1, TSVerificationTransportSMS }; + +@interface OWSRequestFactory : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (TSRequest *)enable2FARequestWithPin:(NSString *)pin; + ++ (TSRequest *)disable2FARequest; + ++ (TSRequest *)acknowledgeMessageDeliveryRequestWithSource:(NSString *)source timestamp:(UInt64)timestamp; + ++ (TSRequest *)acknowledgeMessageDeliveryRequestWithServerGuid:(NSString *)serverGuid; + ++ (TSRequest *)deleteDeviceRequestWithDevice:(OWSDevice *)device; + ++ (TSRequest *)deviceProvisioningCodeRequest; + ++ (TSRequest *)deviceProvisioningRequestWithMessageBody:(NSData *)messageBody ephemeralDeviceId:(NSString *)deviceId; + ++ (TSRequest *)getDevicesRequest; + ++ (TSRequest *)getMessagesRequest; + ++ (TSRequest *)getProfileRequestWithRecipientId:(NSString *)recipientId + udAccessKey:(nullable SMKUDAccessKey *)udAccessKey + NS_SWIFT_NAME(getProfileRequest(recipientId:udAccessKey:)); + ++ (TSRequest *)turnServerInfoRequest; + ++ (TSRequest *)allocAttachmentRequest; + ++ (TSRequest *)attachmentRequestWithAttachmentId:(UInt64)attachmentId; + ++ (TSRequest *)contactsIntersectionRequestWithHashesArray:(NSArray *)hashes; + ++ (TSRequest *)profileAvatarUploadFormRequest; + ++ (TSRequest *)registerForPushRequestWithPushIdentifier:(NSString *)identifier voipIdentifier:(NSString *)voipId; + ++ (TSRequest *)updateAttributesRequest; + ++ (TSRequest *)unregisterAccountRequest; + ++ (TSRequest *)requestVerificationCodeRequestWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken + transport:(TSVerificationTransport)transport; + ++ (TSRequest *)submitMessageRequestWithRecipient:(NSString *)recipientId + messages:(NSArray *)messages + timeStamp:(uint64_t)timeStamp + udAccessKey:(nullable SMKUDAccessKey *)udAccessKey; + ++ (TSRequest *)verifyCodeRequestWithVerificationCode:(NSString *)verificationCode + forNumber:(NSString *)phoneNumber + pin:(nullable NSString *)pin + authKey:(NSString *)authKey; + +#pragma mark - Prekeys + ++ (TSRequest *)availablePreKeysCountRequest; + ++ (TSRequest *)currentSignedPreKeyRequest; + ++ (TSRequest *)recipientPrekeyRequestWithRecipient:(NSString *)recipientNumber + deviceId:(NSString *)deviceId + udAccessKey:(nullable SMKUDAccessKey *)udAccessKey; + ++ (TSRequest *)registerSignedPrekeyRequestWithSignedPreKeyRecord:(SignedPreKeyRecord *)signedPreKey; + ++ (TSRequest *)registerPrekeysRequestWithPrekeyArray:(NSArray *)prekeys + identityKey:(NSData *)identityKeyPublic + signedPreKey:(SignedPreKeyRecord *)signedPreKey; + +#pragma mark - CDS + ++ (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair + enclaveId:(NSString *)enclaveId + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword; + ++ (TSRequest *)enclaveContactDiscoveryRequestWithId:(NSData *)requestId + addressCount:(NSUInteger)addressCount + encryptedAddressData:(NSData *)encryptedAddressData + cryptIv:(NSData *)cryptIv + cryptMac:(NSData *)cryptMac + enclaveId:(NSString *)enclaveId + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword + cookies:(NSArray *)cookies; + ++ (TSRequest *)remoteAttestationAuthRequest; ++ (TSRequest *)cdsFeedbackRequestWithStatus:(NSString *)status + reason:(nullable NSString *)reason NS_SWIFT_NAME(cdsFeedbackRequest(status:reason:)); + +#pragma mark - UD + ++ (TSRequest *)udSenderCertificateRequest; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRequestFactory.m b/SignalUtilitiesKit/OWSRequestFactory.m new file mode 100644 index 000000000..3fbdd6fe8 --- /dev/null +++ b/SignalUtilitiesKit/OWSRequestFactory.m @@ -0,0 +1,543 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSRequestFactory.h" +#import "OWS2FAManager.h" +#import "OWSDevice.h" +#import "ProfileManagerProtocol.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSConstants.h" +#import "TSRequest.h" +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSRequestFactory + +#pragma mark - Dependencies + ++ (TSAccountManager *)tsAccountManager +{ + return TSAccountManager.sharedInstance; +} + ++ (OWS2FAManager *)ows2FAManager +{ + return OWS2FAManager.sharedManager; +} + ++ (id)profileManager +{ + return SSKEnvironment.shared.profileManager; +} + ++ (id)udManager +{ + return SSKEnvironment.shared.udManager; +} + +#pragma mark - + ++ (TSRequest *)enable2FARequestWithPin:(NSString *)pin +{ + OWSAssertDebug(pin.length > 0); + + return [TSRequest requestWithUrl:[NSURL URLWithString:textSecure2FAAPI] + method:@"PUT" + parameters:@{ + @"pin" : pin, + }]; +} + ++ (TSRequest *)disable2FARequest +{ + return [TSRequest requestWithUrl:[NSURL URLWithString:textSecure2FAAPI] method:@"DELETE" parameters:@{}]; +} + ++ (TSRequest *)acknowledgeMessageDeliveryRequestWithSource:(NSString *)source timestamp:(UInt64)timestamp +{ + OWSAssertDebug(timestamp > 0); + + NSString *path = [NSString stringWithFormat:@"v1/messages/%@/%llu", source, timestamp]; + + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"DELETE" parameters:@{}]; +} + ++ (TSRequest *)acknowledgeMessageDeliveryRequestWithServerGuid:(NSString *)serverGuid +{ + OWSAssertDebug(serverGuid.length > 0); + + NSString *path = [NSString stringWithFormat:@"v1/messages/uuid/%@", serverGuid]; + + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"DELETE" parameters:@{}]; +} + ++ (TSRequest *)deleteDeviceRequestWithDevice:(OWSDevice *)device +{ + OWSAssertDebug(device); + + NSString *path = [NSString stringWithFormat:textSecureDevicesAPIFormat, @(device.deviceId)]; + + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"DELETE" parameters:@{}]; +} + ++ (TSRequest *)deviceProvisioningCodeRequest +{ + return [TSRequest requestWithUrl:[NSURL URLWithString:textSecureDeviceProvisioningCodeAPI] + method:@"GET" + parameters:@{}]; +} + ++ (TSRequest *)deviceProvisioningRequestWithMessageBody:(NSData *)messageBody ephemeralDeviceId:(NSString *)deviceId +{ + OWSAssertDebug(messageBody.length > 0); + OWSAssertDebug(deviceId.length > 0); + + NSString *path = [NSString stringWithFormat:textSecureDeviceProvisioningAPIFormat, deviceId]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + @"body" : [messageBody base64EncodedString], + }]; +} + ++ (TSRequest *)getDevicesRequest +{ + NSString *path = [NSString stringWithFormat:textSecureDevicesAPIFormat, @""]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)getMessagesRequest +{ + return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/messages"] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)getProfileRequestWithRecipientId:(NSString *)recipientId + udAccessKey:(nullable SMKUDAccessKey *)udAccessKey +{ + OWSAssertDebug(recipientId.length > 0); + + NSString *path = [NSString stringWithFormat:textSecureProfileAPIFormat, recipientId]; + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; + if (udAccessKey != nil) { + [self useUDAuthWithRequest:request accessKey:udAccessKey]; + } + return request; +} + ++ (TSRequest *)turnServerInfoRequest +{ + return [TSRequest requestWithUrl:[NSURL URLWithString:@"v1/accounts/turn"] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)allocAttachmentRequest +{ + NSString *path = [NSString stringWithFormat:@"%@", textSecureAttachmentsAPI]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)attachmentRequestWithAttachmentId:(UInt64)attachmentId +{ + OWSAssertDebug(attachmentId > 0); + + NSString *path = [NSString stringWithFormat:@"%@/%llu", textSecureAttachmentsAPI, attachmentId]; + + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)availablePreKeysCountRequest +{ + NSString *path = [NSString stringWithFormat:@"%@", textSecureKeysAPI]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)contactsIntersectionRequestWithHashesArray:(NSArray *)hashes +{ + OWSAssertDebug(hashes.count > 0); + + NSString *path = [NSString stringWithFormat:@"%@/%@", textSecureDirectoryAPI, @"tokens"]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + @"contacts" : hashes, + }]; +} + ++ (TSRequest *)currentSignedPreKeyRequest +{ + NSString *path = textSecureSignedKeysAPI; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)profileAvatarUploadFormRequest +{ + NSString *path = textSecureProfileAvatarFormAPI; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)recipientPrekeyRequestWithRecipient:(NSString *)recipientNumber + deviceId:(NSString *)deviceId + udAccessKey:(nullable SMKUDAccessKey *)udAccessKey +{ + OWSAssertDebug(recipientNumber.length > 0); + OWSAssertDebug(deviceId.length > 0); + + NSString *path = [NSString stringWithFormat:@"%@/%@/%@", textSecureKeysAPI, recipientNumber, deviceId]; + + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; + if (udAccessKey != nil) { + [self useUDAuthWithRequest:request accessKey:udAccessKey]; + } + return request; +} + ++ (TSRequest *)registerForPushRequestWithPushIdentifier:(NSString *)identifier voipIdentifier:(NSString *)voipId +{ + OWSAssertDebug(identifier.length > 0); + OWSAssertDebug(voipId.length > 0); + + NSString *path = [NSString stringWithFormat:@"%@/%@", textSecureAccountsAPI, @"apn"]; + OWSAssertDebug(voipId); + return [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + @"apnRegistrationId" : identifier, + @"voipRegistrationId" : voipId ?: @"", + }]; +} + ++ (TSRequest *)updateAttributesRequest +{ + NSString *path = [textSecureAccountsAPI stringByAppendingString:textSecureAttributesAPI]; + + NSString *authKey = self.tsAccountManager.serverAuthToken; + NSString *_Nullable pin = [self.ows2FAManager pinCode]; + + NSDictionary *accountAttributes = [self accountAttributesWithPin:pin authKey:authKey]; + + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:accountAttributes]; +} + ++ (TSRequest *)unregisterAccountRequest +{ + NSString *path = [NSString stringWithFormat:@"%@/%@", textSecureAccountsAPI, @"apn"]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"DELETE" parameters:@{}]; +} + ++ (TSRequest *)requestVerificationCodeRequestWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken + transport:(TSVerificationTransport)transport +{ + OWSAssertDebug(phoneNumber.length > 0); + + NSString *querystring = @"client=ios"; + if (captchaToken.length > 0) { + querystring = [NSString stringWithFormat:@"%@&captcha=%@", querystring, captchaToken]; + } + + NSString *path = [NSString stringWithFormat:@"%@/%@/code/%@?%@", + textSecureAccountsAPI, + [self stringForTransport:transport], + phoneNumber, + querystring]; + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +// request.shouldHaveAuthorizationHeaders = NO; + + if (transport == TSVerificationTransportVoice) { + NSString *_Nullable localizationHeader = [self voiceCodeLocalizationHeader]; + if (localizationHeader.length > 0) { + [request setValue:localizationHeader forHTTPHeaderField:@"Accept-Language"]; + } + } + + return request; +} + ++ (nullable NSString *)voiceCodeLocalizationHeader +{ + NSLocale *locale = [NSLocale currentLocale]; + NSString *_Nullable languageCode = [locale objectForKey:NSLocaleLanguageCode]; + NSString *_Nullable countryCode = [locale objectForKey:NSLocaleCountryCode]; + + if (!languageCode) { + return nil; + } + + OWSAssertDebug([languageCode rangeOfString:@"-"].location == NSNotFound); + + if (!countryCode) { + // In the absence of a country code, just send a language code. + return languageCode; + } + + OWSAssertDebug(languageCode.length == 2); + OWSAssertDebug(countryCode.length == 2); + return [NSString stringWithFormat:@"%@-%@", languageCode, countryCode]; +} + ++ (NSString *)stringForTransport:(TSVerificationTransport)transport +{ + switch (transport) { + case TSVerificationTransportSMS: + return @"sms"; + case TSVerificationTransportVoice: + return @"voice"; + } +} + ++ (TSRequest *)verifyCodeRequestWithVerificationCode:(NSString *)verificationCode + forNumber:(NSString *)phoneNumber + pin:(nullable NSString *)pin + authKey:(NSString *)authKey +{ + OWSAssertDebug(verificationCode.length > 0); + OWSAssertDebug(phoneNumber.length > 0); + OWSAssertDebug(authKey.length > 0); + + NSString *path = [NSString stringWithFormat:@"%@/code/%@", textSecureAccountsAPI, verificationCode]; + + NSMutableDictionary *accountAttributes = + [[self accountAttributesWithPin:pin authKey:authKey] mutableCopy]; + [accountAttributes removeObjectForKey:@"AuthKey"]; + + TSRequest *request = + [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:accountAttributes]; + // The "verify code" request handles auth differently. +// request.authUsername = phoneNumber; +// request.authPassword = authKey; + return request; +} + ++ (NSDictionary *)accountAttributesWithPin:(nullable NSString *)pin + authKey:(NSString *)authKey +{ + uint32_t registrationId = [self.tsAccountManager getOrGenerateRegistrationId]; + + BOOL isManualMessageFetchEnabled = self.tsAccountManager.isManualMessageFetchEnabled; + + OWSAES256Key *profileKey = [self.profileManager localProfileKey]; + NSError *error; + SMKUDAccessKey *_Nullable udAccessKey = [[SMKUDAccessKey alloc] initWithProfileKey:profileKey.keyData error:&error]; + if (error || udAccessKey.keyData.length < 1) { + // Crash app if UD cannot be enabled. + OWSFail(@"Could not determine UD access key: %@.", error); + } + BOOL allowUnrestrictedUD = [self.udManager shouldAllowUnrestrictedAccessLocal] && udAccessKey != nil; + + // We no longer include the signalingKey. + NSMutableDictionary *accountAttributes = [@{ + @"AuthKey" : authKey, + @"voice" : @(YES), // all Signal-iOS clients support voice + @"video" : @(YES), // all Signal-iOS clients support WebRTC-based voice and video calls. + @"fetchesMessages" : @(isManualMessageFetchEnabled), // devices that don't support push must tell the server + // they fetch messages manually + @"registrationId" : [NSString stringWithFormat:@"%i", registrationId], + @"unidentifiedAccessKey" : udAccessKey.keyData.base64EncodedString, + @"unrestrictedUnidentifiedAccess" : @(allowUnrestrictedUD), + } mutableCopy]; + + if (pin.length > 0) { + accountAttributes[@"pin"] = pin; + } + + return [accountAttributes copy]; +} + ++ (TSRequest *)submitMessageRequestWithRecipient:(NSString *)recipientId + messages:(NSArray *)messages + timeStamp:(uint64_t)timeStamp + udAccessKey:(nullable SMKUDAccessKey *)udAccessKey +{ + // NOTE: messages may be empty; See comments in OWSDeviceManager. + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(timeStamp > 0); + + NSString *path = [textSecureMessagesAPI stringByAppendingString:recipientId]; + NSDictionary *parameters = @{ + @"messages" : messages, + @"timestamp" : @(timeStamp), + }; + + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:parameters]; + if (udAccessKey != nil) { + [self useUDAuthWithRequest:request accessKey:udAccessKey]; + } + return request; +} + ++ (TSRequest *)registerSignedPrekeyRequestWithSignedPreKeyRecord:(SignedPreKeyRecord *)signedPreKey +{ + OWSAssertDebug(signedPreKey); + + NSString *path = textSecureSignedKeysAPI; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:[self dictionaryFromSignedPreKey:signedPreKey]]; +} + ++ (TSRequest *)registerPrekeysRequestWithPrekeyArray:(NSArray *)prekeys + identityKey:(NSData *)identityKeyPublic + signedPreKey:(SignedPreKeyRecord *)signedPreKey +{ + OWSAssertDebug(prekeys.count > 0); + OWSAssertDebug(identityKeyPublic.length > 0); + OWSAssertDebug(signedPreKey); + + NSString *path = textSecureKeysAPI; + NSString *publicIdentityKey = [[identityKeyPublic prependKeyType] base64EncodedStringWithOptions:0]; + NSMutableArray *serializedPrekeyList = [NSMutableArray array]; + for (PreKeyRecord *preKey in prekeys) { + [serializedPrekeyList addObject:[self dictionaryFromPreKey:preKey]]; + } + return [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + @"preKeys" : serializedPrekeyList, + @"signedPreKey" : [self dictionaryFromSignedPreKey:signedPreKey], + @"identityKey" : publicIdentityKey + }]; +} + ++ (NSDictionary *)dictionaryFromPreKey:(PreKeyRecord *)preKey +{ + return @{ + @"keyId" : @(preKey.Id), + @"publicKey" : [[preKey.keyPair.publicKey prependKeyType] base64EncodedStringWithOptions:0], + }; +} + ++ (NSDictionary *)dictionaryFromSignedPreKey:(SignedPreKeyRecord *)preKey +{ + return @{ + @"keyId" : @(preKey.Id), + @"publicKey" : [[preKey.keyPair.publicKey prependKeyType] base64EncodedStringWithOptions:0], + @"signature" : [preKey.signature base64EncodedStringWithOptions:0] + }; +} + ++ (TSRequest *)remoteAttestationRequest:(ECKeyPair *)keyPair + enclaveId:(NSString *)enclaveId + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword +{ + OWSAssertDebug(keyPair); + OWSAssertDebug(enclaveId.length > 0); + OWSAssertDebug(authUsername.length > 0); + OWSAssertDebug(authPassword.length > 0); + + NSString *path = [NSString stringWithFormat:@"%@/v1/attestation/%@", contactDiscoveryURL, enclaveId]; + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + // We DO NOT prepend the "key type" byte. + @"clientPublic" : [keyPair.publicKey base64EncodedStringWithOptions:0], + }]; +// request.authUsername = authUsername; +// request.authPassword = authPassword; + + // Don't bother with the default cookie store; + // these cookies are ephemeral. + // + // NOTE: TSNetworkManager now separately disables default cookie handling for all requests. + [request setHTTPShouldHandleCookies:NO]; + + return request; +} + ++ (TSRequest *)enclaveContactDiscoveryRequestWithId:(NSData *)requestId + addressCount:(NSUInteger)addressCount + encryptedAddressData:(NSData *)encryptedAddressData + cryptIv:(NSData *)cryptIv + cryptMac:(NSData *)cryptMac + enclaveId:(NSString *)enclaveId + authUsername:(NSString *)authUsername + authPassword:(NSString *)authPassword + cookies:(NSArray *)cookies +{ + NSString *path = [NSString stringWithFormat:@"%@/v1/discovery/%@", contactDiscoveryURL, enclaveId]; + + TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] + method:@"PUT" + parameters:@{ + @"requestId" : requestId.base64EncodedString, + @"addressCount" : @(addressCount), + @"data" : encryptedAddressData.base64EncodedString, + @"iv" : cryptIv.base64EncodedString, + @"mac" : cryptMac.base64EncodedString, + }]; + +// request.authUsername = authUsername; +// request.authPassword = authPassword; + + // Don't bother with the default cookie store; + // these cookies are ephemeral. + // + // NOTE: TSNetworkManager now separately disables default cookie handling for all requests. + [request setHTTPShouldHandleCookies:NO]; + // Set the cookie header. + OWSAssertDebug(request.allHTTPHeaderFields.count == 0); + [request setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:cookies]]; + + return request; +} + ++ (TSRequest *)remoteAttestationAuthRequest +{ + NSString *path = @"/v1/directory/auth"; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (TSRequest *)cdsFeedbackRequestWithStatus:(NSString *)status + reason:(nullable NSString *)reason +{ + + NSDictionary *parameters; + if (reason == nil) { + parameters = @{}; + } else { + const NSUInteger kServerReasonLimit = 1000; + NSString *limitedReason; + if (reason.length < kServerReasonLimit) { + limitedReason = reason; + } else { + OWSFailDebug(@"failure: reason should be under 1000"); + limitedReason = [reason substringToIndex:kServerReasonLimit - 1]; + } + parameters = @{ @"reason": limitedReason }; + } + NSString *path = [NSString stringWithFormat:@"/v1/directory/feedback-v3/%@", status]; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"PUT" parameters:parameters]; +} + +#pragma mark - UD + ++ (TSRequest *)udSenderCertificateRequest +{ + NSString *path = @"/v1/certificate/delivery"; + return [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; +} + ++ (void)useUDAuthWithRequest:(TSRequest *)request accessKey:(SMKUDAccessKey *)udAccessKey +{ + OWSAssertDebug(request); + OWSAssertDebug(udAccessKey); + + // Suppress normal auth headers. +// request.shouldHaveAuthorizationHeaders = NO; + + // Add UD auth header. + [request setValue:[udAccessKey.keyData base64EncodedString] forHTTPHeaderField:@"Unidentified-Access-Key"]; + +// request.isUDRequest = YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSRequestMaker.swift b/SignalUtilitiesKit/OWSRequestMaker.swift new file mode 100644 index 000000000..f87707b02 --- /dev/null +++ b/SignalUtilitiesKit/OWSRequestMaker.swift @@ -0,0 +1,247 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +@objc +public enum RequestMakerUDAuthError: Int, Error { + case udAuthFailure +} + +public enum RequestMakerError: Error { + case websocketRequestError(statusCode : Int, responseData : Data?, underlyingError : Error) +} + +@objc(OWSRequestMakerResult) +public class RequestMakerResult: NSObject { + @objc + public let responseObject: Any? + + @objc + public let wasSentByUD: Bool + + @objc + public let wasSentByWebsocket: Bool + + @objc + public init(responseObject: Any?, + wasSentByUD: Bool, + wasSentByWebsocket: Bool) { + self.responseObject = responseObject + self.wasSentByUD = wasSentByUD + self.wasSentByWebsocket = wasSentByWebsocket + } +} + +// A utility class that handles: +// +// * UD auth-to-Non-UD auth failover. +// * Websocket-to-REST failover. +@objc(OWSRequestMaker) +public class RequestMaker: NSObject { + + public typealias RequestFactoryBlock = (SMKUDAccessKey?) -> TSRequest + public typealias UDAuthFailureBlock = () -> Void + public typealias WebsocketFailureBlock = () -> Void + + private let label: String + private let requestFactoryBlock: RequestFactoryBlock + private let udAuthFailureBlock: UDAuthFailureBlock + private let websocketFailureBlock: WebsocketFailureBlock + private let recipientId: String + private let udAccess: OWSUDAccess? + private let canFailoverUDAuth: Bool + + @objc + public init(label: String, + requestFactoryBlock : @escaping RequestFactoryBlock, + udAuthFailureBlock : @escaping UDAuthFailureBlock, + websocketFailureBlock : @escaping WebsocketFailureBlock, + recipientId: String, + udAccess: OWSUDAccess?, + canFailoverUDAuth: Bool) { + self.label = label + self.requestFactoryBlock = requestFactoryBlock + self.udAuthFailureBlock = udAuthFailureBlock + self.websocketFailureBlock = websocketFailureBlock + self.recipientId = recipientId + self.udAccess = udAccess + self.canFailoverUDAuth = canFailoverUDAuth + } + + // MARK: - Dependencies + + private var socketManager: TSSocketManager { + return SSKEnvironment.shared.socketManager + } + + private var networkManager: TSNetworkManager { + return SSKEnvironment.shared.networkManager + } + + private var udManager: OWSUDManager { + return SSKEnvironment.shared.udManager + } + + private var profileManager: ProfileManagerProtocol { + return SSKEnvironment.shared.profileManager + } + + // MARK: - + + @objc + public func makeRequestObjc() -> AnyPromise { + let promise = makeRequest() + .recover(on: DispatchQueue.global()) { (error: Error) -> Promise in + switch error { + case NetworkManagerError.taskError(_, let underlyingError): + throw underlyingError + default: + throw error + } + } + let anyPromise = AnyPromise(promise) + anyPromise.retainUntilComplete() + return anyPromise + } + + public func makeRequest() -> Promise { + return makeRequestInternal(skipUD: false, skipWebsocket: false) + } + + private func makeRequestInternal(skipUD: Bool, skipWebsocket: Bool) -> Promise { + var udAccessForRequest: OWSUDAccess? + if !skipUD { + udAccessForRequest = udAccess + } + let isUDRequest: Bool = udAccessForRequest != nil + let request: TSRequest = requestFactoryBlock(udAccessForRequest?.udAccessKey) + let canMakeWebsocketRequests = (socketManager.canMakeRequests() && !skipWebsocket && !isUDRequest) + + if canMakeWebsocketRequests { + return Promise { resolver in + socketManager.make(request, success: { (responseObject: Any?) in + if self.udManager.isUDVerboseLoggingEnabled() { + if isUDRequest { + Logger.debug("UD websocket request '\(self.label)' succeeded.") + } else { + Logger.debug("Non-UD websocket request '\(self.label)' succeeded.") + } + } + + self.requestSucceeded(udAccess: udAccessForRequest) + + resolver.fulfill(RequestMakerResult(responseObject: responseObject, + wasSentByUD: isUDRequest, + wasSentByWebsocket: true)) + }) { (statusCode: Int, responseData: Data?, error: Error) in + resolver.reject(RequestMakerError.websocketRequestError(statusCode: statusCode, responseData: responseData, underlyingError: error)) + } + }.recover { (error: Error) -> Promise in + switch error { + case RequestMakerError.websocketRequestError(let statusCode, _, _): + if isUDRequest && (statusCode == 401 || statusCode == 403) { + // If a UD request fails due to service response (as opposed to network + // failure), mark recipient as _not_ in UD mode, then retry. + self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId) + self.profileManager.fetchProfile(forRecipientId: self.recipientId) + self.udAuthFailureBlock() + + if self.canFailoverUDAuth { + Logger.info("UD websocket request '\(self.label)' auth failed; failing over to non-UD websocket request.") + return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + } else { + Logger.info("UD websocket request '\(self.label)' auth failed; aborting.") + throw RequestMakerUDAuthError.udAuthFailure + } + } + break + default: + break + } + + self.websocketFailureBlock() + if isUDRequest { + Logger.info("UD Web socket request '\(self.label)' failed; failing over to REST request: \(error).") + } else { + Logger.info("Non-UD Web socket request '\(self.label)' failed; failing over to REST request: \(error).") + } + return self.makeRequestInternal(skipUD: skipUD, skipWebsocket: true) + } + } else { + return self.networkManager.makePromise(request: request) + .map(on: DispatchQueue.global()) { (networkManagerResult: TSNetworkManager.NetworkManagerResult) -> RequestMakerResult in + if self.udManager.isUDVerboseLoggingEnabled() { + if isUDRequest { + Logger.debug("UD REST request '\(self.label)' succeeded.") + } else { + Logger.debug("Non-UD REST request '\(self.label)' succeeded.") + } + } + + self.requestSucceeded(udAccess: udAccessForRequest) + + // Unwrap the network manager promise into a request maker promise. + return RequestMakerResult(responseObject: networkManagerResult.responseObject, + wasSentByUD: isUDRequest, + wasSentByWebsocket: false) + }.recover { (error: Error) -> Promise in + switch error { + case NetworkManagerError.taskError(let task, _): + let statusCode = task.statusCode() + if isUDRequest && (statusCode == 401 || statusCode == 403) { + // If a UD request fails due to service response (as opposed to network + // failure), mark recipient as _not_ in UD mode, then retry. + self.udManager.setUnidentifiedAccessMode(.disabled, recipientId: self.recipientId) + self.profileManager.fetchProfile(forRecipientId: self.recipientId) + self.udAuthFailureBlock() + + if self.canFailoverUDAuth { + Logger.info("UD REST request '\(self.label)' auth failed; failing over to non-UD REST request.") + return self.makeRequestInternal(skipUD: true, skipWebsocket: skipWebsocket) + } else { + Logger.info("UD REST request '\(self.label)' auth failed; aborting.") + throw RequestMakerUDAuthError.udAuthFailure + } + } + break + default: + break + } + + if isUDRequest { + Logger.debug("UD REST request '\(self.label)' failed: \(error).") + } else { + Logger.debug("Non-UD REST request '\(self.label)' failed: \(error).") + } + throw error + } + } + } + + private func requestSucceeded(udAccess: OWSUDAccess?) { + // If this was a UD request... + guard let udAccess = udAccess else { + return + } + // ...made for a user in "unknown" UD access mode... + guard udAccess.udAccessMode == .unknown else { + return + } + + if udAccess.isRandomKey { + // If a UD request succeeds for an unknown user with a random key, + // mark recipient as .unrestricted. + udManager.setUnidentifiedAccessMode(.unrestricted, recipientId: recipientId) + } else { + // If a UD request succeeds for an unknown user with a non-random key, + // mark recipient as .enabled. + udManager.setUnidentifiedAccessMode(.enabled, recipientId: recipientId) + } + DispatchQueue.main.async { + self.profileManager.fetchProfile(forRecipientId: self.recipientId) + } + } +} diff --git a/SignalUtilitiesKit/OWSSignalAddress.swift b/SignalUtilitiesKit/OWSSignalAddress.swift new file mode 100644 index 000000000..ce04d6013 --- /dev/null +++ b/SignalUtilitiesKit/OWSSignalAddress.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public enum OWSSignalAddressError: Error { + case assertionError(description: String) +} + +@objc +public class OWSSignalAddress: NSObject { + @objc + public let recipientId: String + + @objc + public let deviceId: UInt + + // MARK: Initializers + + @objc public init(recipientId: String, deviceId: UInt) throws { + guard recipientId.count > 0 else { + throw OWSSignalAddressError.assertionError(description: "Invalid recipient id: \(deviceId)") + } + + guard deviceId > 0 else { + throw OWSSignalAddressError.assertionError(description: "Invalid device id: \(deviceId)") + } + + self.recipientId = recipientId + self.deviceId = deviceId + } +} diff --git a/SignalUtilitiesKit/OWSSignalService.h b/SignalUtilitiesKit/OWSSignalService.h new file mode 100644 index 000000000..32d4b9583 --- /dev/null +++ b/SignalUtilitiesKit/OWSSignalService.h @@ -0,0 +1,37 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange; + +@class AFHTTPSessionManager; +@class OWSPrimaryStorage; +@class TSAccountManager; + +@interface OWSSignalService : NSObject + +/// For uploading avatar assets. +@property (nonatomic, readonly) AFHTTPSessionManager *CDNSessionManager; + ++ (instancetype)sharedInstance; + +- (instancetype)init NS_UNAVAILABLE; + +#pragma mark - Censorship Circumvention + +@property (atomic, readonly) BOOL isCensorshipCircumventionActive; +@property (atomic, readonly) BOOL hasCensoredPhoneNumber; +@property (atomic) BOOL isCensorshipCircumventionManuallyActivated; +@property (atomic) BOOL isCensorshipCircumventionManuallyDisabled; +@property (atomic, nullable) NSString *manualCensorshipCircumventionCountryCode; + +/// For interacting with the Signal Service +- (AFHTTPSessionManager *)buildSignalServiceSessionManager; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSignalService.m b/SignalUtilitiesKit/OWSSignalService.m new file mode 100644 index 000000000..15d312f74 --- /dev/null +++ b/SignalUtilitiesKit/OWSSignalService.m @@ -0,0 +1,328 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSSignalService.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSCensorshipConfiguration.h" +#import "OWSError.h" +#import "OWSHTTPSecurityPolicy.h" +#import "OWSPrimaryStorage.h" +#import "TSAccountManager.h" +#import "TSConstants.h" +#import "YapDatabaseConnection+OWS.h" +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kOWSPrimaryStorage_OWSSignalService = @"kTSStorageManager_OWSSignalService"; +NSString *const kOWSPrimaryStorage_isCensorshipCircumventionManuallyActivated + = @"kTSStorageManager_isCensorshipCircumventionManuallyActivated"; +NSString *const kOWSPrimaryStorage_isCensorshipCircumventionManuallyDisabled + = @"kTSStorageManager_isCensorshipCircumventionManuallyDisabled"; +NSString *const kOWSPrimaryStorage_ManualCensorshipCircumventionDomain + = @"kTSStorageManager_ManualCensorshipCircumventionDomain"; +NSString *const kOWSPrimaryStorage_ManualCensorshipCircumventionCountryCode + = @"kTSStorageManager_ManualCensorshipCircumventionCountryCode"; + +NSString *const kNSNotificationName_IsCensorshipCircumventionActiveDidChange = + @"kNSNotificationName_IsCensorshipCircumventionActiveDidChange"; + +@interface OWSSignalService () + +@property (atomic) BOOL hasCensoredPhoneNumber; + +@property (atomic) BOOL isCensorshipCircumventionActive; + +@end + +#pragma mark - + +@implementation OWSSignalService + +@synthesize isCensorshipCircumventionActive = _isCensorshipCircumventionActive; + ++ (instancetype)sharedInstance +{ + static OWSSignalService *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] initDefault]; + }); + return sharedInstance; +} + +- (instancetype)initDefault +{ + self = [super init]; + if (!self) { + return self; + } + + [self observeNotifications]; + + [self updateHasCensoredPhoneNumber]; + [self updateIsCensorshipCircumventionActive]; + + OWSSingletonAssert(); + + return self; +} + +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(registrationStateDidChange:) + name:RegistrationStateDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(localNumberDidChange:) + name:kNSNotificationName_LocalNumberDidChange + object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)updateHasCensoredPhoneNumber +{ + NSString *localNumber = [TSAccountManager localNumber]; + + if (localNumber) { + self.hasCensoredPhoneNumber = [OWSCensorshipConfiguration isCensoredPhoneNumber:localNumber]; + } else { + OWSLogError(@"no known phone number to check for censorship."); + self.hasCensoredPhoneNumber = NO; + } + + [self updateIsCensorshipCircumventionActive]; +} + +- (BOOL)isCensorshipCircumventionManuallyActivated +{ + return + [[OWSPrimaryStorage dbReadConnection] boolForKey:kOWSPrimaryStorage_isCensorshipCircumventionManuallyActivated + inCollection:kOWSPrimaryStorage_OWSSignalService + defaultValue:NO]; +} + +- (void)setIsCensorshipCircumventionManuallyActivated:(BOOL)value +{ + [[OWSPrimaryStorage dbReadWriteConnection] setObject:@(value) + forKey:kOWSPrimaryStorage_isCensorshipCircumventionManuallyActivated + inCollection:kOWSPrimaryStorage_OWSSignalService]; + + [self updateIsCensorshipCircumventionActive]; +} + +- (BOOL)isCensorshipCircumventionManuallyDisabled +{ + return [[OWSPrimaryStorage dbReadConnection] boolForKey:kOWSPrimaryStorage_isCensorshipCircumventionManuallyDisabled + inCollection:kOWSPrimaryStorage_OWSSignalService + defaultValue:NO]; +} + +- (void)setIsCensorshipCircumventionManuallyDisabled:(BOOL)value +{ + [[OWSPrimaryStorage dbReadWriteConnection] setObject:@(value) + forKey:kOWSPrimaryStorage_isCensorshipCircumventionManuallyDisabled + inCollection:kOWSPrimaryStorage_OWSSignalService]; + + [self updateIsCensorshipCircumventionActive]; +} + + +- (void)updateIsCensorshipCircumventionActive +{ + if (self.isCensorshipCircumventionManuallyDisabled) { + self.isCensorshipCircumventionActive = NO; + } else if (self.isCensorshipCircumventionManuallyActivated) { + self.isCensorshipCircumventionActive = YES; + } else if (self.hasCensoredPhoneNumber) { + self.isCensorshipCircumventionActive = YES; + } else { + self.isCensorshipCircumventionActive = NO; + } +} + +- (void)setIsCensorshipCircumventionActive:(BOOL)isCensorshipCircumventionActive +{ + @synchronized(self) + { + if (_isCensorshipCircumventionActive == isCensorshipCircumventionActive) { + return; + } + + _isCensorshipCircumventionActive = isCensorshipCircumventionActive; + } + + [[NSNotificationCenter defaultCenter] + postNotificationNameAsync:kNSNotificationName_IsCensorshipCircumventionActiveDidChange + object:nil + userInfo:nil]; +} + +- (BOOL)isCensorshipCircumventionActive +{ + @synchronized(self) + { + return _isCensorshipCircumventionActive; + } +} + +- (AFHTTPSessionManager *)buildSignalServiceSessionManager +{ + if (self.isCensorshipCircumventionActive) { + OWSCensorshipConfiguration *censorshipConfiguration = [self buildCensorshipConfiguration]; + OWSLogInfo(@"using reflector HTTPSessionManager via: %@", censorshipConfiguration.domainFrontBaseURL); + return [self reflectorSignalServiceSessionManagerWithCensorshipConfiguration:censorshipConfiguration]; + } else { + return self.defaultSignalServiceSessionManager; + } +} + +- (AFHTTPSessionManager *)defaultSignalServiceSessionManager +{ + NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration; + AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration]; + AFSecurityPolicy *securityPolicy = AFSecurityPolicy.defaultPolicy; + // Snode to snode communication uses self-signed certificates but clients can safely ignore this + securityPolicy.allowInvalidCertificates = YES; + securityPolicy.validatesDomainName = NO; + sessionManager.securityPolicy = securityPolicy; + sessionManager.requestSerializer = [AFJSONRequestSerializer serializer]; + sessionManager.requestSerializer.HTTPShouldHandleCookies = NO; + sessionManager.responseSerializer = [AFJSONResponseSerializer serializerWithReadingOptions:NSJSONReadingAllowFragments]; + NSMutableSet *acceptableContentTypes = sessionManager.responseSerializer.acceptableContentTypes.mutableCopy; + [acceptableContentTypes addObject:@"text/plain"]; + sessionManager.responseSerializer.acceptableContentTypes = acceptableContentTypes; + return sessionManager; +} + +- (AFHTTPSessionManager *)reflectorSignalServiceSessionManagerWithCensorshipConfiguration: + (OWSCensorshipConfiguration *)censorshipConfiguration +{ + NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration; + AFHTTPSessionManager *sessionManager = + [[AFHTTPSessionManager alloc] initWithBaseURL:censorshipConfiguration.domainFrontBaseURL + sessionConfiguration:sessionConf]; + + sessionManager.securityPolicy = censorshipConfiguration.domainFrontSecurityPolicy; + + sessionManager.requestSerializer = [AFJSONRequestSerializer serializer]; + [sessionManager.requestSerializer setValue:censorshipConfiguration.signalServiceReflectorHost + forHTTPHeaderField:@"Host"]; + sessionManager.responseSerializer = [AFJSONResponseSerializer serializer]; + // Disable default cookie handling for all requests. + sessionManager.requestSerializer.HTTPShouldHandleCookies = NO; + + return sessionManager; +} + +#pragma mark - Profile Uploading + +- (AFHTTPSessionManager *)CDNSessionManager +{ + if (self.isCensorshipCircumventionActive) { + OWSCensorshipConfiguration *censorshipConfiguration = [self buildCensorshipConfiguration]; + OWSLogInfo(@"using reflector CDNSessionManager via: %@", censorshipConfiguration.domainFrontBaseURL); + return [self reflectorCDNSessionManagerWithCensorshipConfiguration:censorshipConfiguration]; + } else { + return self.defaultCDNSessionManager; + } +} + +- (AFHTTPSessionManager *)defaultCDNSessionManager +{ + NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration; + AFHTTPSessionManager *sessionManager = + [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:sessionConf]; + + sessionManager.securityPolicy = [OWSHTTPSecurityPolicy sharedPolicy]; + + // Default acceptable content headers are rejected by AWS + sessionManager.responseSerializer.acceptableContentTypes = nil; + + return sessionManager; +} + +- (AFHTTPSessionManager *)reflectorCDNSessionManagerWithCensorshipConfiguration: + (OWSCensorshipConfiguration *)censorshipConfiguration +{ + NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration; + + AFHTTPSessionManager *sessionManager = + [[AFHTTPSessionManager alloc] initWithBaseURL:censorshipConfiguration.domainFrontBaseURL + sessionConfiguration:sessionConf]; + + sessionManager.securityPolicy = censorshipConfiguration.domainFrontSecurityPolicy; + + sessionManager.requestSerializer = [AFJSONRequestSerializer serializer]; + [sessionManager.requestSerializer setValue:censorshipConfiguration.CDNReflectorHost forHTTPHeaderField:@"Host"]; + + sessionManager.responseSerializer = [AFJSONResponseSerializer serializer]; + + return sessionManager; +} + +#pragma mark - Events + +- (void)registrationStateDidChange:(NSNotification *)notification +{ + [self updateHasCensoredPhoneNumber]; +} + +- (void)localNumberDidChange:(NSNotification *)notification +{ + [self updateHasCensoredPhoneNumber]; +} + +#pragma mark - Manual Censorship Circumvention + +- (OWSCensorshipConfiguration *)buildCensorshipConfiguration +{ + OWSAssertDebug(self.isCensorshipCircumventionActive); + + if (self.isCensorshipCircumventionManuallyActivated) { + NSString *countryCode = self.manualCensorshipCircumventionCountryCode; + if (countryCode.length == 0) { + OWSFailDebug(@"manualCensorshipCircumventionCountryCode was unexpectedly 0"); + } + + OWSCensorshipConfiguration *configuration = + [OWSCensorshipConfiguration censorshipConfigurationWithCountryCode:countryCode]; + OWSAssertDebug(configuration); + + return configuration; + } + + OWSCensorshipConfiguration *_Nullable configuration = + [OWSCensorshipConfiguration censorshipConfigurationWithPhoneNumber:TSAccountManager.localNumber]; + if (configuration != nil) { + return configuration; + } + + return OWSCensorshipConfiguration.defaultConfiguration; +} + +- (nullable NSString *)manualCensorshipCircumventionCountryCode +{ + return + [[OWSPrimaryStorage dbReadConnection] objectForKey:kOWSPrimaryStorage_ManualCensorshipCircumventionCountryCode + inCollection:kOWSPrimaryStorage_OWSSignalService]; +} + +- (void)setManualCensorshipCircumventionCountryCode:(nullable NSString *)value +{ + [[OWSPrimaryStorage dbReadWriteConnection] setObject:value + forKey:kOWSPrimaryStorage_ManualCensorshipCircumventionCountryCode + inCollection:kOWSPrimaryStorage_OWSSignalService]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSStorage+Subclass.h b/SignalUtilitiesKit/OWSStorage+Subclass.h new file mode 100644 index 000000000..5808c2863 --- /dev/null +++ b/SignalUtilitiesKit/OWSStorage+Subclass.h @@ -0,0 +1,32 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class YapDatabase; + +@interface OWSStorage (Subclass) + +@property (atomic, nullable, readonly) YapDatabase *database; + +- (void)loadDatabase; + +- (void)runSyncRegistrations; +// completion will be invoked _off_ the main thread. +- (void)runAsyncRegistrationsWithCompletion:(void (^_Nonnull)(void))completion; + +- (BOOL)areAsyncRegistrationsComplete; +- (BOOL)areSyncRegistrationsComplete; + +- (NSString *)databaseFilePath; +- (NSString *)databaseFilePath_SHM; +- (NSString *)databaseFilePath_WAL; + +- (void)resetStorage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSStorage.h b/SignalUtilitiesKit/OWSStorage.h new file mode 100644 index 000000000..386794369 --- /dev/null +++ b/SignalUtilitiesKit/OWSStorage.h @@ -0,0 +1,116 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const StorageIsReadyNotification; + +@class YapDatabaseExtension; + +@protocol OWSDatabaseConnectionDelegate + +- (BOOL)areAllRegistrationsComplete; + +@end + +#pragma mark - + +@interface OWSDatabaseConnection : YapDatabaseConnection + +@property (atomic, weak) id delegate; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDatabase:(YapDatabase *)database + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +@end + +#pragma mark - + +@interface OWSDatabase : YapDatabase + +- (instancetype)init NS_UNAVAILABLE; + +- (id)initWithPath:(NSString *)inPath + serializer:(nullable YapDatabaseSerializer)inSerializer + deserializer:(YapDatabaseDeserializer)inDeserializer + options:(YapDatabaseOptions *)inOptions + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +@end + +#pragma mark - + +typedef void (^OWSStorageMigrationBlock)(void); + +@interface OWSStorage : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initStorage NS_DESIGNATED_INITIALIZER; + +// Returns YES if _ALL_ storage classes have completed both their +// sync _AND_ async view registrations. ++ (BOOL)isStorageReady; + +// This object can be used to filter database notifications. +@property (nonatomic, readonly, nullable) id dbNotificationObject; + +// migrationBlock will be invoked _off_ the main thread. ++ (void)registerExtensionsWithMigrationBlock:(OWSStorageMigrationBlock)migrationBlock; + +#ifdef DEBUG +- (void)closeStorageForTests; +#endif + ++ (void)resetAllStorage; + +- (YapDatabaseConnection *)newDatabaseConnection; + ++ (YapDatabaseOptions *)defaultDatabaseOptions; + +#pragma mark - Extension Registration + ++ (void)incrementVersionOfDatabaseExtension:(NSString *)extensionName; + +- (BOOL)registerExtension:(YapDatabaseExtension *)extension withName:(NSString *)extensionName; + +- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension withName:(NSString *)extensionName; +- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension + withName:(NSString *)extensionName + completion:(nullable dispatch_block_t)completion; + +- (nullable id)registeredExtension:(NSString *)extensionName; + +- (NSArray *)registeredExtensionNames; + +#pragma mark - + +- (unsigned long long)databaseFileSize; +- (unsigned long long)databaseWALFileSize; +- (unsigned long long)databaseSHMFileSize; + +- (YapDatabaseConnection *)registrationConnection; + +#pragma mark - Password + +/** + * Returns NO if: + * + * - Keychain is locked because device has just been restarted. + * - Password could not be retrieved because of a keychain error. + */ ++ (BOOL)isDatabasePasswordAccessible; + ++ (nullable NSData *)tryToLoadDatabaseLegacyPassphrase:(NSError **)errorHandle; ++ (void)removeLegacyPassphrase; + ++ (void)storeDatabaseCipherKeySpec:(NSData *)cipherKeySpecData; + +- (void)logFileSizes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSStorage.m b/SignalUtilitiesKit/OWSStorage.m new file mode 100644 index 000000000..8cb00e925 --- /dev/null +++ b/SignalUtilitiesKit/OWSStorage.m @@ -0,0 +1,940 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSStorage.h" +#import "AppContext.h" +#import "NSNotificationCenter+OWS.h" +#import "NSUserDefaults+OWS.h" +#import "OWSBackgroundTask.h" +#import "OWSFileSystem.h" +#import "OWSPrimaryStorage.h" +#import "OWSStorage+Subclass.h" +#import "TSAttachmentStream.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const StorageIsReadyNotification = @"StorageIsReadyNotification"; +NSString *const OWSResetStorageNotification = @"OWSResetStorageNotification"; + +static NSString *keychainService = @"TSKeyChainService"; +static NSString *keychainDBLegacyPassphrase = @"TSDatabasePass"; +static NSString *keychainDBCipherKeySpec = @"OWSDatabaseCipherKeySpec"; + +const NSUInteger kDatabasePasswordLength = 30; + +typedef NSData *_Nullable (^LoadDatabaseMetadataBlock)(NSError **_Nullable); +typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); + +NSString *const kNSUserDefaults_DatabaseExtensionVersionMap = @"kNSUserDefaults_DatabaseExtensionVersionMap"; + +#pragma mark - + +@interface YapDatabaseConnection () + +- (id)initWithDatabase:(YapDatabase *)database; + +@end + +#pragma mark - + +@implementation OWSDatabaseConnection + +- (id)initWithDatabase:(YapDatabase *)database delegate:(id)delegate +{ + self = [super initWithDatabase:database]; + + if (!self) { + return self; + } + + OWSAssertDebug(delegate); + + self.delegate = delegate; + + return self; +} + +// Assert that the database is in a ready state (specifically that any sync database +// view registrations have completed and any async registrations have been started) +// before creating write transactions. +// +// Creating write transactions before the _sync_ database views are registered +// causes YapDatabase to rebuild all of our database views, which is catastrophic. +// Specifically, it causes YDB's "view version" checks to fail. +- (void)readWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block +{ + id delegate = self.delegate; + OWSAssertDebug(delegate); + OWSAssertDebug(delegate.areAllRegistrationsComplete); + + OWSBackgroundTask *_Nullable backgroundTask = nil; + if (CurrentAppContext().isMainApp && !CurrentAppContext().isRunningTests) { + backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + } + [super readWriteWithBlock:block]; + backgroundTask = nil; +} + +- (void)asyncReadWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block +{ + [self asyncReadWriteWithBlock:block completionQueue:NULL completionBlock:NULL]; +} + +- (void)asyncReadWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block + completionBlock:(nullable dispatch_block_t)completionBlock +{ + [self asyncReadWriteWithBlock:block completionQueue:NULL completionBlock:completionBlock]; +} + +- (void)asyncReadWriteWithBlock:(void (^)(YapDatabaseReadWriteTransaction *transaction))block + completionQueue:(nullable dispatch_queue_t)completionQueue + completionBlock:(nullable dispatch_block_t)completionBlock +{ + id delegate = self.delegate; + OWSAssertDebug(delegate); + OWSAssertDebug(delegate.areAllRegistrationsComplete); + + __block OWSBackgroundTask *_Nullable backgroundTask = nil; + if (CurrentAppContext().isMainApp) { + backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + } + [super asyncReadWriteWithBlock:block completionQueue:completionQueue completionBlock:^{ + if (completionBlock) { + completionBlock(); + } + backgroundTask = nil; + }]; +} + +@end + +#pragma mark - + +// This class is only used in DEBUG builds. +@interface YapDatabase () + +- (void)addConnection:(YapDatabaseConnection *)connection; + +- (YapDatabaseConnection *)registrationConnection; + +@end + +#pragma mark - + +@interface OWSDatabase () + +@property (atomic, weak) id delegate; + +@end + +#pragma mark - + +@implementation OWSDatabase + +- (id)initWithPath:(NSString *)inPath + serializer:(nullable YapDatabaseSerializer)inSerializer + deserializer:(YapDatabaseDeserializer)inDeserializer + options:(YapDatabaseOptions *)inOptions + delegate:(id)delegate +{ + self = [super initWithPath:inPath serializer:inSerializer deserializer:inDeserializer options:inOptions]; + + if (!self) { + return self; + } + + OWSAssertDebug(delegate); + + self.delegate = delegate; + + return self; +} + +// This clobbers the superclass implementation to include asserts which +// ensure that the database is in a ready state before creating write transactions. +// +// See comments in OWSDatabaseConnection. +- (YapDatabaseConnection *)newConnection +{ + id delegate = self.delegate; + OWSAssertDebug(delegate); + + OWSDatabaseConnection *connection = [[OWSDatabaseConnection alloc] initWithDatabase:self delegate:delegate]; + [self addConnection:connection]; + return connection; +} + +- (YapDatabaseConnection *)registrationConnection +{ + YapDatabaseConnection *connection = [super registrationConnection]; + return connection; +} + +@end + +#pragma mark - + +@interface OWSUnknownDBObject : TSYapDatabaseObject + +@end + +#pragma mark - + +/** + * A default object to return when we can't deserialize an object from YapDB. This can prevent crashes when + * old objects linger after their definition file is removed. The danger is that, the objects can lay in wait + * until the next time a DB extension is added and we necessarily enumerate the entire DB. + */ +@implementation OWSUnknownDBObject + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + OWSFailDebug(@"Tried to save object from unknown collection"); + + return [super encodeWithCoder:aCoder]; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + return self; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSFailDebug(@"Tried to save unknown object"); + + // No-op. +} + +- (void)touchWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSFailDebug(@"Tried to touch unknown object"); + + // No-op. +} + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSFailDebug(@"Tried to remove unknown object"); + + // No-op. +} + +@end + +#pragma mark - + +@interface OWSUnarchiverDelegate : NSObject + +@end + +#pragma mark - + +@implementation OWSUnarchiverDelegate + +- (nullable Class)unarchiver:(NSKeyedUnarchiver *)unarchiver + cannotDecodeObjectOfClassName:(NSString *)name + originalClasses:(NSArray *)classNames +{ + if ([name isEqualToString:@"TSRecipient"]) { + OWSLogError(@"Could not decode object: %@", name); + } else { + NSLog(@"Could not decode object: %@", name); + } + return [OWSUnknownDBObject class]; +} + +@end + +#pragma mark - + +@interface OWSStorage () + +@property (atomic, nullable) YapDatabase *database; + +@property (nonatomic) NSMutableArray *extensionNames; + +@end + +#pragma mark - + +@implementation OWSStorage + +- (instancetype)initStorage +{ + self = [super init]; + + if (self) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(resetStorage) + name:OWSResetStorageNotification + object:nil]; + + self.extensionNames = [NSMutableArray new]; + } + + return self; +} + +- (void)dealloc +{ + // Surface memory leaks by logging the deallocation of this class. + OWSLogVerbose(@"Dealloc: %@", self.class); + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)loadDatabase +{ + if (![self tryToLoadDatabase]) { + // Failing to load the database is catastrophic. + // + // The best we can try to do is to discard the current database + // and behave like a clean install. + OWSFailDebug(@"Could not load database"); + + // Try to reset app by deleting all databases. + // + // TODO: Possibly clean up all app files. + // [OWSStorage deleteDatabaseFiles]; + + if (![self tryToLoadDatabase]) { + OWSFailDebug(@"Could not load database (second try)"); + + // Sleep to give analytics events time to be delivered. + [NSThread sleepForTimeInterval:15.0f]; + + OWSFail(@"Failed to initialize database."); + } + } +} + +- (nullable id)dbNotificationObject +{ + OWSAssertDebug(self.database); + + return self.database; +} + +- (BOOL)areAsyncRegistrationsComplete +{ + OWSAbstractMethod(); + + return NO; +} + +- (BOOL)areSyncRegistrationsComplete +{ + OWSAbstractMethod(); + + return NO; +} + +- (BOOL)areAllRegistrationsComplete +{ + return self.areSyncRegistrationsComplete && self.areAsyncRegistrationsComplete; +} + +- (void)runSyncRegistrations +{ + OWSAbstractMethod(); +} + +- (void)runAsyncRegistrationsWithCompletion:(void (^_Nonnull)(void))completion +{ + OWSAbstractMethod(); +} + ++ (void)registerExtensionsWithMigrationBlock:(OWSStorageMigrationBlock)migrationBlock +{ + OWSAssertDebug(migrationBlock); + + __block OWSBackgroundTask *_Nullable backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + + [OWSPrimaryStorage.sharedManager runSyncRegistrations]; + + [OWSPrimaryStorage.sharedManager runAsyncRegistrationsWithCompletion:^{ + OWSAssertDebug(self.isStorageReady); + + [self postRegistrationCompleteNotification]; + + migrationBlock(); + + backgroundTask = nil; + }]; +} + +- (YapDatabaseConnection *)registrationConnection +{ + return self.database.registrationConnection; +} + +// Returns YES IFF all registrations are complete. ++ (void)postRegistrationCompleteNotification +{ + OWSAssertDebug(self.isStorageReady); + + OWSLogInfo(@""); + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:StorageIsReadyNotification + object:nil + userInfo:nil]; + }); +} + ++ (BOOL)isStorageReady +{ + return OWSPrimaryStorage.sharedManager.areAllRegistrationsComplete; +} + ++ (YapDatabaseOptions *)defaultDatabaseOptions +{ + YapDatabaseOptions *options = [[YapDatabaseOptions alloc] init]; + options.corruptAction = YapDatabaseCorruptAction_Fail; + options.enableMultiProcessSupport = YES; + + // We leave a portion of the header decrypted so that iOS will recognize the file + // as a SQLite database. Otherwise, because the database lives in a shared data container, + // and our usage of sqlite's write-ahead logging retains a lock on the database, the OS + // would kill the app/share extension as soon as it is backgrounded. + options.cipherUnencryptedHeaderLength = kSqliteHeaderLength; + + // If we want to migrate to the new cipher defaults in SQLCipher4+ we'll need to do a one time + // migration. See the `PRAGMA cipher_migrate` documentation for details. + // https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_migrate + options.legacyCipherCompatibilityVersion = 3; + + // If any of these asserts fails, we need to verify and update + // OWSDatabaseConverter which assumes the values of these options. + OWSAssertDebug(options.cipherDefaultkdfIterNumber == 0); + OWSAssertDebug(options.kdfIterNumber == 0); + OWSAssertDebug(options.cipherPageSize == 0); + OWSAssertDebug(options.pragmaPageSize == 0); + OWSAssertDebug(options.pragmaJournalSizeLimit == 0); + OWSAssertDebug(options.pragmaMMapSize == 0); + + return options; +} + +- (BOOL)tryToLoadDatabase +{ + __weak OWSStorage *weakSelf = self; + YapDatabaseOptions *options = [self.class defaultDatabaseOptions]; + options.cipherKeySpecBlock = ^{ + // NOTE: It's critical that we don't capture a reference to self + // (e.g. by using OWSAssertDebug()) or this database will contain a + // circular reference and will leak. + OWSStorage *strongSelf = weakSelf; + OWSCAssertDebug(strongSelf); + + // Rather than compute this once and capture the value of the key + // in the closure, we prefer to fetch the key from the keychain multiple times + // in order to keep the key out of application memory. + NSData *databaseKeySpec = [strongSelf databaseKeySpec]; + OWSCAssertDebug(databaseKeySpec.length == kSQLCipherKeySpecLength); + return databaseKeySpec; + }; + + // Sanity checking elsewhere asserts we should only regenerate key specs when + // there is no existing database, so rather than lazily generate in the cipherKeySpecBlock + // we must ensure the keyspec exists before we create the database. + [self ensureDatabaseKeySpecExists]; + + OWSDatabase *database = [[OWSDatabase alloc] initWithPath:[self databaseFilePath] + serializer:nil + deserializer:[[self class] logOnFailureDeserializer] + options:options + delegate:self]; + + if (!database) { + return NO; + } + + _database = database; + + return YES; +} + +/** + * NSCoding sometimes throws exceptions killing our app. We want to log that exception. + **/ ++ (YapDatabaseDeserializer)logOnFailureDeserializer +{ + OWSUnarchiverDelegate *unarchiverDelegate = [OWSUnarchiverDelegate new]; + + return ^id(NSString __unused *collection, NSString __unused *key, NSData *data) { + if (!data || data.length <= 0) { + OWSFailDebug(@"can't deserialize null object: %@", collection); + return [OWSUnknownDBObject new]; + } + + @try { + NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; + unarchiver.delegate = unarchiverDelegate; + return [unarchiver decodeObjectForKey:@"root"]; + } @catch (NSException *exception) { + // Sync log in case we bail + @throw exception; + } + }; +} + +- (YapDatabaseConnection *)newDatabaseConnection +{ + YapDatabaseConnection *dbConnection = self.database.newConnection; + if (!dbConnection) { + OWSFail(@"Storage could not open new database connection."); + } + return dbConnection; +} + +#pragma mark - Extension Registration + ++ (void)incrementVersionOfDatabaseExtension:(NSString *)extensionName +{ + OWSLogError(@"%@", extensionName); + + // Don't increment version of a given extension more than once + // per launch. + static NSMutableSet *incrementedViewSet = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + incrementedViewSet = [NSMutableSet new]; + }); + @synchronized(incrementedViewSet) { + if ([incrementedViewSet containsObject:extensionName]) { + OWSLogInfo(@"Ignoring redundant increment: %@", extensionName); + return; + } + [incrementedViewSet addObject:extensionName]; + } + + NSUserDefaults *appUserDefaults = [NSUserDefaults appUserDefaults]; + OWSAssertDebug(appUserDefaults); + NSMutableDictionary *_Nullable versionMap = + [[appUserDefaults valueForKey:kNSUserDefaults_DatabaseExtensionVersionMap] mutableCopy]; + if (!versionMap) { + versionMap = [NSMutableDictionary new]; + } + NSNumber *_Nullable versionSuffix = versionMap[extensionName]; + versionMap[extensionName] = @(versionSuffix.intValue + 1); + [appUserDefaults setValue:versionMap forKey:kNSUserDefaults_DatabaseExtensionVersionMap]; + [appUserDefaults synchronize]; +} + +- (nullable NSString *)appendSuffixToDatabaseExtensionVersionIfNecessary:(nullable NSString *)versionTag + extensionName:(NSString *)extensionName +{ + OWSAssertIsOnMainThread(); + + NSUserDefaults *appUserDefaults = [NSUserDefaults appUserDefaults]; + OWSAssertDebug(appUserDefaults); + NSDictionary *_Nullable versionMap = + [appUserDefaults valueForKey:kNSUserDefaults_DatabaseExtensionVersionMap]; + NSNumber *_Nullable versionSuffix = versionMap[extensionName]; + + if (versionSuffix) { + NSString *result = + [NSString stringWithFormat:@"%@.%@", (versionTag.length < 1 ? @"0" : versionTag), versionSuffix]; + OWSLogWarn(@"database extension version: %@ + %@ -> %@", versionTag, versionSuffix, result); + return result; + } + return versionTag; +} + +- (YapDatabaseExtension *)updateExtensionVersion:(YapDatabaseExtension *)extension withName:(NSString *)extensionName +{ + OWSAssertDebug(extension); + OWSAssertDebug(extensionName.length > 0); + + if ([extension isKindOfClass:[YapDatabaseAutoView class]]) { + YapDatabaseAutoView *databaseView = (YapDatabaseAutoView *)extension; + YapDatabaseAutoView *databaseViewCopy = [[YapDatabaseAutoView alloc] + initWithGrouping:databaseView.grouping + sorting:databaseView.sorting + versionTag:[self appendSuffixToDatabaseExtensionVersionIfNecessary:databaseView.versionTag + extensionName:extensionName] + options:databaseView.options]; + return databaseViewCopy; + } else if ([extension isKindOfClass:[YapDatabaseSecondaryIndex class]]) { + YapDatabaseSecondaryIndex *secondaryIndex = (YapDatabaseSecondaryIndex *)extension; + OWSAssertDebug(secondaryIndex->setup); + OWSAssertDebug(secondaryIndex->handler); + YapDatabaseSecondaryIndex *secondaryIndexCopy = [[YapDatabaseSecondaryIndex alloc] + initWithSetup:secondaryIndex->setup + handler:secondaryIndex->handler + versionTag:[self appendSuffixToDatabaseExtensionVersionIfNecessary:secondaryIndex.versionTag + extensionName:extensionName] + options:secondaryIndex->options]; + return secondaryIndexCopy; + } else if ([extension isKindOfClass:[YapDatabaseFullTextSearch class]]) { + YapDatabaseFullTextSearch *fullTextSearch = (YapDatabaseFullTextSearch *)extension; + + NSString *versionTag = [self appendSuffixToDatabaseExtensionVersionIfNecessary:fullTextSearch.versionTag extensionName:extensionName]; + YapDatabaseFullTextSearch *fullTextSearchCopy = + [[YapDatabaseFullTextSearch alloc] initWithColumnNames:fullTextSearch->columnNames.array + options:fullTextSearch->options + handler:fullTextSearch->handler + ftsVersion:fullTextSearch->ftsVersion + versionTag:versionTag]; + + return fullTextSearchCopy; + } else if ([extension isKindOfClass:[YapDatabaseCrossProcessNotification class]]) { + // versionTag doesn't matter for YapDatabaseCrossProcessNotification. + return extension; + } else { + // This method needs to be able to update the versionTag of all extensions. + // If we start using other extension types, we need to modify this method to + // handle them as well. + OWSFailDebug(@"Unknown extension type: %@", [extension class]); + + return extension; + } +} + +- (BOOL)registerExtension:(YapDatabaseExtension *)extension withName:(NSString *)extensionName +{ + extension = [self updateExtensionVersion:extension withName:extensionName]; + + OWSAssertDebug(![self.extensionNames containsObject:extensionName]); + [self.extensionNames addObject:extensionName]; + + return [self.database registerExtension:extension withName:extensionName]; +} + +- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension + withName:(NSString *)extensionName +{ + [self asyncRegisterExtension:extension withName:extensionName completion:nil]; +} + +- (void)asyncRegisterExtension:(YapDatabaseExtension *)extension + withName:(NSString *)extensionName + completion:(nullable dispatch_block_t)completion +{ + extension = [self updateExtensionVersion:extension withName:extensionName]; + + OWSAssertDebug(![self.extensionNames containsObject:extensionName]); + [self.extensionNames addObject:extensionName]; + + [self.database asyncRegisterExtension:extension + withName:extensionName + completionBlock:^(BOOL ready) { + if (!ready) { + OWSFailDebug(@"asyncRegisterExtension failed: %@", extensionName); + } else { + OWSLogVerbose(@"asyncRegisterExtension succeeded: %@", extensionName); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (completion) { + completion(); + } + }); + }]; +} + +- (nullable id)registeredExtension:(NSString *)extensionName +{ + return [self.database registeredExtension:extensionName]; +} + +- (NSArray *)registeredExtensionNames +{ + return [self.extensionNames copy]; +} + +#pragma mark - Password + ++ (void)deleteDatabaseFiles +{ + [OWSFileSystem deleteFile:[OWSPrimaryStorage legacyDatabaseFilePath]]; + [OWSFileSystem deleteFile:[OWSPrimaryStorage legacyDatabaseFilePath_SHM]]; + [OWSFileSystem deleteFile:[OWSPrimaryStorage legacyDatabaseFilePath_WAL]]; + [OWSFileSystem deleteFile:[OWSPrimaryStorage sharedDataDatabaseFilePath]]; + [OWSFileSystem deleteFile:[OWSPrimaryStorage sharedDataDatabaseFilePath_SHM]]; + [OWSFileSystem deleteFile:[OWSPrimaryStorage sharedDataDatabaseFilePath_WAL]]; +} + +- (void)closeStorageForTests +{ + [self resetStorage]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)resetStorage +{ + self.database = nil; + + [OWSStorage deleteDatabaseFiles]; +} + ++ (void)resetAllStorage +{ + [[NSNotificationCenter defaultCenter] postNotificationName:OWSResetStorageNotification object:nil]; + + // This might be redundant but in the spirit of thoroughness... + [self deleteDatabaseFiles]; + + [self deleteDBKeys]; + + if (CurrentAppContext().isMainApp) { + [TSAttachmentStream deleteAttachments]; + } + + // TODO: Delete Profiles on Disk? +} + +#pragma mark - Password + +- (NSString *)databaseFilePath +{ + OWSAbstractMethod(); + + return @""; +} + +- (NSString *)databaseFilePath_SHM +{ + OWSAbstractMethod(); + + return @""; +} + +- (NSString *)databaseFilePath_WAL +{ + OWSAbstractMethod(); + + return @""; +} + +#pragma mark - Keychain + ++ (BOOL)isDatabasePasswordAccessible +{ + NSError *error; + NSData *cipherKeySpec = [self tryToLoadDatabaseCipherKeySpec:&error]; + + if (cipherKeySpec && !error) { + return YES; + } + + if (error) { + OWSLogWarn(@"Database key couldn't be accessed: %@", error.localizedDescription); + } + + return NO; +} + ++ (nullable NSData *)tryToLoadDatabaseLegacyPassphrase:(NSError **)errorHandle +{ + return [self tryToLoadKeyChainValue:keychainDBLegacyPassphrase errorHandle:errorHandle]; +} + ++ (nullable NSData *)tryToLoadDatabaseCipherKeySpec:(NSError **)errorHandle +{ + NSData *_Nullable data = [self tryToLoadKeyChainValue:keychainDBCipherKeySpec errorHandle:errorHandle]; + OWSAssertDebug(!data || data.length == kSQLCipherKeySpecLength); + + return data; +} + ++ (void)storeDatabaseCipherKeySpec:(NSData *)cipherKeySpecData +{ + OWSAssertDebug(cipherKeySpecData.length == kSQLCipherKeySpecLength); + + [self storeKeyChainValue:cipherKeySpecData keychainKey:keychainDBCipherKeySpec]; +} + ++ (void)removeLegacyPassphrase +{ + OWSLogInfo(@"removing legacy passphrase"); + + NSError *_Nullable error; + BOOL result = [CurrentAppContext().keychainStorage removeWithService:keychainService + key:keychainDBLegacyPassphrase + error:&error]; + if (error || !result) { + OWSFailDebug(@"could not remove legacy passphrase."); + } +} + +- (void)ensureDatabaseKeySpecExists +{ + NSError *error; + NSData *_Nullable keySpec = [[self class] tryToLoadDatabaseCipherKeySpec:&error]; + + if (error || (keySpec.length != kSQLCipherKeySpecLength)) { + // Because we use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + // the keychain will be inaccessible after device restart until + // device is unlocked for the first time. If the app receives + // a push notification, we won't be able to access the keychain to + // process that notification, so we should just terminate by throwing + // an uncaught exception. + NSString *errorDescription = [NSString + stringWithFormat:@"CipherKeySpec inaccessible. New install or no unlock since device restart? Error: %@", + error]; + if (CurrentAppContext().isMainApp) { + UIApplicationState applicationState = CurrentAppContext().reportedApplicationState; + errorDescription = [errorDescription + stringByAppendingFormat:@", ApplicationState: %@", NSStringForUIApplicationState(applicationState)]; + } + OWSLogError(@"%@", errorDescription); + [DDLog flushLog]; + + if (CurrentAppContext().isMainApp) { + if (CurrentAppContext().isInBackground) { + // Rather than crash here, we should have already detected the situation earlier + // and exited gracefully (in the app delegate) using isDatabasePasswordAccessible. + // This is a last ditch effort to avoid blowing away the user's database. + [self raiseKeySpecInaccessibleExceptionWithErrorDescription:errorDescription]; + } + } else { + [self raiseKeySpecInaccessibleExceptionWithErrorDescription:@"CipherKeySpec inaccessible; not main app."]; + } + + // At this point, either this is a new install so there's no existing password to retrieve + // or the keychain has become corrupt. Either way, we want to get back to a + // "known good state" and behave like a new install. + BOOL doesDBExist = [NSFileManager.defaultManager fileExistsAtPath:[self databaseFilePath]]; + if (doesDBExist) { + OWSFailDebug(@"Could not load database metadata"); + } + + if (!CurrentAppContext().isRunningTests) { + // Try to reset app by deleting database. + [OWSStorage resetAllStorage]; + } + + keySpec = [Randomness generateRandomBytes:(int)kSQLCipherKeySpecLength]; + [[self class] storeDatabaseCipherKeySpec:keySpec]; + } +} + +- (NSData *)databaseKeySpec +{ + NSError *error; + NSData *_Nullable keySpec = [[self class] tryToLoadDatabaseCipherKeySpec:&error]; + + if (error) { + OWSLogError(@"failed to fetch databaseKeySpec with error: %@", error); + [self raiseKeySpecInaccessibleExceptionWithErrorDescription:@"CipherKeySpec inaccessible"]; + } + + if (keySpec.length != kSQLCipherKeySpecLength) { + OWSLogError(@"keyspec had length: %lu", (unsigned long)keySpec.length); + [self raiseKeySpecInaccessibleExceptionWithErrorDescription:@"CipherKeySpec invalid"]; + } + + return keySpec; +} + +- (void)raiseKeySpecInaccessibleExceptionWithErrorDescription:(NSString *)errorDescription +{ + OWSAssertDebug(CurrentAppContext().isMainApp && CurrentAppContext().isInBackground); + + // Sleep to give analytics events time to be delivered. + [NSThread sleepForTimeInterval:5.0f]; + + // Presumably this happened in response to a push notification. It's possible that the keychain is corrupted + // but it could also just be that the user hasn't yet unlocked their device since our password is + // kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + OWSFail(@"%@", errorDescription); +} + ++ (void)deleteDBKeys +{ + NSError *_Nullable error; + BOOL result = [CurrentAppContext().keychainStorage removeWithService:keychainService + key:keychainDBLegacyPassphrase + error:&error]; + if (error || !result) { + OWSFailDebug(@"could not remove legacy passphrase."); + } + result = [CurrentAppContext().keychainStorage removeWithService:keychainService + key:keychainDBCipherKeySpec + error:&error]; + if (error || !result) { + OWSFailDebug(@"could not remove cipher key spec."); + } +} + +- (unsigned long long)databaseFileSize +{ + return [OWSFileSystem fileSizeOfPath:self.databaseFilePath].unsignedLongLongValue; +} + +- (unsigned long long)databaseWALFileSize +{ + return [OWSFileSystem fileSizeOfPath:self.databaseFilePath_WAL].unsignedLongLongValue; +} + +- (unsigned long long)databaseSHMFileSize +{ + return [OWSFileSystem fileSizeOfPath:self.databaseFilePath_SHM].unsignedLongLongValue; +} + ++ (nullable NSData *)tryToLoadKeyChainValue:(NSString *)keychainKey errorHandle:(NSError **)errorHandle +{ + OWSAssertDebug(keychainKey.length > 0); + OWSAssertDebug(errorHandle); + + NSData *_Nullable data = + [CurrentAppContext().keychainStorage dataForService:keychainService key:keychainKey error:errorHandle]; + if (*errorHandle || !data) { + OWSLogWarn(@"could not load keychain value."); + } + return data; +} + ++ (void)storeKeyChainValue:(NSData *)data keychainKey:(NSString *)keychainKey +{ + OWSAssertDebug(keychainKey.length > 0); + OWSAssertDebug(data.length > 0); + + NSError *error; + BOOL success = + [CurrentAppContext().keychainStorage setWithData:data service:keychainService key:keychainKey error:&error]; + if (!success || error) { + OWSFailDebug(@"Could not store database metadata"); + + // Sleep to give analytics events time to be delivered. + [NSThread sleepForTimeInterval:15.0f]; + + OWSFail(@"Setting keychain value failed with error: %@", error); + } else { + OWSLogWarn(@"Successfully set new keychain value."); + } +} + +- (void)logFileSizes +{ + OWSLogInfo(@"Database file size: %@", [OWSFileSystem fileSizeOfPath:self.databaseFilePath]); + OWSLogInfo(@"\t SHM file size: %@", [OWSFileSystem fileSizeOfPath:self.databaseFilePath_SHM]); + OWSLogInfo(@"\t WAL file size: %@", [OWSFileSystem fileSizeOfPath:self.databaseFilePath_WAL]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncConfigurationMessage.h b/SignalUtilitiesKit/OWSSyncConfigurationMessage.h new file mode 100644 index 000000000..4d88ff8bb --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncConfigurationMessage.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSSyncConfigurationMessage : OWSOutgoingSyncMessage + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithReadReceiptsEnabled:(BOOL)readReceiptsEnabled + showUnidentifiedDeliveryIndicators:(BOOL)showUnidentifiedDeliveryIndicators + showTypingIndicators:(BOOL)showTypingIndicators + sendLinkPreviews:(BOOL)sendLinkPreviews NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncConfigurationMessage.m b/SignalUtilitiesKit/OWSSyncConfigurationMessage.m new file mode 100644 index 000000000..1254b7856 --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncConfigurationMessage.m @@ -0,0 +1,66 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSSyncConfigurationMessage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSSyncConfigurationMessage () + +@property (nonatomic, readonly) BOOL areReadReceiptsEnabled; +@property (nonatomic, readonly) BOOL showUnidentifiedDeliveryIndicators; +@property (nonatomic, readonly) BOOL showTypingIndicators; +@property (nonatomic, readonly) BOOL sendLinkPreviews; + +@end + +@implementation OWSSyncConfigurationMessage + +- (instancetype)initWithReadReceiptsEnabled:(BOOL)areReadReceiptsEnabled + showUnidentifiedDeliveryIndicators:(BOOL)showUnidentifiedDeliveryIndicators + showTypingIndicators:(BOOL)showTypingIndicators + sendLinkPreviews:(BOOL)sendLinkPreviews +{ + self = [super init]; + if (!self) { + return nil; + } + + _areReadReceiptsEnabled = areReadReceiptsEnabled; + _showUnidentifiedDeliveryIndicators = showUnidentifiedDeliveryIndicators; + _showTypingIndicators = showTypingIndicators; + _sendLinkPreviews = sendLinkPreviews; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + SSKProtoSyncMessageConfigurationBuilder *configurationBuilder = [SSKProtoSyncMessageConfiguration builder]; + configurationBuilder.readReceipts = self.areReadReceiptsEnabled; + configurationBuilder.unidentifiedDeliveryIndicators = self.showUnidentifiedDeliveryIndicators; + configurationBuilder.typingIndicators = self.showTypingIndicators; + configurationBuilder.linkPreviews = self.sendLinkPreviews; + + NSError *error; + SSKProtoSyncMessageConfiguration *_Nullable configurationProto = [configurationBuilder buildAndReturnError:&error]; + if (error || !configurationProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + + SSKProtoSyncMessageBuilder *builder = [SSKProtoSyncMessage builder]; + builder.configuration = configurationProto; + return builder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncContactsMessage.h b/SignalUtilitiesKit/OWSSyncContactsMessage.h new file mode 100644 index 000000000..89b3c8862 --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncContactsMessage.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol ProfileManagerProtocol; + +@class OWSIdentityManager; +@class SignalAccount; + +@interface OWSSyncContactsMessage : OWSOutgoingSyncMessage + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithSignalAccounts:(NSArray *)signalAccounts + identityManager:(OWSIdentityManager *)identityManager + profileManager:(id)profileManager NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (nullable NSData *)buildPlainTextAttachmentDataWithTransaction:(YapDatabaseReadTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncContactsMessage.m b/SignalUtilitiesKit/OWSSyncContactsMessage.m new file mode 100644 index 000000000..58e979984 --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncContactsMessage.m @@ -0,0 +1,157 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSSyncContactsMessage.h" +#import "Contact.h" +#import "ContactsManagerProtocol.h" +#import "OWSContactsOutputStream.h" +#import "OWSIdentityManager.h" +#import "ProfileManagerProtocol.h" +#import "ProtoUtils.h" +#import "SSKEnvironment.h" +#import "SignalAccount.h" +#import "TSAccountManager.h" +#import "TSAttachment.h" +#import "TSAttachmentStream.h" +#import "TSContactThread.h" +#import +#import +#import "OWSPrimaryStorage.h" + +@import Contacts; + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSSyncContactsMessage () + +@property (nonatomic, readonly) NSArray *signalAccounts; +@property (nonatomic, readonly) OWSIdentityManager *identityManager; +@property (nonatomic, readonly) id profileManager; + +@end + +@implementation OWSSyncContactsMessage + +- (instancetype)initWithSignalAccounts:(NSArray *)signalAccounts + identityManager:(OWSIdentityManager *)identityManager + profileManager:(id)profileManager +{ + self = [super init]; + if (!self) { + return self; + } + + _signalAccounts = signalAccounts; + _identityManager = identityManager; + _profileManager = profileManager; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +#pragma mark - Dependencies + +- (id)contactsManager { + return SSKEnvironment.shared.contactsManager; +} + +- (TSAccountManager *)tsAccountManager { + return TSAccountManager.sharedInstance; +} + +#pragma mark - + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + NSError *error; + if (self.attachmentIds.count > 1) { + OWSLogError(@"Expected sync contact message to have one or zero attachments, but found %lu.", (unsigned long)self.attachmentIds.count); + } + + SSKProtoSyncMessageContactsBuilder *contactsBuilder; + if (self.attachmentIds.count == 0) { + SSKProtoAttachmentPointerBuilder *attachmentProtoBuilder = [SSKProtoAttachmentPointer builderWithId:0]; + SSKProtoAttachmentPointer *attachmentProto = [attachmentProtoBuilder buildAndReturnError:&error]; + contactsBuilder = [SSKProtoSyncMessageContacts builder]; + [contactsBuilder setBlob:attachmentProto]; + __block NSData *data; + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + data = [self buildPlainTextAttachmentDataWithTransaction:transaction]; + }]; + [contactsBuilder setData:data]; + } else { + SSKProtoAttachmentPointer *attachmentProto = [TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject]; + if (attachmentProto == nil) { + OWSFailDebug(@"Couldn't build protobuf."); + return nil; + } + contactsBuilder = [SSKProtoSyncMessageContacts builder]; + [contactsBuilder setBlob:attachmentProto]; + } + [contactsBuilder setIsComplete:YES]; + + SSKProtoSyncMessageContacts *contactsProto = [contactsBuilder buildAndReturnError:&error]; + if (error || contactsProto == nil) { + OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error); + return nil; + } + SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder]; + [syncMessageBuilder setContacts:contactsProto]; + + return syncMessageBuilder; +} + +- (nullable NSData *)buildPlainTextAttachmentDataWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSMutableArray *signalAccounts = [self.signalAccounts mutableCopy]; + + // TODO use temp file stream to avoid loading everything into memory at once + // First though, we need to re-engineer our attachment process to accept streams (encrypting with stream, + // and uploading with streams). + NSOutputStream *dataOutputStream = [NSOutputStream outputStreamToMemory]; + [dataOutputStream open]; + OWSContactsOutputStream *contactsOutputStream = + [[OWSContactsOutputStream alloc] initWithOutputStream:dataOutputStream]; + + for (SignalAccount *signalAccount in signalAccounts) { + OWSRecipientIdentity *_Nullable recipientIdentity = + [self.identityManager recipientIdentityForRecipientId:signalAccount.recipientId]; + NSData *_Nullable profileKeyData = [self.profileManager profileKeyDataForRecipientId:signalAccount.recipientId]; + + OWSDisappearingMessagesConfiguration *_Nullable disappearingMessagesConfiguration; + NSString *conversationColorName; + + TSContactThread *_Nullable contactThread = [TSContactThread getThreadWithContactId:signalAccount.recipientId transaction:transaction]; + if (contactThread) { + conversationColorName = contactThread.conversationColorName; + disappearingMessagesConfiguration = [contactThread disappearingMessagesConfigurationWithTransaction:transaction]; + } else { + conversationColorName = [TSThread stableColorNameForNewConversationWithString:signalAccount.recipientId]; + } + + [contactsOutputStream writeSignalAccount:signalAccount + recipientIdentity:recipientIdentity + profileKeyData:profileKeyData + contactsManager:self.contactsManager + conversationColorName:conversationColorName + disappearingMessagesConfiguration:disappearingMessagesConfiguration]; + } + + [dataOutputStream close]; + + if (contactsOutputStream.hasError) { + OWSFailDebug(@"Could not write contacts sync stream."); + return nil; + } + + return [dataOutputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncGroupsMessage.h b/SignalUtilitiesKit/OWSSyncGroupsMessage.h new file mode 100644 index 000000000..6f7ccc188 --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncGroupsMessage.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class YapDatabaseReadTransaction; +@class TSGroupThread; + +@interface OWSSyncGroupsMessage : OWSOutgoingSyncMessage + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithGroupThread:(TSGroupThread *)thread NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (nullable NSData *)buildPlainTextAttachmentDataWithTransaction:(YapDatabaseReadTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncGroupsMessage.m b/SignalUtilitiesKit/OWSSyncGroupsMessage.m new file mode 100644 index 000000000..c3def4420 --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncGroupsMessage.m @@ -0,0 +1,104 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSSyncGroupsMessage.h" +#import "OWSGroupsOutputStream.h" +#import "TSAttachment.h" +#import "TSAttachmentStream.h" +#import "TSContactThread.h" +#import "TSGroupModel.h" +#import "TSGroupThread.h" +#import +#import +#import "OWSPrimaryStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSSyncGroupsMessage () + +@property (nonatomic, readonly) TSGroupThread *groupThread; + +@end + +@implementation OWSSyncGroupsMessage + +- (instancetype)initWithGroupThread:(TSGroupThread *)thread +{ + self = [super init]; + if (!self) { + return self; + } + + _groupThread = thread; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + NSError *error; + if (self.attachmentIds.count > 1) { + OWSLogError(@"Expected sync group message to have one or zero attachments, but found %lu.", (unsigned long)self.attachmentIds.count); + } + + SSKProtoSyncMessageGroupsBuilder *groupsBuilder; + if (self.attachmentIds.count == 0) { + SSKProtoAttachmentPointerBuilder *attachmentProtoBuilder = [SSKProtoAttachmentPointer builderWithId:0]; + SSKProtoAttachmentPointer *attachmentProto = [attachmentProtoBuilder buildAndReturnError:&error]; + groupsBuilder = [SSKProtoSyncMessageGroups builder]; + [groupsBuilder setBlob:attachmentProto]; + __block NSData *data; + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + data = [self buildPlainTextAttachmentDataWithTransaction:transaction]; + }]; + [groupsBuilder setData:data]; + } else { + SSKProtoAttachmentPointer *attachmentProto = [TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject]; + if (attachmentProto == nil) { + OWSFailDebug(@"Couldn't build protobuf."); + return nil; + } + groupsBuilder = [SSKProtoSyncMessageGroups builder]; + [groupsBuilder setBlob:attachmentProto]; + } + + SSKProtoSyncMessageGroups *_Nullable groupsProto = [groupsBuilder buildAndReturnError:&error]; + if (error || !groupsProto) { + OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error); + return nil; + } + + SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder]; + [syncMessageBuilder setGroups:groupsProto]; + + return syncMessageBuilder; +} + +- (nullable NSData *)buildPlainTextAttachmentDataWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + // TODO use temp file stream to avoid loading everything into memory at once + // First though, we need to re-engineer our attachment process to accept streams (encrypting with stream, + // and uploading with streams). + NSOutputStream *dataOutputStream = [NSOutputStream outputStreamToMemory]; + [dataOutputStream open]; + OWSGroupsOutputStream *groupsOutputStream = [[OWSGroupsOutputStream alloc] initWithOutputStream:dataOutputStream]; + [groupsOutputStream writeGroup:self.groupThread transaction:transaction]; + [dataOutputStream close]; + + if (groupsOutputStream.hasError) { + OWSFailDebug(@"Could not write groups sync stream."); + return nil; + } + + return [dataOutputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncGroupsRequestMessage.h b/SignalUtilitiesKit/OWSSyncGroupsRequestMessage.h new file mode 100644 index 000000000..a3e5202ee --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncGroupsRequestMessage.h @@ -0,0 +1,27 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSSyncGroupsRequestMessage : TSOutgoingMessage + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithThread:(nullable TSThread *)thread groupId:(NSData *)groupId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncGroupsRequestMessage.m b/SignalUtilitiesKit/OWSSyncGroupsRequestMessage.m new file mode 100644 index 000000000..29740759a --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncGroupsRequestMessage.m @@ -0,0 +1,85 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSSyncGroupsRequestMessage.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSSyncGroupsRequestMessage () + +@property (nonatomic) NSData *groupId; + +@end + +#pragma mark - + +@implementation OWSSyncGroupsRequestMessage + +- (instancetype)initWithThread:(nullable TSThread *)thread groupId:(NSData *)groupId +{ + // MJK TODO - remove senderTimestamp + self = [super initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:0 + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + if (!self) { + return self; + } + + OWSAssertDebug(groupId.length > 0); + _groupId = groupId; + + return self; +} + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeSync]; } + +- (BOOL)shouldBeSaved +{ + return NO; +} + +- (BOOL)shouldSyncTranscript +{ + return NO; +} + +- (BOOL)isSilent +{ + // Avoid "phantom messages" + + return YES; +} + +- (nullable id)dataMessageBuilder +{ + SSKProtoGroupContextBuilder *groupContextBuilder = + [SSKProtoGroupContext builderWithId:self.groupId type:SSKProtoGroupContextTypeRequestInfo]; + + NSError *error; + SSKProtoGroupContext *_Nullable groupContextProto = [groupContextBuilder buildAndReturnError:&error]; + if (error || !groupContextProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + + SSKProtoDataMessageBuilder *builder = [SSKProtoDataMessage builder]; + [builder setTimestamp:self.timestamp]; + [builder setGroup:groupContextProto]; + + return builder; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSSyncManagerProtocol.h b/SignalUtilitiesKit/OWSSyncManagerProtocol.h new file mode 100644 index 000000000..e4ccd1bc5 --- /dev/null +++ b/SignalUtilitiesKit/OWSSyncManagerProtocol.h @@ -0,0 +1,34 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class AnyPromise; +@class SignalAccount; +@class YapDatabaseReadTransaction; +@class TSGroupThread; + +@protocol OWSSyncManagerProtocol + +- (void)sendConfigurationSyncMessage; + +- (AnyPromise *)syncLocalContact __attribute__((warn_unused_result)); + +- (AnyPromise *)syncContact:(NSString *)hexEncodedPubKey transaction:(YapDatabaseReadTransaction *)transaction; + +- (AnyPromise *)syncAllContacts __attribute__((warn_unused_result)); + +- (AnyPromise *)syncContactsForSignalAccounts:(NSArray *)signalAccounts __attribute__((warn_unused_result)); + +- (AnyPromise *)syncAllGroups __attribute__((warn_unused_result)); + +- (AnyPromise *)syncGroupForThread:(TSGroupThread *)thread; + +- (AnyPromise *)syncAllOpenGroups __attribute__((warn_unused_result)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSThumbnailService.swift b/SignalUtilitiesKit/OWSThumbnailService.swift new file mode 100644 index 000000000..d65a502d5 --- /dev/null +++ b/SignalUtilitiesKit/OWSThumbnailService.swift @@ -0,0 +1,178 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import AVFoundation + +public enum OWSThumbnailError: Error { + case failure(description: String) + case assertionFailure(description: String) + case externalError(description: String, underlyingError:Error) +} + +@objc public class OWSLoadedThumbnail: NSObject { + public typealias DataSourceBlock = () throws -> Data + + @objc + public let image: UIImage + let dataSourceBlock: DataSourceBlock + + @objc + public init(image: UIImage, filePath: String) { + self.image = image + self.dataSourceBlock = { + return try Data(contentsOf: URL(fileURLWithPath: filePath)) + } + } + + @objc + public init(image: UIImage, data: Data) { + self.image = image + self.dataSourceBlock = { + return data + } + } + + @objc + public func data() throws -> Data { + return try dataSourceBlock() + } +} + +private struct OWSThumbnailRequest { + public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void + public typealias FailureBlock = (Error) -> Void + + let attachment: TSAttachmentStream + let thumbnailDimensionPoints: UInt + let success: SuccessBlock + let failure: FailureBlock + + init(attachment: TSAttachmentStream, thumbnailDimensionPoints: UInt, success: @escaping SuccessBlock, failure: @escaping FailureBlock) { + self.attachment = attachment + self.thumbnailDimensionPoints = thumbnailDimensionPoints + self.success = success + self.failure = failure + } +} + +@objc public class OWSThumbnailService: NSObject { + + // MARK: - Singleton class + + @objc(shared) + public static let shared = OWSThumbnailService() + + public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void + public typealias FailureBlock = (Error) -> Void + + private let serialQueue = DispatchQueue(label: "OWSThumbnailService") + + // This property should only be accessed on the serialQueue. + // + // We want to process requests in _reverse_ order in which they + // arrive so that we prioritize the most recent view state. + private var thumbnailRequestStack = [OWSThumbnailRequest]() + + private override init() { + super.init() + + SwiftSingletons.register(self) + } + + private func canThumbnailAttachment(attachment: TSAttachmentStream) -> Bool { + return attachment.isImage || attachment.isAnimated || attachment.isVideo + } + + // success and failure will be called async _off_ the main thread. + @objc + public func ensureThumbnail(forAttachment attachment: TSAttachmentStream, + thumbnailDimensionPoints: UInt, + success: @escaping SuccessBlock, + failure: @escaping FailureBlock) { + serialQueue.async { + let thumbnailRequest = OWSThumbnailRequest(attachment: attachment, thumbnailDimensionPoints: thumbnailDimensionPoints, success: success, failure: failure) + self.thumbnailRequestStack.append(thumbnailRequest) + + self.processNextRequestSync() + } + } + + private func processNextRequestAsync() { + serialQueue.async { + self.processNextRequestSync() + } + } + + // This should only be called on the serialQueue. + private func processNextRequestSync() { + guard let thumbnailRequest = thumbnailRequestStack.popLast() else { + return + } + + do { + let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest) + DispatchQueue.global().async { + thumbnailRequest.success(loadedThumbnail) + } + } catch { + Logger.error("Could not create thumbnail: \(error)") + + DispatchQueue.global().async { + thumbnailRequest.failure(error) + } + } + } + + // This should only be called on the serialQueue. + // + // It should be safe to assume that an attachment will never end up with two thumbnails of + // the same size since: + // + // * Thumbnails are only added by this method. + // * This method checks for an existing thumbnail using the same connection. + // * This method is performed on the serial queue. + private func process(thumbnailRequest: OWSThumbnailRequest) throws -> OWSLoadedThumbnail { + let attachment = thumbnailRequest.attachment + guard canThumbnailAttachment(attachment: attachment) else { + throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.") + } + let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints) + if FileManager.default.fileExists(atPath: thumbnailPath) { + guard let image = UIImage(contentsOfFile: thumbnailPath) else { + throw OWSThumbnailError.failure(description: "Could not load thumbnail.") + } + return OWSLoadedThumbnail(image: image, filePath: thumbnailPath) + } + + Logger.verbose("Creating thumbnail of size: \(thumbnailRequest.thumbnailDimensionPoints)") + + let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent + guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else { + throw OWSThumbnailError.failure(description: "Could not create attachment's thumbnail directory.") + } + guard let originalFilePath = attachment.originalFilePath else { + throw OWSThumbnailError.failure(description: "Missing original file path.") + } + let maxDimension = CGFloat(thumbnailRequest.thumbnailDimensionPoints) + let thumbnailImage: UIImage + if attachment.isImage || attachment.isAnimated { + thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension) + } else if attachment.isVideo { + thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension) + } else { + throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.") + } + guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else { + throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.") + } + do { + try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath), options: .atomicWrite) + } catch let error as NSError { + throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error) + } + OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath) + return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData) + } +} diff --git a/SignalUtilitiesKit/OWSUDManager.swift b/SignalUtilitiesKit/OWSUDManager.swift new file mode 100644 index 000000000..7d2138f75 --- /dev/null +++ b/SignalUtilitiesKit/OWSUDManager.swift @@ -0,0 +1,518 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + + + +public enum OWSUDError: Error { + case assertionError(description: String) + case invalidData(description: String) +} + +@objc +public enum OWSUDCertificateExpirationPolicy: Int { + // We want to try to rotate the sender certificate + // on a frequent basis, but we don't want to block + // sending on this. + case strict + case permissive +} + +@objc +public enum UnidentifiedAccessMode: Int { + case unknown + case enabled + case disabled + case unrestricted +} + +private func string(forUnidentifiedAccessMode mode: UnidentifiedAccessMode) -> String { + switch mode { + case .unknown: + return "unknown" + case .enabled: + return "enabled" + case .disabled: + return "disabled" + case .unrestricted: + return "unrestricted" + } +} + +@objc +public class OWSUDAccess: NSObject { + @objc + public let udAccessKey: SMKUDAccessKey + + @objc + public let udAccessMode: UnidentifiedAccessMode + + @objc + public let isRandomKey: Bool + + @objc + public required init(udAccessKey: SMKUDAccessKey, + udAccessMode: UnidentifiedAccessMode, + isRandomKey: Bool) { + self.udAccessKey = udAccessKey + self.udAccessMode = udAccessMode + self.isRandomKey = isRandomKey + } +} + +@objc public protocol OWSUDManager: class { + + @objc func setup() + + @objc func trustRoot() -> ECPublicKey + + @objc func isUDVerboseLoggingEnabled() -> Bool + + // MARK: - Recipient State + + @objc + func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, recipientId: String) + + @objc + func unidentifiedAccessMode(forRecipientId recipientId: RecipientIdentifier) -> UnidentifiedAccessMode + + @objc + func udAccessKey(forRecipientId recipientId: RecipientIdentifier) -> SMKUDAccessKey? + + @objc + func udAccess(forRecipientId recipientId: RecipientIdentifier, + requireSyncAccess: Bool) -> OWSUDAccess? + + // MARK: Sender Certificate + + // We use completion handlers instead of a promise so that message sending + // logic can access the strongly typed certificate data. + @objc + func ensureSenderCertificate(success:@escaping (SMKSenderCertificate) -> Void, + failure:@escaping (Error) -> Void) + + // MARK: Unrestricted Access + + @objc + func shouldAllowUnrestrictedAccessLocal() -> Bool + @objc + func setShouldAllowUnrestrictedAccessLocal(_ value: Bool) + + @objc + func getSenderCertificate() -> SMKSenderCertificate? +} + +// MARK: - + +@objc +public class OWSUDManagerImpl: NSObject, OWSUDManager { + + private let dbConnection: YapDatabaseConnection + + // MARK: Local Configuration State + private let kUDCollection = "kUDCollection" + private let kUDCurrentSenderCertificateKey_Production = "kUDCurrentSenderCertificateKey_Production" + private let kUDCurrentSenderCertificateKey_Staging = "kUDCurrentSenderCertificateKey_Staging" + private let kUDCurrentSenderCertificateDateKey_Production = "kUDCurrentSenderCertificateDateKey_Production" + private let kUDCurrentSenderCertificateDateKey_Staging = "kUDCurrentSenderCertificateDateKey_Staging" + private let kUDUnrestrictedAccessKey = "kUDUnrestrictedAccessKey" + + // MARK: Recipient State + private let kUnidentifiedAccessCollection = "kUnidentifiedAccessCollection" + + var certificateValidator: SMKCertificateValidator + + @objc + public required init(primaryStorage: OWSPrimaryStorage) { + self.dbConnection = primaryStorage.newDatabaseConnection() + self.certificateValidator = SMKCertificateDefaultValidator(trustRoot: OWSUDManagerImpl.trustRoot()) + + super.init() + + SwiftSingletons.register(self) + } + + @objc public func setup() { + AppReadiness.runNowOrWhenAppDidBecomeReady { + guard self.tsAccountManager.isRegistered() else { + return + } + + // Any error is silently ignored on startup. + self.ensureSenderCertificate(certificateExpirationPolicy: .strict).retainUntilComplete() + } + NotificationCenter.default.addObserver(self, + selector: #selector(registrationStateDidChange), + name: .RegistrationStateDidChange, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(didBecomeActive), + name: NSNotification.Name.OWSApplicationDidBecomeActive, + object: nil) + } + + @objc + func registrationStateDidChange() { + AssertIsOnMainThread() + + guard tsAccountManager.isRegisteredAndReady() else { + return + } + + // Any error is silently ignored + ensureSenderCertificate(certificateExpirationPolicy: .strict).retainUntilComplete() + } + + @objc func didBecomeActive() { + AssertIsOnMainThread() + + AppReadiness.runNowOrWhenAppDidBecomeReady { + guard self.tsAccountManager.isRegistered() else { + return + } + + // Any error is silently ignored on startup. + self.ensureSenderCertificate(certificateExpirationPolicy: .strict).retainUntilComplete() + } + } + + // MARK: - + + @objc + public func isUDVerboseLoggingEnabled() -> Bool { + return false + } + + // MARK: - Dependencies + + private var profileManager: ProfileManagerProtocol { + return SSKEnvironment.shared.profileManager + } + + private var tsAccountManager: TSAccountManager { + return TSAccountManager.sharedInstance() + } + + // MARK: - Recipient state + + @objc + public func randomUDAccessKey() -> SMKUDAccessKey { + return SMKUDAccessKey(randomKeyData: ()) + } + + private func unidentifiedAccessMode(forRecipientId recipientId: RecipientIdentifier, + isLocalNumber: Bool, + transaction: YapDatabaseReadTransaction) -> UnidentifiedAccessMode { + let defaultValue: UnidentifiedAccessMode = isLocalNumber ? .enabled : .unknown + guard let existingRawValue = transaction.object(forKey: recipientId, inCollection: kUnidentifiedAccessCollection) as? Int else { + return defaultValue + } + guard let existingValue = UnidentifiedAccessMode(rawValue: existingRawValue) else { + owsFailDebug("Couldn't parse mode value.") + return defaultValue + } + return existingValue + } + + @objc + public func unidentifiedAccessMode(forRecipientId recipientId: RecipientIdentifier) -> UnidentifiedAccessMode { + var isLocalNumber = false + if let localNumber = tsAccountManager.localNumber() { + isLocalNumber = recipientId == localNumber + } + + var mode: UnidentifiedAccessMode = .unknown + dbConnection.read { (transaction) in + mode = self.unidentifiedAccessMode(forRecipientId: recipientId, isLocalNumber: isLocalNumber, transaction: transaction) + } + return mode + } + + @objc + public func setUnidentifiedAccessMode(_ mode: UnidentifiedAccessMode, recipientId: String) { + var isLocalNumber = false + if let localNumber = tsAccountManager.localNumber() { + if recipientId == localNumber { + Logger.info("Setting local UD access mode: \(string(forUnidentifiedAccessMode: mode))") + isLocalNumber = true + } + } + + Storage.writeSync { (transaction) in + let oldMode = self.unidentifiedAccessMode(forRecipientId: recipientId, isLocalNumber: isLocalNumber, transaction: transaction) + + transaction.setObject(mode.rawValue as Int, forKey: recipientId, inCollection: self.kUnidentifiedAccessCollection) + + if mode != oldMode { + Logger.info("Setting UD access mode for \(recipientId): \(string(forUnidentifiedAccessMode: oldMode)) -> \(string(forUnidentifiedAccessMode: mode))") + } + } + } + + // Returns the UD access key for a given recipient + // if we have a valid profile key for them. + @objc + public func udAccessKey(forRecipientId recipientId: RecipientIdentifier) -> SMKUDAccessKey? { + guard let profileKey = profileManager.profileKeyData(forRecipientId: recipientId) else { + // Mark as "not a UD recipient". + return nil + } + do { + let udAccessKey = try SMKUDAccessKey(profileKey: profileKey) + return udAccessKey + } catch { + Logger.error("Could not determine udAccessKey: \(error)") + return nil + } + } + + // Returns the UD access key for sending to a given recipient. + @objc + public func udAccess(forRecipientId recipientId: RecipientIdentifier, + requireSyncAccess: Bool) -> OWSUDAccess? { + if requireSyncAccess { + guard let localNumber = tsAccountManager.localNumber() else { + if isUDVerboseLoggingEnabled() { + Logger.info("UD disabled for \(recipientId), no local number.") + } + owsFailDebug("Missing local number.") + return nil + } + if localNumber != recipientId { + let selfAccessMode = unidentifiedAccessMode(forRecipientId: localNumber) + guard selfAccessMode != .disabled else { + if isUDVerboseLoggingEnabled() { + Logger.info("UD disabled for \(recipientId), UD disabled for sync messages.") + } + return nil + } + } + } + + let accessMode = unidentifiedAccessMode(forRecipientId: recipientId) + switch accessMode { + case .unrestricted: + // Unrestricted users should use a random key. + if isUDVerboseLoggingEnabled() { + Logger.info("UD enabled for \(recipientId) with random key.") + } + let udAccessKey = randomUDAccessKey() + return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: true) + case .unknown: + // Unknown users should use a derived key if possible, + // and otherwise use a random key. + if let udAccessKey = udAccessKey(forRecipientId: recipientId) { + if isUDVerboseLoggingEnabled() { + Logger.info("UD unknown for \(recipientId); trying derived key.") + } + return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: false) + } else { + if isUDVerboseLoggingEnabled() { + Logger.info("UD unknown for \(recipientId); trying random key.") + } + let udAccessKey = randomUDAccessKey() + return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: true) + } + case .enabled: + guard let udAccessKey = udAccessKey(forRecipientId: recipientId) else { + if isUDVerboseLoggingEnabled() { + Logger.info("UD disabled for \(recipientId), no profile key for this recipient.") + } + if (!CurrentAppContext().isRunningTests) { + owsFailDebug("Couldn't find profile key for UD-enabled user.") + } + return nil + } + if isUDVerboseLoggingEnabled() { + Logger.info("UD enabled for \(recipientId).") + } + return OWSUDAccess(udAccessKey: udAccessKey, udAccessMode: accessMode, isRandomKey: false) + case .disabled: + if isUDVerboseLoggingEnabled() { + Logger.info("UD disabled for \(recipientId), UD not enabled for this recipient.") + } + return nil + } + } + + // MARK: - Sender Certificate + + #if DEBUG + @objc + public func hasSenderCertificate() -> Bool { + return senderCertificate(certificateExpirationPolicy: .permissive) != nil + } + #endif + + private func senderCertificate(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> SMKSenderCertificate? { + if certificateExpirationPolicy == .strict { + guard let certificateDate = dbConnection.object(forKey: senderCertificateDateKey(), inCollection: kUDCollection) as? Date else { + return nil + } + guard certificateDate.timeIntervalSinceNow < kDayInterval else { + // Discard certificates that we obtained more than 24 hours ago. + return nil + } + } + + guard let certificateData = dbConnection.object(forKey: senderCertificateKey(), inCollection: kUDCollection) as? Data else { + return nil + } + + do { + let certificate = try SMKSenderCertificate.parse(data: certificateData) + + guard isValidCertificate(certificate) else { + Logger.warn("Current sender certificate is not valid.") + return nil + } + + return certificate + } catch { + owsFailDebug("Certificate could not be parsed: \(error)") + return nil + } + } + + func setSenderCertificate(_ certificateData: Data) { + dbConnection.setObject(Date(), forKey: senderCertificateDateKey(), inCollection: kUDCollection) + dbConnection.setObject(certificateData, forKey: senderCertificateKey(), inCollection: kUDCollection) + } + + private func senderCertificateKey() -> String { + return IsUsingProductionService() ? kUDCurrentSenderCertificateKey_Production : kUDCurrentSenderCertificateKey_Staging + } + + private func senderCertificateDateKey() -> String { + return IsUsingProductionService() ? kUDCurrentSenderCertificateDateKey_Production : kUDCurrentSenderCertificateDateKey_Staging + } + + @objc + public func ensureSenderCertificate(success:@escaping (SMKSenderCertificate) -> Void, + failure:@escaping (Error) -> Void) { + return ensureSenderCertificate(certificateExpirationPolicy: .permissive, + success: success, + failure: failure) + } + + private func ensureSenderCertificate(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy, + success:@escaping (SMKSenderCertificate) -> Void, + failure:@escaping (Error) -> Void) { + firstly { + ensureSenderCertificate(certificateExpirationPolicy: certificateExpirationPolicy) + }.map { certificate in + success(certificate) + }.catch { error in + failure(error) + }.retainUntilComplete() + } + + public func ensureSenderCertificate(certificateExpirationPolicy: OWSUDCertificateExpirationPolicy) -> Promise { + // Try to obtain a new sender certificate. + return firstly { + generateSenderCertificate() + }.map { (certificateData: Data, certificate: SMKSenderCertificate) in + + // Cache the current sender certificate. + self.setSenderCertificate(certificateData) + + return certificate + } + } + + private func generateSenderCertificate() -> Promise<(certificateData: Data, certificate: SMKSenderCertificate)> { + return Promise<(certificateData: Data, certificate: SMKSenderCertificate)> { seal in + // Loki: Generate a sender certificate locally + let sender = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + let certificate = SMKSenderCertificate(senderDeviceId: OWSDevicePrimaryDeviceId, senderRecipientId: sender) + let certificateAsData = try certificate.serialized() + guard isValidCertificate(certificate) else { + throw OWSUDError.invalidData(description: "Invalid sender certificate.") + } + seal.fulfill((certificateData: certificateAsData, certificate: certificate)) + } + } + + @objc + public func getSenderCertificate() -> SMKSenderCertificate? { + do { + let sender = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + let certificate = SMKSenderCertificate(senderDeviceId: OWSDevicePrimaryDeviceId, senderRecipientId: sender) + guard self.isValidCertificate(certificate) else { + throw OWSUDError.invalidData(description: "Invalid sender certificate returned by server") + } + return certificate + } catch { + print("[Loki] Couldn't get UD sender certificate due to error: \(error).") + return nil + } + } + + private func requestSenderCertificate() -> Promise<(certificateData: Data, certificate: SMKSenderCertificate)> { + return firstly { + SignalServiceRestClient().requestUDSenderCertificate() + }.map { certificateData -> (certificateData: Data, certificate: SMKSenderCertificate) in + let certificate = try SMKSenderCertificate.parse(data: certificateData) + + guard self.isValidCertificate(certificate) else { + throw OWSUDError.invalidData(description: "Invalid sender certificate returned by server") + } + + return (certificateData: certificateData, certificate: certificate) + } + } + + private func isValidCertificate(_ certificate: SMKSenderCertificate) -> Bool { + // Ensure that the certificate will not expire in the next hour. + // We want a threshold long enough to ensure that any outgoing message + // sends will complete before the expiration. + let nowMs = NSDate.ows_millisecondTimeStamp() + let anHourFromNowMs = nowMs + kHourInMs + + do { + try certificateValidator.throwswrapped_validate(senderCertificate: certificate, validationTime: anHourFromNowMs) + return true + } catch { + OWSLogger.error("Invalid certificate") + return false + } + } + + @objc + public func trustRoot() -> ECPublicKey { + return OWSUDManagerImpl.trustRoot() + } + + @objc + public class func trustRoot() -> ECPublicKey { + guard let trustRootData = NSData(fromBase64String: kUDTrustRoot) else { + // This exits. + owsFail("Invalid trust root data.") + } + + do { + return try ECPublicKey(serializedKeyData: trustRootData as Data) + } catch { + // This exits. + owsFail("Invalid trust root.") + } + } + + // MARK: - Unrestricted Access + + @objc + public func shouldAllowUnrestrictedAccessLocal() -> Bool { + return dbConnection.bool(forKey: kUDUnrestrictedAccessKey, inCollection: kUDCollection, defaultValue: false) + } + + @objc + public func setShouldAllowUnrestrictedAccessLocal(_ value: Bool) { + dbConnection.setBool(value, forKey: kUDUnrestrictedAccessKey, inCollection: kUDCollection) + + // Try to update the account attributes to reflect this change. + tsAccountManager.updateAccountAttributes().retainUntilComplete() + } +} diff --git a/SignalUtilitiesKit/OWSUnknownContactBlockOfferMessage.h b/SignalUtilitiesKit/OWSUnknownContactBlockOfferMessage.h new file mode 100644 index 000000000..256edd2dd --- /dev/null +++ b/SignalUtilitiesKit/OWSUnknownContactBlockOfferMessage.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSErrorMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +// This is a deprecated class, we're keeping it around to avoid YapDB serialization errors +// TODO - remove this class, clean up existing instances, ensure any missed ones don't explode (UnknownDBObject) +__attribute__((deprecated)) @interface OWSUnknownContactBlockOfferMessage : TSErrorMessage + +@property (nonatomic, readonly) NSString *contactId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSUnknownContactBlockOfferMessage.m b/SignalUtilitiesKit/OWSUnknownContactBlockOfferMessage.m new file mode 100644 index 000000000..3d11291cf --- /dev/null +++ b/SignalUtilitiesKit/OWSUnknownContactBlockOfferMessage.m @@ -0,0 +1,38 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSUnknownContactBlockOfferMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSUnknownContactBlockOfferMessage () + +@property (nonatomic) NSString *contactId; + +@end + +#pragma mark - + +// This is a deprecated class, we're keeping it around to avoid YapDB serialization errors +// TODO - remove this class, clean up existing instances, ensure any missed ones don't explode (UnknownDBObject) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +@implementation OWSUnknownContactBlockOfferMessage +#pragma clang diagnostic pop + +- (BOOL)shouldUseReceiptDateForSorting +{ + // Use the timestamp, not the "received at" timestamp to sort, + // since we're creating these interactions after the fact and back-dating them. + return NO; +} + +- (BOOL)isDynamicInteraction +{ + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSUploadOperation.h b/SignalUtilitiesKit/OWSUploadOperation.h new file mode 100644 index 000000000..b8c68d4c3 --- /dev/null +++ b/SignalUtilitiesKit/OWSUploadOperation.h @@ -0,0 +1,27 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOperation.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSOutgoingMessage; +@class YapDatabaseConnection; + +extern NSString *const kAttachmentUploadProgressNotification; +extern NSString *const kAttachmentUploadProgressKey; +extern NSString *const kAttachmentUploadAttachmentIDKey; + +@interface OWSUploadOperation : OWSOperation + +@property (nullable, readonly) NSError *lastError; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithAttachmentId:(NSString *)attachmentId + threadID:(NSString *)threadID + dbConnection:(YapDatabaseConnection *)dbConnection NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSUploadOperation.m b/SignalUtilitiesKit/OWSUploadOperation.m new file mode 100644 index 000000000..4b0d7b382 --- /dev/null +++ b/SignalUtilitiesKit/OWSUploadOperation.m @@ -0,0 +1,191 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSUploadOperation.h" +#import "MIMETypeUtil.h" +#import "NSError+MessageSending.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSDispatch.h" +#import "OWSError.h" +#import "OWSOperation.h" +#import "OWSRequestFactory.h" +#import "SSKEnvironment.h" +#import "TSAttachmentStream.h" +#import "TSNetworkManager.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kAttachmentUploadProgressNotification = @"kAttachmentUploadProgressNotification"; +NSString *const kAttachmentUploadProgressKey = @"kAttachmentUploadProgressKey"; +NSString *const kAttachmentUploadAttachmentIDKey = @"kAttachmentUploadAttachmentIDKey"; + +// Use a slightly non-zero value to ensure that the progress +// indicator shows up as quickly as possible. +static const CGFloat kAttachmentUploadProgressTheta = 0.001f; + +@interface OWSUploadOperation () + +@property (readonly, nonatomic) NSString *attachmentId; +@property (readonly, nonatomic) NSString *threadID; +@property (readonly, nonatomic) YapDatabaseConnection *dbConnection; + +@end + +#pragma mark - + +@implementation OWSUploadOperation + +- (instancetype)initWithAttachmentId:(NSString *)attachmentId + threadID:(NSString *)threadID + dbConnection:(YapDatabaseConnection *)dbConnection +{ + self = [super init]; + if (!self) { + return self; + } + + self.remainingRetries = 4; + + _attachmentId = attachmentId; + _threadID = threadID; + _dbConnection = dbConnection; + + return self; +} + +- (TSNetworkManager *)networkManager +{ + return SSKEnvironment.shared.networkManager; +} + +- (void)run +{ + __block TSAttachmentStream *attachmentStream; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + attachmentStream = [TSAttachmentStream fetchObjectWithUniqueID:self.attachmentId transaction:transaction]; + }]; + + if (!attachmentStream) { + NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError(); + // Not finding a local attachment is a terminal failure + error.isRetryable = NO; + [self reportError:error]; + return; + } + + if (attachmentStream.isUploaded) { + OWSLogDebug(@"Attachment previously uploaded."); + [self reportSuccess]; + return; + } + + [self fireNotificationWithProgress:0]; + + __block SNOpenGroup *publicChat; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.threadID transaction:transaction]; + }]; + NSString *server = (publicChat != nil) ? publicChat.server : SNFileServerAPI.server; + + [[SNFileServerAPI uploadAttachment:attachmentStream withID:self.attachmentId toServer:server] + .thenOn(dispatch_get_main_queue(), ^() { + [self reportSuccess]; + }) + .catchOn(dispatch_get_main_queue(), ^(NSError *error) { + [self reportError:error]; + }) retainUntilComplete]; +} + +- (void)uploadWithServerId:(UInt64)serverId + location:(NSString *)location + attachmentStream:(TSAttachmentStream *)attachmentStream +{ + OWSLogDebug(@"started uploading data for attachment: %@", self.attachmentId); + NSError *error; + NSData *attachmentData = [attachmentStream readDataFromFileWithError:&error]; + if (error) { + OWSLogError(@"Failed to read attachment data with error: %@", error); + error.isRetryable = YES; + [self reportError:error]; + return; + } + + NSData *encryptionKey; + NSData *digest; + NSData *_Nullable encryptedAttachmentData = + [Cryptography encryptAttachmentData:attachmentData outKey:&encryptionKey outDigest:&digest]; + if (!encryptedAttachmentData) { + OWSFailDebug(@"could not encrypt attachment data."); + error = OWSErrorMakeFailedToSendOutgoingMessageError(); + error.isRetryable = YES; + [self reportError:error]; + return; + } + attachmentStream.encryptionKey = encryptionKey; + attachmentStream.digest = digest; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]]; + request.HTTPMethod = @"PUT"; + [request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; + + AFURLSessionManager *manager = [[AFURLSessionManager alloc] + initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + + NSURLSessionUploadTask *uploadTask; + uploadTask = [manager uploadTaskWithRequest:request + fromData:encryptedAttachmentData + progress:^(NSProgress *_Nonnull uploadProgress) { + [self fireNotificationWithProgress:uploadProgress.fractionCompleted]; + } + completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) { + OWSAssertIsOnMainThread(); + if (error) { + error.isRetryable = YES; + [self reportError:error]; + return; + } + + NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode; + BOOL isValidResponse = (statusCode >= 200) && (statusCode < 400); + if (!isValidResponse) { + OWSLogError(@"Unexpected server response: %d", (int)statusCode); + NSError *invalidResponseError = OWSErrorMakeUnableToProcessServerResponseError(); + invalidResponseError.isRetryable = YES; + [self reportError:invalidResponseError]; + return; + } + + OWSLogInfo(@"Uploaded attachment: %p serverId: %llu, byteCount: %u", + attachmentStream.uniqueId, + attachmentStream.serverId, + attachmentStream.byteCount); + attachmentStream.serverId = serverId; + attachmentStream.isUploaded = YES; + [attachmentStream saveAsyncWithCompletionBlock:^{ + [self reportSuccess]; + }]; + }]; + + [uploadTask resume]; +} + +- (void)fireNotificationWithProgress:(CGFloat)aProgress +{ + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + CGFloat progress = MAX(kAttachmentUploadProgressTheta, aProgress); + [notificationCenter postNotificationNameAsync:kAttachmentUploadProgressNotification + object:nil + userInfo:@{ + kAttachmentUploadProgressKey : @(progress), + kAttachmentUploadAttachmentIDKey : self.attachmentId + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSVerificationStateChangeMessage.h b/SignalUtilitiesKit/OWSVerificationStateChangeMessage.h new file mode 100644 index 000000000..775bd14c4 --- /dev/null +++ b/SignalUtilitiesKit/OWSVerificationStateChangeMessage.h @@ -0,0 +1,26 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSRecipientIdentity.h" +#import "TSInfoMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSThread; + +@interface OWSVerificationStateChangeMessage : TSInfoMessage + +@property (nonatomic, readonly) NSString *recipientId; +@property (nonatomic, readonly) OWSVerificationState verificationState; +@property (nonatomic, readonly) BOOL isLocalChange; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + thread:(TSThread *)thread + recipientId:(NSString *)recipientId + verificationState:(OWSVerificationState)verificationState + isLocalChange:(BOOL)isLocalChange; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSVerificationStateChangeMessage.m b/SignalUtilitiesKit/OWSVerificationStateChangeMessage.m new file mode 100644 index 000000000..8f4402281 --- /dev/null +++ b/SignalUtilitiesKit/OWSVerificationStateChangeMessage.m @@ -0,0 +1,35 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSVerificationStateChangeMessage.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSVerificationStateChangeMessage + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + thread:(TSThread *)thread + recipientId:(NSString *)recipientId + verificationState:(OWSVerificationState)verificationState + isLocalChange:(BOOL)isLocalChange +{ + OWSAssertDebug(recipientId.length > 0); + + self = [super initWithTimestamp:timestamp inThread:thread messageType:TSInfoMessageVerificationStateChange]; + if (!self) { + return self; + } + + _recipientId = recipientId; + _verificationState = verificationState; + _isLocalChange = isLocalChange; + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSVerificationStateSyncMessage.h b/SignalUtilitiesKit/OWSVerificationStateSyncMessage.h new file mode 100644 index 000000000..0c0ebdc3d --- /dev/null +++ b/SignalUtilitiesKit/OWSVerificationStateSyncMessage.h @@ -0,0 +1,27 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSOutgoingSyncMessage.h" +#import "OWSRecipientIdentity.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSVerificationStateSyncMessage : OWSOutgoingSyncMessage + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithVerificationState:(OWSVerificationState)verificationState + identityKey:(NSData *)identityKey + verificationForRecipientId:(NSString *)recipientId NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +// This is a clunky name, but we want to differentiate it from `recipientIdentifier` inherited from `TSOutgoingMessage` +@property (nonatomic, readonly) NSString *verificationForRecipientId; + +@property (nonatomic, readonly) size_t paddingBytesLength; +@property (nonatomic, readonly) size_t unpaddedVerifiedLength; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSVerificationStateSyncMessage.m b/SignalUtilitiesKit/OWSVerificationStateSyncMessage.m new file mode 100644 index 000000000..7b84416b9 --- /dev/null +++ b/SignalUtilitiesKit/OWSVerificationStateSyncMessage.m @@ -0,0 +1,111 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSVerificationStateSyncMessage.h" +#import "OWSIdentityManager.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - + +@interface OWSVerificationStateSyncMessage () + +@property (nonatomic, readonly) OWSVerificationState verificationState; +@property (nonatomic, readonly) NSData *identityKey; + +@end + +#pragma mark - + +@implementation OWSVerificationStateSyncMessage + +- (instancetype)initWithVerificationState:(OWSVerificationState)verificationState + identityKey:(NSData *)identityKey + verificationForRecipientId:(NSString *)verificationForRecipientId +{ + OWSAssertDebug(identityKey.length == kIdentityKeyLength); + OWSAssertDebug(verificationForRecipientId.length > 0); + + // we only sync user's marking as un/verified. Never sync the conflicted state, the sibling device + // will figure that out on it's own. + OWSAssertDebug(verificationState != OWSVerificationStateNoLongerVerified); + + self = [super init]; + if (!self) { + return self; + } + + _verificationState = verificationState; + _identityKey = identityKey; + _verificationForRecipientId = verificationForRecipientId; + + // This sync message should be 1-512 bytes longer than the corresponding NullMessage + // we store this values so the corresponding NullMessage can subtract it from the total length. + _paddingBytesLength = arc4random_uniform(512) + 1; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable SSKProtoSyncMessageBuilder *)syncMessageBuilder +{ + OWSAssertDebug(self.identityKey.length == kIdentityKeyLength); + OWSAssertDebug(self.verificationForRecipientId.length > 0); + + // we only sync user's marking as un/verified. Never sync the conflicted state, the sibling device + // will figure that out on it's own. + OWSAssertDebug(self.verificationState != OWSVerificationStateNoLongerVerified); + + // We add the same amount of padding in the VerificationStateSync message and it's coresponding NullMessage so that + // the sync message is indistinguishable from an outgoing Sent transcript corresponding to the NullMessage. We pad + // the NullMessage so as to obscure it's content. The sync message (like all sync messages) will be *additionally* + // padded by the superclass while being sent. The end result is we send a NullMessage of a non-distinct size, and a + // verification sync which is ~1-512 bytes larger then that. + OWSAssertDebug(self.paddingBytesLength != 0); + + SSKProtoVerified *_Nullable verifiedProto = BuildVerifiedProtoWithRecipientId( + self.verificationForRecipientId, self.identityKey, self.verificationState, self.paddingBytesLength); + if (!verifiedProto) { + OWSFailDebug(@"could not build protobuf."); + return nil; + } + + SSKProtoSyncMessageBuilder *syncMessageBuilder = [SSKProtoSyncMessage builder]; + [syncMessageBuilder setVerified:verifiedProto]; + return syncMessageBuilder; +} + +- (size_t)unpaddedVerifiedLength +{ + OWSAssertDebug(self.identityKey.length == kIdentityKeyLength); + OWSAssertDebug(self.verificationForRecipientId.length > 0); + + // we only sync user's marking as un/verified. Never sync the conflicted state, the sibling device + // will figure that out on it's own. + OWSAssertDebug(self.verificationState != OWSVerificationStateNoLongerVerified); + + SSKProtoVerified *_Nullable verifiedProto = BuildVerifiedProtoWithRecipientId( + self.verificationForRecipientId, self.identityKey, self.verificationState, 0); + if (!verifiedProto) { + OWSFailDebug(@"could not build protobuf."); + return 0; + } + NSError *error; + NSData *_Nullable verifiedData = [verifiedProto serializedDataAndReturnError:&error]; + if (error || !verifiedData) { + OWSFailDebug(@"could not serialize protobuf."); + return 0; + } + return verifiedData.length; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSWebSocket.h b/SignalUtilitiesKit/OWSWebSocket.h new file mode 100644 index 000000000..062bf0364 --- /dev/null +++ b/SignalUtilitiesKit/OWSWebSocket.h @@ -0,0 +1,55 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +static void *OWSWebSocketStateObservationContext = &OWSWebSocketStateObservationContext; + +extern NSString *const kNSNotification_OWSWebSocketStateDidChange; + +typedef NS_ENUM(NSUInteger, OWSWebSocketState) { + OWSWebSocketStateClosed, + OWSWebSocketStateConnecting, + OWSWebSocketStateOpen, +}; + +typedef void (^TSSocketMessageSuccess)(id _Nullable responseObject); +// statusCode is zero by default, if request never made or failed. +typedef void (^TSSocketMessageFailure)(NSInteger statusCode, NSData *_Nullable responseData, NSError *error); + +@class TSRequest; + +@interface OWSWebSocket : NSObject + +@property (nonatomic, readonly) OWSWebSocketState state; + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +// If the app is in the foreground, we'll try to open the socket unless it's already +// open or connecting. +// +// If the app is in the background, we'll try to open the socket unless it's already +// open or connecting _and_ keep it open for at least N seconds. +// If the app is in the background and the socket is already open or connecting this +// might prolong how long we keep the socket open. +// +// This method can be called from any thread. +- (void)requestSocketOpen; + +// This can be used to force the socket to close and re-open, if it is open. +- (void)cycleSocket; + +#pragma mark - Message Sending + +@property (atomic, readonly) BOOL canMakeRequests; + +- (void)makeRequest:(TSRequest *)request + success:(TSSocketMessageSuccess)success + failure:(TSSocketMessageFailure)failure; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OWSWebSocket.m b/SignalUtilitiesKit/OWSWebSocket.m new file mode 100644 index 000000000..3be2ee136 --- /dev/null +++ b/SignalUtilitiesKit/OWSWebSocket.m @@ -0,0 +1,1141 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSWebSocket.h" +#import "AppContext.h" +#import "AppReadiness.h" +#import "NSNotificationCenter+OWS.h" +#import "NSTimer+OWS.h" +#import "NotificationsProtocol.h" +#import "OWSBackgroundTask.h" +#import "OWSDevicesService.h" +#import "OWSError.h" +#import "OWSMessageManager.h" +#import "OWSMessageReceiver.h" +#import "OWSPrimaryStorage.h" +#import "OWSSignalService.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSConstants.h" +#import "TSErrorMessage.h" +#import "TSRequest.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static const CGFloat kSocketHeartbeatPeriodSeconds = 30.f; +static const CGFloat kSocketReconnectDelaySeconds = 5.f; + +// If the app is in the background, it should keep the +// websocket open if: +// +// a) It has received a notification in the last 25 seconds. +static const CGFloat kBackgroundOpenSocketDurationSeconds = 25.f; +// b) It has received a message over the socket in the last 15 seconds. +static const CGFloat kBackgroundKeepSocketAliveDurationSeconds = 15.f; +// c) It is in the process of making a request. +static const CGFloat kMakeRequestKeepSocketAliveDurationSeconds = 30.f; + +NSString *const kNSNotification_OWSWebSocketStateDidChange = @"kNSNotification_OWSWebSocketStateDidChange"; + +@interface TSSocketMessage : NSObject + +@property (nonatomic, readonly) UInt64 requestId; +@property (nonatomic, nullable) TSSocketMessageSuccess success; +@property (nonatomic, nullable) TSSocketMessageFailure failure; +@property (nonatomic) BOOL hasCompleted; +@property (nonatomic, readonly) OWSBackgroundTask *backgroundTask; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +#pragma mark - + +@implementation TSSocketMessage + +- (instancetype)initWithRequestId:(UInt64)requestId + success:(TSSocketMessageSuccess)success + failure:(TSSocketMessageFailure)failure +{ + if (self = [super init]) { + OWSAssertDebug(success); + OWSAssertDebug(failure); + + _requestId = requestId; + _success = success; + _failure = failure; + _backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + } + + return self; +} + +- (void)didSucceedWithResponseObject:(id _Nullable)responseObject +{ + @synchronized(self) { + if (self.hasCompleted) { + return; + } + self.hasCompleted = YES; + } + + OWSAssertDebug(self.success); + OWSAssertDebug(self.failure); + + TSSocketMessageSuccess success = self.success; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + success(responseObject); + }); + + self.success = nil; + self.failure = nil; +} + +- (void)timeoutIfNecessary +{ + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageRequestFailed, + NSLocalizedString( + @"ERROR_DESCRIPTION_REQUEST_TIMED_OUT", @"Error indicating that a socket request timed out.")); + + [self didFailWithStatusCode:0 responseData:nil error:error]; +} + +- (void)didFailBeforeSending +{ + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageRequestFailed, + NSLocalizedString(@"ERROR_DESCRIPTION_REQUEST_FAILED", @"Error indicating that a socket request failed.")); + + [self didFailWithStatusCode:0 responseData:nil error:error]; +} + +- (void)didFailWithStatusCode:(NSInteger)statusCode responseData:(nullable NSData *)responseData error:(NSError *)error +{ + OWSAssertDebug(error); + + @synchronized(self) { + if (self.hasCompleted) { + return; + } + self.hasCompleted = YES; + } + + OWSLogError(@"didFailWithStatusCode: %zd, %@", statusCode, error); + + OWSAssertDebug(self.success); + OWSAssertDebug(self.failure); + + TSSocketMessageFailure failure = self.failure; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + failure(statusCode, responseData, error); + }); + + self.success = nil; + self.failure = nil; +} + +@end + +#pragma mark - + +// OWSWebSocket's properties should only be accessed from the main thread. +@interface OWSWebSocket () + +// This class has a few "tiers" of state. +// +// The first tier is the actual websocket and the timers used +// to keep it alive and connected. +@property (nonatomic, nullable) id websocket; +@property (nonatomic, nullable) NSTimer *heartbeatTimer; +@property (nonatomic, nullable) NSTimer *reconnectTimer; + +#pragma mark - + +// The second tier is the state property. We initiate changes +// to the websocket by changing this property's value, and delegate +// events from the websocket also update this value as the websocket's +// state changes. +// +// Due to concurrency, this property can fall out of sync with the +// websocket's actual state, so we're defensive and distrustful of +// this property. +// +// We only ever access this state on the main thread. +@property (nonatomic) OWSWebSocketState state; + +#pragma mark - + +// The third tier is the state that is used to determine what the +// "desired" state of the websocket is. +// +// If we're keeping the socket open in the background, all three of these +// properties will be set. Otherwise (if the app is active or if we're not +// trying to keep the socket open), all three should be clear. +// +// This represents how long we're trying to keep the socket open. +@property (nonatomic, nullable) NSDate *backgroundKeepAliveUntilDate; +// This timer is used to check periodically whether we should +// close the socket. +@property (nonatomic, nullable) NSTimer *backgroundKeepAliveTimer; +// This is used to manage the iOS "background task" used to +// keep the app alive in the background. +@property (nonatomic, nullable) OWSBackgroundTask *backgroundTask; + +// We cache this value instead of consulting [UIApplication sharedApplication].applicationState, +// because UIKit only provides a "will resign active" notification, not a "did resign active" +// notification. +@property (nonatomic) BOOL appIsActive; + +@property (nonatomic) BOOL hasObservedNotifications; + +// This property should only be accessed while synchronized on the socket manager. +@property (nonatomic, readonly) NSMutableDictionary *socketMessageMap; + +@property (atomic) BOOL canMakeRequests; + +@end + +#pragma mark - + +@implementation OWSWebSocket + +- (instancetype)init +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSAssertIsOnMainThread(); + + _state = OWSWebSocketStateClosed; + _socketMessageMap = [NSMutableDictionary new]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Dependencies + +- (OWSSignalService *)signalService +{ + return [OWSSignalService sharedInstance]; +} + +- (OWSMessageReceiver *)messageReceiver +{ + return SSKEnvironment.shared.messageReceiver; +} + +- (TSAccountManager *)tsAccountManager +{ + return TSAccountManager.sharedInstance; +} + +- (OutageDetection *)outageDetection +{ + return OutageDetection.sharedManager; +} + +- (OWSPrimaryStorage *)primaryStorage +{ + return SSKEnvironment.shared.primaryStorage; +} + +- (id)notificationsManager +{ + return SSKEnvironment.shared.notificationsManager; +} + +- (id)udManager { + return SSKEnvironment.shared.udManager; +} + +#pragma mark - + +// We want to observe these notifications lazily to avoid accessing +// the data store in [application: didFinishLaunchingWithOptions:]. +- (void)observeNotificationsIfNecessary +{ + if (self.hasObservedNotifications) { + return; + } + self.hasObservedNotifications = YES; + + self.appIsActive = CurrentAppContext().isMainAppAndActive; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:OWSApplicationDidBecomeActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResignActive:) + name:OWSApplicationWillResignActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(registrationStateDidChange:) + name:RegistrationStateDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(isCensorshipCircumventionActiveDidChange:) + name:kNSNotificationName_IsCensorshipCircumventionActiveDidChange + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deviceListUpdateModifiedDeviceList:) + name:NSNotificationName_DeviceListUpdateModifiedDeviceList + object:nil]; +} + +#pragma mark - Manage Socket + +- (void)ensureWebsocketIsOpen +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(!self.signalService.isCensorshipCircumventionActive); + + // Try to reuse the existing socket (if any) if it is in a valid state. + if (self.websocket) { + switch (self.websocket.state) { + case SSKWebSocketStateOpen: + self.state = OWSWebSocketStateOpen; + return; + case SSKWebSocketStateConnecting: + OWSLogVerbose(@"WebSocket is already connecting"); + self.state = OWSWebSocketStateConnecting; + return; + default: + break; + } + } + + OWSLogWarn(@"Creating new websocket"); + + // If socket is not already open or connecting, connect now. + // + // First we need to close the existing websocket, if any. + // The websocket delegate methods are invoked _after_ the websocket + // state changes, so we may be just learning about a socket failure + // or close event now. + self.state = OWSWebSocketStateClosed; + // Now open a new socket. + self.state = OWSWebSocketStateConnecting; +} + +- (NSString *)stringFromOWSWebSocketState:(OWSWebSocketState)state +{ + switch (state) { + case OWSWebSocketStateClosed: + return @"Closed"; + case OWSWebSocketStateOpen: + return @"Open"; + case OWSWebSocketStateConnecting: + return @"Connecting"; + } +} + +// We need to keep websocket state and class state tightly aligned. +// +// Sometimes we'll need to update class state to reflect changes +// in socket state; sometimes we'll need to update socket state +// and class state to reflect changes in app state. +// +// We learn about changes to socket state through websocket +// delegate methods. These delegate methods are sometimes +// invoked _after_ web socket state changes, so we sometimes learn +// about changes to socket state in [ensureWebsocket]. Put another way, +// it's not safe to assume we'll learn of changes to websocket state +// in the websocket delegate methods. +// +// Therefore, we use the [setState:] setter to ensure alignment between +// websocket state and class state. +- (void)setState:(OWSWebSocketState)state +{ + OWSAssertIsOnMainThread(); + + // If this state update is redundant, verify that + // class state and socket state are aligned. + // + // Note: it's not safe to check the socket's readyState here as + // it may have been just updated on another thread. If so, + // we'll learn of that state change soon. + if (_state == state) { + switch (state) { + case OWSWebSocketStateClosed: + OWSAssertDebug(!self.websocket); + break; + case OWSWebSocketStateOpen: + OWSAssertDebug(self.websocket); + break; + case OWSWebSocketStateConnecting: + OWSAssertDebug(self.websocket); + break; + } + return; + } + + OWSLogWarn( + @"Socket state: %@ -> %@", [self stringFromOWSWebSocketState:_state], [self stringFromOWSWebSocketState:state]); + + // If this state update is _not_ redundant, + // update class state to reflect the new state. + switch (state) { + case OWSWebSocketStateClosed: { + [self resetSocket]; + break; + } + case OWSWebSocketStateOpen: { + OWSAssertDebug(self.state == OWSWebSocketStateConnecting); + + self.heartbeatTimer = [NSTimer timerWithTimeInterval:kSocketHeartbeatPeriodSeconds + target:self + selector:@selector(webSocketHeartBeat) + userInfo:nil + repeats:YES]; + + // Additionally, we want the ping timer to work in the background too. + [[NSRunLoop mainRunLoop] addTimer:self.heartbeatTimer forMode:NSDefaultRunLoopMode]; + + // If the socket is open, we don't need to worry about reconnecting. + [self clearReconnect]; + break; + } + case OWSWebSocketStateConnecting: { + // Discard the old socket which is already closed or is closing. + [self resetSocket]; + + // Create a new web socket. + NSString *webSocketConnect = + [textSecureWebSocketAPI stringByAppendingString:[self webSocketAuthenticationString]]; + NSURL *webSocketConnectURL = [NSURL URLWithString:webSocketConnect]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:webSocketConnectURL]; + + id socket = [SSKWebSocketManager buildSocketWithRequest:request]; + socket.delegate = self; + + [self setWebsocket:socket]; + + // `connect` could hypothetically call a delegate method (e.g. if + // the socket failed immediately for some reason), so we update the state + // _before_ calling it, not after. + _state = state; + self.canMakeRequests = state == OWSWebSocketStateOpen; + [socket connect]; + [self failAllPendingSocketMessagesIfNecessary]; + return; + } + } + + _state = state; + self.canMakeRequests = state == OWSWebSocketStateOpen; + + [self failAllPendingSocketMessagesIfNecessary]; + [self notifyStatusChange]; +} + +- (void)notifyStatusChange +{ + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotification_OWSWebSocketStateDidChange + object:nil + userInfo:nil]; +} + +#pragma mark - + +- (void)resetSocket +{ + OWSAssertIsOnMainThread(); + + self.websocket.delegate = nil; + [self.websocket disconnect]; + self.websocket = nil; + [self.heartbeatTimer invalidate]; + self.heartbeatTimer = nil; +} + +- (void)closeWebSocket +{ + OWSAssertIsOnMainThread(); + + if (self.websocket) { + OWSLogWarn(@"closeWebSocket."); + } + + self.state = OWSWebSocketStateClosed; +} + +#pragma mark - Message Sending + +- (void)makeRequest:(TSRequest *)request success:(TSSocketMessageSuccess)success failure:(TSSocketMessageFailure)failure +{ + OWSAssertDebug(request); + OWSAssertDebug(request.HTTPMethod.length > 0); + OWSAssertDebug(success); + OWSAssertDebug(failure); + + TSSocketMessage *socketMessage = [[TSSocketMessage alloc] initWithRequestId:[Cryptography randomUInt64] + success:success + failure:failure]; + + @synchronized(self) { + self.socketMessageMap[@(socketMessage.requestId)] = socketMessage; + } + + NSURL *requestUrl = request.URL; + NSString *requestPath = [@"/" stringByAppendingString:requestUrl.path]; + + NSData *_Nullable jsonData = nil; + if (request.parameters) { + NSError *error; + jsonData = + [NSJSONSerialization dataWithJSONObject:request.parameters options:(NSJSONWritingOptions)0 error:&error]; + if (!jsonData || error) { + OWSFailDebug(@"could not serialize request JSON: %@", error); + [socketMessage didFailBeforeSending]; + return; + } + } + + WebSocketProtoWebSocketRequestMessageBuilder *requestBuilder = + [WebSocketProtoWebSocketRequestMessage builderWithVerb:request.HTTPMethod + path:requestPath + requestID:socketMessage.requestId]; + if (jsonData) { + // TODO: Do we need body & headers for requests with no parameters? + [requestBuilder setBody:jsonData]; + [requestBuilder addHeaders:@"content-type:application/json"]; + } + + for (NSString *headerField in request.allHTTPHeaderFields) { + NSString *headerValue = request.allHTTPHeaderFields[headerField]; + + OWSAssertDebug([headerField isKindOfClass:[NSString class]]); + OWSAssertDebug([headerValue isKindOfClass:[NSString class]]); + [requestBuilder addHeaders:[NSString stringWithFormat:@"%@:%@", headerField, headerValue]]; + } + + NSError *error; + WebSocketProtoWebSocketRequestMessage *_Nullable requestProto = [requestBuilder buildAndReturnError:&error]; + if (!requestProto || error) { + OWSFailDebug(@"could not build proto: %@", error); + return; + } + + WebSocketProtoWebSocketMessageBuilder *messageBuilder = + [WebSocketProtoWebSocketMessage builderWithType:WebSocketProtoWebSocketMessageTypeRequest]; + [messageBuilder setRequest:requestProto]; + + NSData *_Nullable messageData = [messageBuilder buildSerializedDataAndReturnError:&error]; + if (!messageData || error) { + OWSFailDebug(@"could not serialize proto: %@.", error); + [socketMessage didFailBeforeSending]; + return; + } + + if (!self.canMakeRequests) { + OWSLogError(@"makeRequest: socket not open."); + [socketMessage didFailBeforeSending]; + return; + } + + BOOL wasScheduled = [self.websocket writeData:messageData error:&error]; + if (!wasScheduled || error) { + OWSFailDebug(@"could not send socket request: %@", error); + [socketMessage didFailBeforeSending]; + return; + } + OWSLogInfo(@"making request: %llu, %@: %@, jsonData.length: %zd", + socketMessage.requestId, + request.HTTPMethod, + requestPath, + jsonData.length); + + const int64_t kSocketTimeoutSeconds = 10; + __weak TSSocketMessage *weakSocketMessage = socketMessage; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kSocketTimeoutSeconds * NSEC_PER_SEC), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + ^{ + [weakSocketMessage timeoutIfNecessary]; + }); +} + +- (void)processWebSocketResponseMessage:(WebSocketProtoWebSocketResponseMessage *)message +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(message); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self processWebSocketResponseMessageAsync:message]; + }); +} + +- (void)processWebSocketResponseMessageAsync:(WebSocketProtoWebSocketResponseMessage *)message +{ + OWSAssertDebug(message); + + OWSLogInfo(@"received WebSocket response requestId: %llu, status: %u", message.requestID, message.status); + + DispatchMainThreadSafe(^{ + [self requestSocketAliveForAtLeastSeconds:kMakeRequestKeepSocketAliveDurationSeconds]; + }); + + UInt64 requestId = message.requestID; + UInt32 responseStatus = message.status; + NSString *_Nullable responseMessage; + if (message.hasMessage) { + responseMessage = message.message; + } + NSData *_Nullable responseData; + if (message.hasBody) { + responseData = message.body; + } + + BOOL hasValidResponse = YES; + id responseObject = responseData; + if (responseData) { + NSError *error; + id _Nullable responseJson = + [NSJSONSerialization JSONObjectWithData:responseData options:(NSJSONReadingOptions)0 error:&error]; + if (!responseJson || error) { + OWSFailDebug(@"could not parse WebSocket response JSON: %@.", error); + hasValidResponse = NO; + } else { + responseObject = responseJson; + } + } + + TSSocketMessage *_Nullable socketMessage; + @synchronized(self) { + socketMessage = self.socketMessageMap[@(requestId)]; + [self.socketMessageMap removeObjectForKey:@(requestId)]; + } + + if (!socketMessage) { + OWSLogError(@"received response to unknown request."); + } else { + BOOL hasSuccessStatus = 200 <= responseStatus && responseStatus <= 299; + BOOL didSucceed = hasSuccessStatus && hasValidResponse; + if (didSucceed) { + [self.tsAccountManager setIsDeregistered:NO]; + + [socketMessage didSucceedWithResponseObject:responseObject]; + } else { + if (responseStatus == 403) { + // This should be redundant with our check for the socket + // failing due to 403, but let's be thorough. + if (self.tsAccountManager.isRegisteredAndReady) { + [self.tsAccountManager setIsDeregistered:YES]; + } else { + OWSFailDebug(@"Ignoring auth failure; not registered and ready."); + } + } + + NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageResponseFailed, + NSLocalizedString( + @"ERROR_DESCRIPTION_RESPONSE_FAILED", @"Error indicating that a socket response failed.")); + [socketMessage didFailWithStatusCode:(NSInteger)responseStatus responseData:responseData error:error]; + } + } +} + +- (void)failAllPendingSocketMessagesIfNecessary +{ + if (!self.canMakeRequests) { + [self failAllPendingSocketMessages]; + } +} + +- (void)failAllPendingSocketMessages +{ + NSArray *socketMessages; + @synchronized(self) { + socketMessages = self.socketMessageMap.allValues; + [self.socketMessageMap removeAllObjects]; + } + + OWSLogInfo(@"failAllPendingSocketMessages: %zd.", socketMessages.count); + + for (TSSocketMessage *socketMessage in socketMessages) { + [socketMessage didFailBeforeSending]; + } +} + +#pragma mark - SSKWebSocketDelegate + +- (void)websocketDidConnectWithSocket:(id)websocket +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(websocket); + if (websocket != self.websocket) { + // Ignore events from obsolete web sockets. + return; + } + + self.state = OWSWebSocketStateOpen; + + // If socket opens, we know we're not de-registered. + [self.tsAccountManager setIsDeregistered:NO]; + + [self.outageDetection reportConnectionSuccess]; +} + +- (void)websocketDidDisconnectWithSocket:(id)websocket error:(nullable NSError *)error +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(websocket); + if (websocket != self.websocket) { + // Ignore events from obsolete web sockets. + return; + } + + OWSLogError(@"Websocket did fail with error: %@", error); + if ([error.domain isEqualToString:SSKWebSocketError.errorDomain]) { + NSNumber *_Nullable statusCode = error.userInfo[SSKWebSocketError.kStatusCodeKey]; + if (statusCode.unsignedIntegerValue == 403) { + if (self.tsAccountManager.isRegisteredAndReady) { + [self.tsAccountManager setIsDeregistered:YES]; + } else { + OWSLogWarn(@"Ignoring auth failure; not registered and ready."); + } + } + } + + [self handleSocketFailure]; +} + +- (void)websocketDidReceiveDataWithSocket:(id)websocket data:(NSData *)data +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(websocket); + + if (websocket != self.websocket) { + // Ignore events from obsolete web sockets. + return; + } + + // If we receive a response, we know we're not de-registered. + [self.tsAccountManager setIsDeregistered:NO]; + + NSError *error; + WebSocketProtoWebSocketMessage *_Nullable wsMessage = [WebSocketProtoWebSocketMessage parseData:data error:&error]; + if (!wsMessage || error) { + OWSFailDebug(@"could not parse proto: %@", error); + return; + } + + if (wsMessage.type == WebSocketProtoWebSocketMessageTypeRequest) { + [self processWebSocketRequestMessage:wsMessage.request]; + } else if (wsMessage.type == WebSocketProtoWebSocketMessageTypeResponse) { + [self processWebSocketResponseMessage:wsMessage.response]; + } else { + OWSLogWarn(@"webSocket:didReceiveMessage: unknown."); + } +} + +#pragma mark - + +- (dispatch_queue_t)serialQueue +{ + static dispatch_queue_t _serialQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _serialQueue = dispatch_queue_create("org.signal.websocket", DISPATCH_QUEUE_SERIAL); + }); + + return _serialQueue; +} + +- (void)processWebSocketRequestMessage:(WebSocketProtoWebSocketRequestMessage *)message +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@"Got message with verb: %@ and path: %@", message.verb, message.path); + + // If we receive a message over the socket while the app is in the background, + // prolong how long the socket stays open. + [self requestSocketAliveForAtLeastSeconds:kBackgroundKeepSocketAliveDurationSeconds]; + + if ([message.path isEqualToString:@"/api/v1/message"] && [message.verb isEqualToString:@"PUT"]) { + + __block OWSBackgroundTask *_Nullable backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__]; + + dispatch_async(self.serialQueue, ^{ + BOOL success = NO; + @try { + BOOL useSignalingKey = [message.headers containsObject:@"X-Signal-Key: true"]; + NSData *_Nullable decryptedPayload; + if (useSignalingKey) { + NSString *_Nullable signalingKey = TSAccountManager.signalingKey; + OWSAssertDebug(signalingKey); + decryptedPayload = + [Cryptography decryptAppleMessagePayload:message.body withSignalingKey:signalingKey]; + } else { + OWSAssertDebug([message.headers containsObject:@"X-Signal-Key: false"]); + + decryptedPayload = message.body; + } + + if (!decryptedPayload) { + OWSLogWarn(@"Failed to decrypt incoming payload or bad HMAC"); + } else { + [self.messageReceiver handleReceivedEnvelopeData:decryptedPayload]; + success = YES; + } + } @catch (NSException *exception) { + OWSFailDebug(@"Received an invalid envelope: %@", exception.debugDescription); + // TODO: Add analytics. + } + + if (!success) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + TSErrorMessage *errorMessage = [TSErrorMessage corruptedMessageInUnknownThread]; + [self.notificationsManager notifyUserForThreadlessErrorMessage:errorMessage + transaction:transaction]; + }]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [self sendWebSocketMessageAcknowledgement:message]; + OWSAssertDebug(backgroundTask); + backgroundTask = nil; + }); + }); + } else if ([message.path isEqualToString:@"/api/v1/queue/empty"]) { + // Queue is drained. + + [self sendWebSocketMessageAcknowledgement:message]; + } else { + OWSLogWarn(@"Unsupported WebSocket Request"); + + [self sendWebSocketMessageAcknowledgement:message]; + } +} + +- (void)sendWebSocketMessageAcknowledgement:(WebSocketProtoWebSocketRequestMessage *)request +{ + OWSAssertIsOnMainThread(); + + NSError *error; + + WebSocketProtoWebSocketResponseMessageBuilder *responseBuilder = + [WebSocketProtoWebSocketResponseMessage builderWithRequestID:request.requestID status:200]; + [responseBuilder setMessage:@"OK"]; + WebSocketProtoWebSocketResponseMessage *_Nullable response = [responseBuilder buildAndReturnError:&error]; + if (!response || error) { + OWSFailDebug(@"could not build proto: %@", error); + return; + } + + WebSocketProtoWebSocketMessageBuilder *messageBuilder = + [WebSocketProtoWebSocketMessage builderWithType:WebSocketProtoWebSocketMessageTypeResponse]; + [messageBuilder setResponse:response]; + + NSData *_Nullable messageData = [messageBuilder buildSerializedDataAndReturnError:&error]; + if (!messageData || error) { + OWSFailDebug(@"could not serialize proto: %@", error); + return; + } + + [self.websocket writeData:messageData error:&error]; + if (error) { + OWSLogWarn(@"Error while trying to write on websocket %@", error); + [self handleSocketFailure]; + } +} + +- (void)cycleSocket +{ + OWSAssertIsOnMainThread(); + + [self closeWebSocket]; + + [self applyDesiredSocketState]; +} + +- (void)handleSocketFailure +{ + OWSAssertIsOnMainThread(); + + [self closeWebSocket]; + + if ([self shouldSocketBeOpen]) { + // If we should retry, use `ensureReconnect` to + // reconnect after a delay. + [self ensureReconnect]; + } else { + // Otherwise clean up and align state. + [self applyDesiredSocketState]; + } + + [self.outageDetection reportConnectionFailure]; +} + +- (void)webSocketHeartBeat +{ + OWSAssertIsOnMainThread(); + + if ([self shouldSocketBeOpen]) { + NSError *error; + [self.websocket writePingAndReturnError:&error]; + if (error) { + OWSLogWarn(@"Error in websocket heartbeat: %@", error.localizedDescription); + [self handleSocketFailure]; + } + } else { + OWSLogWarn(@"webSocketHeartBeat closing web socket"); + [self closeWebSocket]; + [self applyDesiredSocketState]; + } +} + +- (NSString *)webSocketAuthenticationString +{ + return [NSString stringWithFormat:@"?login=%@&password=%@", + [[TSAccountManager localNumber] stringByReplacingOccurrencesOfString:@"+" withString:@"%2B"], + [TSAccountManager serverAuthToken]]; +} + +#pragma mark - Socket LifeCycle + +- (BOOL)shouldSocketBeOpen +{ + OWSAssertIsOnMainThread(); + + // Loki: Since we don't use web sockets, disable them + return NO; + + // Don't open socket in app extensions. + if (!CurrentAppContext().isMainApp) { + return NO; + } + + if (![self.tsAccountManager isRegisteredAndReady]) { + return NO; + } + + if (self.signalService.isCensorshipCircumventionActive) { + OWSLogWarn(@"Skipping opening of websocket due to censorship circumvention."); + return NO; + } + + if (self.appIsActive) { + // If app is active, keep web socket alive. + return YES; + } else if (self.backgroundKeepAliveUntilDate && [self.backgroundKeepAliveUntilDate timeIntervalSinceNow] > 0.f) { + OWSAssertDebug(self.backgroundKeepAliveTimer); + // If app is doing any work in the background, keep web socket alive. + return YES; + } else { + return NO; + } +} + +- (void)requestSocketAliveForAtLeastSeconds:(CGFloat)durationSeconds +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(durationSeconds > 0.f); + + if (self.appIsActive) { + // If app is active, clean up state used to keep socket alive in background. + [self clearBackgroundState]; + } else if (!self.backgroundKeepAliveUntilDate) { + OWSAssertDebug(!self.backgroundKeepAliveUntilDate); + OWSAssertDebug(!self.backgroundKeepAliveTimer); + + OWSLogInfo(@"activating socket in the background"); + + // Set up state used to keep socket alive in background. + self.backgroundKeepAliveUntilDate = [NSDate dateWithTimeIntervalSinceNow:durationSeconds]; + + // To be defensive, clean up any existing backgroundKeepAliveTimer. + [self.backgroundKeepAliveTimer invalidate]; + // Start a new timer that will fire every second while the socket is open in the background. + // This timer will ensure we close the websocket when the time comes. + self.backgroundKeepAliveTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f + target:self + selector:@selector(backgroundKeepAliveFired) + userInfo:nil + repeats:YES]; + // Additionally, we want the reconnect timer to work in the background too. + [[NSRunLoop mainRunLoop] addTimer:self.backgroundKeepAliveTimer forMode:NSDefaultRunLoopMode]; + + __weak typeof(self) weakSelf = self; + self.backgroundTask = + [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__ + completionBlock:^(BackgroundTaskState backgroundTaskState) { + OWSAssertIsOnMainThread(); + __strong typeof(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + if (backgroundTaskState == BackgroundTaskState_Expired) { + [strongSelf clearBackgroundState]; + } + [strongSelf applyDesiredSocketState]; + }]; + } else { + OWSAssertDebug(self.backgroundKeepAliveUntilDate); + OWSAssertDebug(self.backgroundKeepAliveTimer); + OWSAssertDebug([self.backgroundKeepAliveTimer isValid]); + + if ([self.backgroundKeepAliveUntilDate timeIntervalSinceNow] < durationSeconds) { + // Update state used to keep socket alive in background. + self.backgroundKeepAliveUntilDate = [NSDate dateWithTimeIntervalSinceNow:durationSeconds]; + } + } + + [self applyDesiredSocketState]; +} + +- (void)backgroundKeepAliveFired +{ + OWSAssertIsOnMainThread(); + + [self applyDesiredSocketState]; +} + +- (void)requestSocketOpen +{ + DispatchMainThreadSafe(^{ + [self observeNotificationsIfNecessary]; + + // If the app is active and the user is registered, this will + // simply open the websocket. + // + // If the app is inactive, it will open the websocket for a + // period of time. + [self requestSocketAliveForAtLeastSeconds:kBackgroundOpenSocketDurationSeconds]; + }); +} + +// This method aligns the socket state with the "desired" socket state. +- (void)applyDesiredSocketState +{ + OWSAssertIsOnMainThread(); + +#ifdef DEBUG + if (CurrentAppContext().isRunningTests) { + OWSLogWarn(@"Suppressing socket in tests."); + return; + } +#endif + + if (!AppReadiness.isAppReady) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [self applyDesiredSocketState]; + }]; + }); + return; + } + + if ([self shouldSocketBeOpen]) { + if (self.state != OWSWebSocketStateOpen) { + // If we want the socket to be open and it's not open, + // start up the reconnect timer immediately (don't wait for an error). + // There's little harm in it and this will make us more robust to edge + // cases. + [self ensureReconnect]; + } + [self ensureWebsocketIsOpen]; + } else { + [self clearBackgroundState]; + [self clearReconnect]; + [self closeWebSocket]; + } +} + +- (void)clearBackgroundState +{ + OWSAssertIsOnMainThread(); + + self.backgroundKeepAliveUntilDate = nil; + [self.backgroundKeepAliveTimer invalidate]; + self.backgroundKeepAliveTimer = nil; + self.backgroundTask = nil; +} + +#pragma mark - Reconnect + +- (void)ensureReconnect +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug([self shouldSocketBeOpen]); + + if (self.reconnectTimer) { + OWSAssertDebug([self.reconnectTimer isValid]); + } else { + // TODO: It'd be nice to do exponential backoff. + self.reconnectTimer = [NSTimer timerWithTimeInterval:kSocketReconnectDelaySeconds + target:self + selector:@selector(applyDesiredSocketState) + userInfo:nil + repeats:YES]; + // Additionally, we want the reconnect timer to work in the background too. + [[NSRunLoop mainRunLoop] addTimer:self.reconnectTimer forMode:NSDefaultRunLoopMode]; + } +} + +- (void)clearReconnect +{ + OWSAssertIsOnMainThread(); + + [self.reconnectTimer invalidate]; + self.reconnectTimer = nil; +} + +#pragma mark - Notifications + +- (void)applicationDidBecomeActive:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + self.appIsActive = YES; + [self applyDesiredSocketState]; +} + +- (void)applicationWillResignActive:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + self.appIsActive = NO; + // TODO: It might be nice to use `requestSocketAliveForAtLeastSeconds:` to + // keep the socket open for a few seconds after the app is + // inactivated. + [self applyDesiredSocketState]; +} + +- (void)registrationStateDidChange:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [self applyDesiredSocketState]; +} + +- (void)isCensorshipCircumventionActiveDidChange:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [self applyDesiredSocketState]; +} + +- (void)deviceListUpdateModifiedDeviceList:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [self cycleSocket]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/OldSnodeAPI.swift b/SignalUtilitiesKit/OldSnodeAPI.swift new file mode 100644 index 000000000..fa3809a8e --- /dev/null +++ b/SignalUtilitiesKit/OldSnodeAPI.swift @@ -0,0 +1,44 @@ +import PromiseKit + +@objc(LKSnodeAPI) +public final class OldSnodeAPI : NSObject { + + // MARK: Sending + @objc(sendSignalMessage:) + public static func objc_sendSignalMessage(_ signalMessage: SignalMessage) -> AnyPromise { + let promise = sendSignalMessage(signalMessage).mapValues2 { AnyPromise.from($0) }.map2 { Set($0) } + return AnyPromise.from(promise) + } + + public static func sendSignalMessage(_ signalMessage: SignalMessage) -> Promise>> { + // Convert the message to a Loki message + guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: SnodeAPI.Error.generic) } + let publicKey = lokiMessage.recipientPublicKey + let notificationCenter = NotificationCenter.default + notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp)) + // Calculate proof of work + return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise>> in + notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp)) + // Get the target snodes + return SnodeAPI.getTargetSnodes(for: publicKey).map2 { snodes in + notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp)) + let parameters = lokiMessageWithPoW.toJSON() + return Set(snodes.map { snode in + // Send the message to the target snode + return attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) { + SnodeAPI.invoke(.sendMessage, on: snode, associatedWith: publicKey, parameters: parameters) + }.map2 { rawResponse in + if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int { + guard powDifficulty != SnodeAPI.powDifficulty, powDifficulty < 100 else { return rawResponse } + print("[Loki] Setting proof of work difficulty to \(powDifficulty).") + SnodeAPI.powDifficulty = UInt(powDifficulty) + } else { + print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).") + } + return rawResponse + } + }) + } + } + } +} diff --git a/SignalUtilitiesKit/OnionRequestAPI+Encryption.swift b/SignalUtilitiesKit/OnionRequestAPI+Encryption.swift new file mode 100644 index 000000000..77a195950 --- /dev/null +++ b/SignalUtilitiesKit/OnionRequestAPI+Encryption.swift @@ -0,0 +1,72 @@ +import CryptoSwift +import PromiseKit + +extension OnionRequestAPI { + + internal static func encode(ciphertext: Data, json: JSON) throws -> Data { + // The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 | + guard JSONSerialization.isValidJSONObject(json) else { throw HTTP.Error.invalidJSON } + let jsonAsData = try JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ]) + let ciphertextSize = Int32(ciphertext.count).littleEndian + let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: MemoryLayout.size) } + return ciphertextSizeAsData + ciphertext + jsonAsData + } + + /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. + internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise { + let (promise, seal) = Promise.pending() + DispatchQueue.global(qos: .userInitiated).async { + do { + guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) } + // Wrapping isn't needed for file server or open group onion requests + switch destination { + case .snode(let snode): + let snodeX25519PublicKey = snode.publicKeySet.x25519Key + let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) + let plaintext = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ]) + let result = try EncryptionUtilities.encrypt(plaintext, using: snodeX25519PublicKey) + seal.fulfill(result) + case .server(_, let serverX25519PublicKey): + let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) + let result = try EncryptionUtilities.encrypt(plaintext, using: serverX25519PublicKey) + seal.fulfill(result) + } + } catch (let error) { + seal.reject(error) + } + } + return promise + } + + /// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request. + internal static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: EncryptionResult) -> Promise { + let (promise, seal) = Promise.pending() + DispatchQueue.global(qos: .userInitiated).async { + var parameters: JSON + switch rhs { + case .snode(let snode): + let snodeED25519PublicKey = snode.publicKeySet.ed25519Key + parameters = [ "destination" : snodeED25519PublicKey ] + case .server(let host, _): + parameters = [ "host" : host, "target" : "/loki/v2/lsrpc", "method" : "POST" ] + } + parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() + let x25519PublicKey: String + switch lhs { + case .snode(let snode): + let snodeX25519PublicKey = snode.publicKeySet.x25519Key + x25519PublicKey = snodeX25519PublicKey + case .server(_, let serverX25519PublicKey): + x25519PublicKey = serverX25519PublicKey + } + do { + let plaintext = try encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters) + let result = try EncryptionUtilities.encrypt(plaintext, using: x25519PublicKey) + seal.fulfill(result) + } catch (let error) { + seal.reject(error) + } + } + return promise + } +} diff --git a/SignalUtilitiesKit/OutageDetection.swift b/SignalUtilitiesKit/OutageDetection.swift new file mode 100644 index 000000000..2717dc570 --- /dev/null +++ b/SignalUtilitiesKit/OutageDetection.swift @@ -0,0 +1,128 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import os + +@objc +public class OutageDetection: NSObject { + @objc(sharedManager) + public static let shared = OutageDetection() + + @objc public static let outageStateDidChange = Notification.Name("OutageStateDidChange") + + // These properties should only be accessed on the main thread. + @objc + public var hasOutage = false { + didSet { + AssertIsOnMainThread() + + if hasOutage != oldValue { + Logger.info("hasOutage: \(hasOutage).") + + NotificationCenter.default.postNotificationNameAsync(OutageDetection.outageStateDidChange, object: nil) + } + } + } + private var shouldCheckForOutage = false { + didSet { + // Loki: Don't check for outages +// AssertIsOnMainThread() +// ensureCheckTimer() + } + } + + // We only show the outage warning when we're certain there's an outage. + // DNS lookup failures, etc. are not considered an outage. + private func checkForOutageSync() -> Bool { + let host = CFHostCreateWithName(nil, "uptime.signal.org" as CFString).takeRetainedValue() + CFHostStartInfoResolution(host, .addresses, nil) + var success: DarwinBoolean = false + guard let addresses = CFHostGetAddressing(host, &success)?.takeUnretainedValue() as NSArray? else { + Logger.error("CFHostGetAddressing failed: no addresses.") + return false + } + guard success.boolValue else { + Logger.error("CFHostGetAddressing failed.") + return false + } + var isOutageDetected = false + for case let address as NSData in addresses { + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + if getnameinfo(address.bytes.assumingMemoryBound(to: sockaddr.self), socklen_t(address.length), + &hostname, socklen_t(hostname.count), nil, 0, NI_NUMERICHOST) == 0 { + let addressString = String(cString: hostname) + let kHealthyAddress = "127.0.0.1" + let kOutageAddress = "127.0.0.2" + if addressString == kHealthyAddress { + // Do nothing. + } else if addressString == kOutageAddress { + isOutageDetected = true + } else { + owsFailDebug("unexpected address: \(addressString)") + } + } + } + return isOutageDetected + } + + private func checkForOutageAsync() { + Logger.info("") + + DispatchQueue.global().async { + let isOutageDetected = self.checkForOutageSync() + DispatchQueue.main.async { + self.hasOutage = isOutageDetected + } + } + } + + private var checkTimer: Timer? + private func ensureCheckTimer() { + // Only monitor for outages in the main app. + guard CurrentAppContext().isMainApp else { + return + } + + if shouldCheckForOutage { + if checkTimer != nil { + // Already has timer. + return + } + + // The TTL of the DNS record is 60 seconds. + checkTimer = WeakTimer.scheduledTimer(timeInterval: 60, target: self, userInfo: nil, repeats: true) { [weak self] _ in + AssertIsOnMainThread() + + guard CurrentAppContext().isMainAppAndActive else { + return + } + + guard let strongSelf = self else { + return + } + + strongSelf.checkForOutageAsync() + } + } else { + checkTimer?.invalidate() + checkTimer = nil + } + } + + @objc + public func reportConnectionSuccess() { + DispatchMainThreadSafe { + self.shouldCheckForOutage = false + self.hasOutage = false + } + } + + @objc + public func reportConnectionFailure() { + DispatchMainThreadSafe { + self.shouldCheckForOutage = true + } + } +} diff --git a/SignalUtilitiesKit/ParamParser.swift b/SignalUtilitiesKit/ParamParser.swift new file mode 100644 index 000000000..9a470ebb6 --- /dev/null +++ b/SignalUtilitiesKit/ParamParser.swift @@ -0,0 +1,142 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +// A DSL for parsing expected and optional values from a Dictionary, appropriate for +// validating a service response. +// +// Additionally it includes some helpers to DRY up common conversions. +// +// Rather than exhaustively enumerate accessors for types like `requireUInt32`, `requireInt64`, etc. +// We instead leverage generics at the call site. +// +// do { +// // Required +// let name: String = try paramParser.required(key: "name") +// let count: UInt32 = try paramParser.required(key: "count") +// +// // Optional +// let last_seen: Date? = try paramParser.optional(key: "last_seen") +// +// return Foo(name: name, count: count, isNew: lastSeen == nil) +// } catch { +// handleInvalidResponse(error: error) +// } +// +public class ParamParser { + public typealias Key = AnyHashable + + let dictionary: Dictionary + + public init(dictionary: Dictionary) { + self.dictionary = dictionary + } + + public convenience init?(responseObject: Any?) { + guard let responseDict = responseObject as? [String: AnyObject] else { + return nil + } + + self.init(dictionary: responseDict) + } + + // MARK: Errors + + public enum ParseError: Error { + case missingField(Key) + case invalidFormat(Key) + } + + private func invalid(key: Key) -> ParseError { + return ParseError.invalidFormat(key) + } + + private func missing(key: Key) -> ParseError { + return ParseError.missingField(key) + } + + // MARK: - Public API + + public func required(key: Key) throws -> T { + guard let value: T = try optional(key: key) else { + throw missing(key: key) + } + + return value + } + + public func optional(key: Key) throws -> T? { + guard let someValue = dictionary[key] else { + return nil + } + + guard !(someValue is NSNull) else { + return nil + } + + guard let typedValue = someValue as? T else { + throw invalid(key: key) + } + + return typedValue + } + + // MARK: FixedWidthIntegers (e.g. Int, Int32, UInt, UInt32, etc.) + + // You can't blindly cast accross Integer types, so we need to specify and validate which Int type we want. + // In general, you'll find numeric types parsed into a Dictionary as `Int`. + + public func required(key: Key) throws -> T where T: FixedWidthInteger { + guard let value: T = try optional(key: key) else { + throw missing(key: key) + } + + return value + } + + public func optional(key: Key) throws -> T? where T: FixedWidthInteger { + guard let someValue: Any = try optional(key: key) else { + return nil + } + + switch someValue { + case let typedValue as T: + return typedValue + case let int as Int: + guard int >= T.min, int <= T.max else { + throw invalid(key: key) + } + return T(int) + default: + throw invalid(key: key) + } + } + + // MARK: Base64 Data + + public func requiredBase64EncodedData(key: Key) throws -> Data { + guard let data: Data = try optionalBase64EncodedData(key: key) else { + throw ParseError.missingField(key) + } + + return data + } + + public func optionalBase64EncodedData(key: Key) throws -> Data? { + guard let encodedData: String = try self.optional(key: key) else { + return nil + } + + guard let data = Data(base64Encoded: encodedData) else { + throw ParseError.invalidFormat(key) + } + + guard data.count > 0 else { + return nil + } + + return data + } +} diff --git a/SignalUtilitiesKit/PhoneNumber.h b/SignalUtilitiesKit/PhoneNumber.h new file mode 100644 index 000000000..0e7cb9e30 --- /dev/null +++ b/SignalUtilitiesKit/PhoneNumber.h @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#define COUNTRY_CODE_PREFIX @"+" + +/** + * + * PhoneNumber is used to deal with the nitty details of parsing/canonicalizing phone numbers. + * Everything that expects a valid phone number should take a PhoneNumber, not a string, to avoid stringly typing. + * + */ +@interface PhoneNumber : NSObject + ++ (nullable PhoneNumber *)phoneNumberFromE164:(NSString *)text; + ++ (nullable PhoneNumber *)tryParsePhoneNumberFromUserSpecifiedText:(NSString *)text; ++ (nullable PhoneNumber *)tryParsePhoneNumberFromE164:(NSString *)text; + +// This will try to parse the input text as a phone number using +// the default region and the country code for this client's phone +// number. +// +// Order matters; better results will appear first. ++ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber; + ++ (NSString *)removeFormattingCharacters:(NSString *)inputString; ++ (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input; ++ (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input + withSpecifiedCountryCodeString:(NSString *)countryCodeString; ++ (NSString *)bestEffortLocalizedPhoneNumberWithE164:(NSString *)phoneNumber; + ++ (NSString *)regionCodeFromCountryCodeString:(NSString *)countryCodeString; + +- (NSURL *)toSystemDialerURL; +- (NSString *)toE164; +- (nullable NSNumber *)getCountryCode; +@property (nonatomic, readonly, nullable) NSString *nationalNumber; +- (BOOL)isValid; + +- (NSComparisonResult)compare:(PhoneNumber *)other; + ++ (NSString *)defaultCountryCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/PhoneNumber.m b/SignalUtilitiesKit/PhoneNumber.m new file mode 100644 index 000000000..e279f5053 --- /dev/null +++ b/SignalUtilitiesKit/PhoneNumber.m @@ -0,0 +1,584 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "PhoneNumber.h" +#import "PhoneNumberUtil.h" +#import "AppContext.h" +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const RPDefaultsKeyPhoneNumberString = @"RPDefaultsKeyPhoneNumberString"; +static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneNumberCanonical"; + +@interface PhoneNumber () + +@property (nonatomic, readonly) NBPhoneNumber *phoneNumber; +@property (nonatomic, readonly) NSString *e164; + +@end + +#pragma mark - + +@implementation PhoneNumber + +- (instancetype)initWithPhoneNumber:(NBPhoneNumber *)phoneNumber e164:(NSString *)e164 +{ + if (self = [self init]) { + OWSAssertDebug(phoneNumber); + OWSAssertDebug(e164.length > 0); + + _phoneNumber = phoneNumber; + _e164 = e164; + } + return self; +} + ++ (nullable PhoneNumber *)phoneNumberFromText:(NSString *)text andRegion:(NSString *)regionCode { + OWSAssertDebug(text != nil); + OWSAssertDebug(regionCode != nil); + + PhoneNumberUtil *phoneUtil = [PhoneNumberUtil sharedThreadLocal]; + + NSError *parseError = nil; + NBPhoneNumber *number = [phoneUtil parse:text defaultRegion:regionCode error:&parseError]; + + if (parseError) { + return nil; + } + + NSError *toE164Error; + NSString *e164 = [phoneUtil format:number numberFormat:NBEPhoneNumberFormatE164 error:&toE164Error]; + if (toE164Error) { + OWSLogDebug(@"Issue while formatting number: %@", [toE164Error description]); + return nil; + } + + return [[PhoneNumber alloc] initWithPhoneNumber:number e164:e164]; +} + ++ (nullable PhoneNumber *)phoneNumberFromUserSpecifiedText:(NSString *)text { + OWSAssertDebug(text != nil); + + return [PhoneNumber phoneNumberFromText:text andRegion:[self defaultCountryCode]]; +} + ++ (NSString *)defaultCountryCode +{ + NSLocale *locale = [NSLocale currentLocale]; + + NSString *_Nullable countryCode = nil; +#if TARGET_OS_IPHONE + countryCode = [[PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil countryCodeByCarrier]; + + if ([countryCode isEqualToString:@"ZZ"]) { + countryCode = [locale objectForKey:NSLocaleCountryCode]; + } +#else + countryCode = [locale objectForKey:NSLocaleCountryCode]; +#endif + if (!countryCode) { + OWSFailDebug(@"Could not identify country code for locale: %@", locale); + countryCode = @"US"; + } + return countryCode; +} + ++ (nullable PhoneNumber *)phoneNumberFromE164:(NSString *)text { + return [[PhoneNumber alloc] initWithPhoneNumber:[NBPhoneNumber new] e164:text]; + // Loki: Original code: + // ======== +// OWSAssertDebug(text != nil); +// OWSAssertDebug([text hasPrefix:COUNTRY_CODE_PREFIX]); +// PhoneNumber *number = [PhoneNumber phoneNumberFromText:text andRegion:@"ZZ"]; +// OWSAssertDebug(number != nil); +// return number; + // ======== +} + ++ (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input { + return [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:input + withSpecifiedRegionCode:[self defaultCountryCode]]; +} + ++ (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input + withSpecifiedCountryCodeString:(NSString *)countryCodeString { + return [PhoneNumber + bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:input + withSpecifiedRegionCode: + [PhoneNumber regionCodeFromCountryCodeString:countryCodeString]]; +} + ++ (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input + withSpecifiedRegionCode:(NSString *)regionCode { + NBAsYouTypeFormatter *formatter = [[NBAsYouTypeFormatter alloc] initWithRegionCode:regionCode]; + + NSString *result = input; + for (NSUInteger i = 0; i < input.length; i++) { + result = [formatter inputDigit:[input substringWithRange:NSMakeRange(i, 1)]]; + } + return result; +} + ++ (NSString *)formatIntAsEN:(int)value +{ + static NSNumberFormatter *formatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [NSNumberFormatter new]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US"]; + }); + return [formatter stringFromNumber:@(value)]; +} + ++ (NSString *)bestEffortLocalizedPhoneNumberWithE164:(NSString *)phoneNumber +{ + OWSAssertDebug(phoneNumber); + + if (![phoneNumber hasPrefix:COUNTRY_CODE_PREFIX]) { + return phoneNumber; + } + + PhoneNumber *_Nullable parsedPhoneNumber = [self tryParsePhoneNumberFromE164:phoneNumber]; + if (!parsedPhoneNumber) { + OWSLogWarn(@"could not parse phone number."); + return phoneNumber; + } + NSNumber *_Nullable countryCode = [parsedPhoneNumber getCountryCode]; + if (!countryCode) { + OWSLogWarn(@"parsed phone number has no country code."); + return phoneNumber; + } + NSString *countryCodeString = [self formatIntAsEN:countryCode.intValue]; + if (countryCodeString.length < 1) { + OWSLogWarn(@"invalid country code."); + return phoneNumber; + } + NSString *_Nullable formattedPhoneNumber = + [self bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:phoneNumber + withSpecifiedRegionCode:countryCodeString]; + if (!countryCode) { + OWSLogWarn(@"could not format phone number."); + return phoneNumber; + } + return formattedPhoneNumber; +} + ++ (NSString *)regionCodeFromCountryCodeString:(NSString *)countryCodeString { + NBPhoneNumberUtil *phoneUtil = [PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil; + NSString *regionCode = + [phoneUtil getRegionCodeForCountryCode:@([[countryCodeString substringFromIndex:1] integerValue])]; + return regionCode; +} + ++ (nullable PhoneNumber *)tryParsePhoneNumberFromUserSpecifiedText:(NSString *)text { + OWSAssertDebug(text != nil); + + if ([text isEqualToString:@""]) { + return nil; + } + NSString *sanitizedString = [self removeFormattingCharacters:text]; + + return [self phoneNumberFromUserSpecifiedText:sanitizedString]; +} + ++ (nullable NSString *)nationalPrefixTransformRuleForDefaultRegion +{ + static NSString *result = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *defaultCountryCode = [self defaultCountryCode]; + NBMetadataHelper *helper = [[NBMetadataHelper alloc] init]; + NBPhoneMetaData *defaultRegionMetadata = [helper getMetadataForRegion:defaultCountryCode]; + result = defaultRegionMetadata.nationalPrefixTransformRule; + }); + return result; +} + +// clientPhoneNumber is the local user's phone number and should never change. ++ (nullable NSString *)nationalPrefixTransformRuleForClientPhoneNumber:(NSString *)clientPhoneNumber +{ + if (clientPhoneNumber.length < 1) { + return nil; + } + static NSString *result = nil; + static NSString *cachedClientPhoneNumber = nil; + static dispatch_once_t onceToken; + + // clientPhoneNumber is the local user's phone number and should never change. + static void (^updateCachedClientPhoneNumber)(void); + updateCachedClientPhoneNumber = ^(void) { + NSNumber *localCallingCode = [[PhoneNumber phoneNumberFromE164:clientPhoneNumber] getCountryCode]; + if (localCallingCode != nil) { + NSString *localCallingCodePrefix = [NSString stringWithFormat:@"+%@", localCallingCode]; + NSString *localCountryCode = + [PhoneNumberUtil.sharedThreadLocal probableCountryCodeForCallingCode:localCallingCodePrefix]; + if (localCountryCode && ![localCountryCode isEqualToString:[self defaultCountryCode]]) { + NBMetadataHelper *helper = [[NBMetadataHelper alloc] init]; + NBPhoneMetaData *localNumberRegionMetadata = [helper getMetadataForRegion:localCountryCode]; + result = localNumberRegionMetadata.nationalPrefixTransformRule; + } else { + result = nil; + } + } + cachedClientPhoneNumber = [clientPhoneNumber copy]; + }; + +#ifdef DEBUG + // For performance, we want to cahce this result, but it breaks tests since local number + // can change. + if (CurrentAppContext().isRunningTests) { + updateCachedClientPhoneNumber(); + } else { + dispatch_once(&onceToken, ^{ + updateCachedClientPhoneNumber(); + }); + } +#else + dispatch_once(&onceToken, ^{ + updateCachedClientPhoneNumber(); + }); + OWSAssertDebug([cachedClientPhoneNumber isEqualToString:clientPhoneNumber]); +#endif + + return result; +} + ++ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber +{ + NSMutableArray *result = + [[self tryParsePhoneNumbersFromNormalizedText:text clientPhoneNumber:clientPhoneNumber] mutableCopy]; + + // A handful of countries (Mexico, Argentina, etc.) require a "national" prefix after + // their country calling code. + // + // It's a bit hacky, but we reconstruct these national prefixes from libPhoneNumber's + // parsing logic. It's okay if we botch this a little. The risk is that we end up with + // some misformatted numbers with extra non-numeric regex syntax. These erroneously + // parsed numbers will never be presented to the user, since they'll never survive the + // contacts intersection. + // + // 1. Try to apply a "national prefix" using the phone's region. + NSString *nationalPrefixTransformRuleForDefaultRegion = [self nationalPrefixTransformRuleForDefaultRegion]; + if ([nationalPrefixTransformRuleForDefaultRegion containsString:@"$1"]) { + NSString *normalizedText = + [nationalPrefixTransformRuleForDefaultRegion stringByReplacingOccurrencesOfString:@"$1" withString:text]; + if (![normalizedText containsString:@"$"]) { + [result addObjectsFromArray:[self tryParsePhoneNumbersFromNormalizedText:normalizedText + clientPhoneNumber:clientPhoneNumber]]; + } + } + + // 2. Try to apply a "national prefix" using the region that corresponds to the + // calling code for the local phone number. + NSString *nationalPrefixTransformRuleForClientPhoneNumber = + [self nationalPrefixTransformRuleForClientPhoneNumber:clientPhoneNumber]; + if ([nationalPrefixTransformRuleForClientPhoneNumber containsString:@"$1"]) { + NSString *normalizedText = + [nationalPrefixTransformRuleForClientPhoneNumber stringByReplacingOccurrencesOfString:@"$1" + withString:text]; + if (![normalizedText containsString:@"$"]) { + [result addObjectsFromArray:[self tryParsePhoneNumbersFromNormalizedText:normalizedText + clientPhoneNumber:clientPhoneNumber]]; + } + } + + return [result copy]; +} + ++ (NSArray *)tryParsePhoneNumbersFromNormalizedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber +{ + OWSAssertDebug(text != nil); + + text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if ([text isEqualToString:@""]) { + return nil; + } + + NSString *sanitizedString = [self removeFormattingCharacters:text]; + OWSAssertDebug(sanitizedString != nil); + + NSMutableArray *result = [NSMutableArray new]; + NSMutableSet *phoneNumberSet = [NSMutableSet new]; + void (^tryParsingWithCountryCode)(NSString *, NSString *) = ^(NSString *text, + NSString *countryCode) { + PhoneNumber *phoneNumber = [PhoneNumber phoneNumberFromText:text + andRegion:countryCode]; + if (phoneNumber && [phoneNumber toE164] && ![phoneNumberSet containsObject:[phoneNumber toE164]]) { + [result addObject:phoneNumber]; + [phoneNumberSet addObject:[phoneNumber toE164]]; + } + }; + + tryParsingWithCountryCode(sanitizedString, [self defaultCountryCode]); + + if ([sanitizedString hasPrefix:@"+"]) { + // If the text starts with "+", don't try prepending + // anything else. + return result; + } + + // Try just adding "+" and parsing it. + tryParsingWithCountryCode([NSString stringWithFormat:@"+%@", sanitizedString], [self defaultCountryCode]); + + // Order matters; better results should appear first so prefer + // matches with the same country code as this client's phone number. + if (clientPhoneNumber.length == 0) { + OWSFailDebug(@"clientPhoneNumber had unexpected length"); + return result; + } + + // Note that NBPhoneNumber uses "country code" to refer to what we call a + // "calling code" (i.e. 44 in +44123123). Within SSK we use "country code" + // (and sometimes "region code") to refer to a country's ISO 2-letter code + // (ISO 3166-1 alpha-2). + NSNumber *callingCodeForLocalNumber = [[PhoneNumber phoneNumberFromE164:clientPhoneNumber] getCountryCode]; + if (callingCodeForLocalNumber == nil) { + OWSFailDebug(@"callingCodeForLocalNumber was unexpectedly nil"); + return result; + } + + NSString *callingCodePrefix = [NSString stringWithFormat:@"+%@", callingCodeForLocalNumber]; + + tryParsingWithCountryCode([callingCodePrefix stringByAppendingString:sanitizedString], [self defaultCountryCode]); + + // Try to determine what the country code is for the local phone number + // and also try parsing the phone number using that country code if it + // differs from the device's region code. + // + // For example, a French person living in Italy might have an + // Italian phone number but use French region/language for their + // phone. They're likely to have both Italian and French contacts. + NSString *localCountryCode = + [PhoneNumberUtil.sharedThreadLocal probableCountryCodeForCallingCode:callingCodePrefix]; + if (localCountryCode && ![localCountryCode isEqualToString:[self defaultCountryCode]]) { + tryParsingWithCountryCode([callingCodePrefix stringByAppendingString:sanitizedString], localCountryCode); + } + + NSString *_Nullable phoneNumberByApplyingMissingAreaCode = + [self applyMissingAreaCodeWithCallingCodeForReferenceNumber:callingCodeForLocalNumber + referenceNumber:clientPhoneNumber + sanitizedInputText:sanitizedString]; + if (phoneNumberByApplyingMissingAreaCode) { + tryParsingWithCountryCode(phoneNumberByApplyingMissingAreaCode, localCountryCode); + } + + return result; +} + +#pragma mark - missing area code + ++ (nullable NSString *)applyMissingAreaCodeWithCallingCodeForReferenceNumber:(NSNumber *)callingCodeForReferenceNumber + referenceNumber:(NSString *)referenceNumber + sanitizedInputText:(NSString *)sanitizedInputText +{ + if ([callingCodeForReferenceNumber isEqual:@(55)]) { + return + [self applyMissingBrazilAreaCodeWithReferenceNumber:referenceNumber sanitizedInputText:sanitizedInputText]; + } else if ([callingCodeForReferenceNumber isEqual:@(1)]) { + return [self applyMissingUnitedStatesAreaCodeWithReferenceNumber:referenceNumber + sanitizedInputText:sanitizedInputText]; + } else { + return nil; + } +} + +#pragma mark - missing brazil area code + ++ (nullable NSString *)applyMissingBrazilAreaCodeWithReferenceNumber:(NSString *)referenceNumber + sanitizedInputText:(NSString *)sanitizedInputText +{ + NSError *error; + NSRegularExpression *missingAreaCodeRegex = + [[NSRegularExpression alloc] initWithPattern:@"^(9?\\d{8})$" options:0 error:&error]; + if (error) { + OWSFailDebug(@"failure: %@", error); + return nil; + } + + if ([missingAreaCodeRegex firstMatchInString:sanitizedInputText + options:0 + range:NSMakeRange(0, sanitizedInputText.length)] + == nil) { + } + + NSString *_Nullable referenceAreaCode = [self brazilAreaCodeFromReferenceNumberE164:referenceNumber]; + if (!referenceAreaCode) { + return nil; + } + return [NSString stringWithFormat:@"+55%@%@", referenceAreaCode, sanitizedInputText]; +} + ++ (nullable NSString *)brazilAreaCodeFromReferenceNumberE164:(NSString *)referenceNumberE164 +{ + NSError *error; + NSRegularExpression *areaCodeRegex = + [[NSRegularExpression alloc] initWithPattern:@"^\\+55(\\d{2})9?\\d{8}" options:0 error:&error]; + if (error) { + OWSFailDebug(@"failure: %@", error); + return nil; + } + + NSArray *matches = + [areaCodeRegex matchesInString:referenceNumberE164 options:0 range:NSMakeRange(0, referenceNumberE164.length)]; + if (matches.count == 0) { + OWSFailDebug(@"failure: unexpectedly unable to extract area code from US number"); + return nil; + } + NSTextCheckingResult *match = matches[0]; + + NSRange firstCaptureRange = [match rangeAtIndex:1]; + return [referenceNumberE164 substringWithRange:firstCaptureRange]; +} + +#pragma mark - missing US area code + ++ (nullable NSString *)applyMissingUnitedStatesAreaCodeWithReferenceNumber:(NSString *)referenceNumber + sanitizedInputText:(NSString *)sanitizedInputText +{ + NSError *error; + NSRegularExpression *missingAreaCodeRegex = + [[NSRegularExpression alloc] initWithPattern:@"^(\\d{7})$" options:0 error:&error]; + if (error) { + OWSFailDebug(@"failure: %@", error); + return nil; + } + + if ([missingAreaCodeRegex firstMatchInString:sanitizedInputText + options:0 + range:NSMakeRange(0, sanitizedInputText.length)] + == nil) { + // area code isn't missing + return nil; + } + + NSString *_Nullable referenceAreaCode = [self unitedStateAreaCodeFromReferenceNumberE164:referenceNumber]; + if (!referenceAreaCode) { + return nil; + } + return [NSString stringWithFormat:@"+1%@%@", referenceAreaCode, sanitizedInputText]; +} + ++ (nullable NSString *)unitedStateAreaCodeFromReferenceNumberE164:(NSString *)referenceNumberE164 +{ + NSError *error; + NSRegularExpression *areaCodeRegex = + [[NSRegularExpression alloc] initWithPattern:@"^\\+1(\\d{3})" options:0 error:&error]; + if (error) { + OWSFailDebug(@"failure: %@", error); + return nil; + } + + NSArray *matches = + [areaCodeRegex matchesInString:referenceNumberE164 options:0 range:NSMakeRange(0, referenceNumberE164.length)]; + if (matches.count == 0) { + OWSFailDebug(@"failure: unexpectedly unable to extract area code from US number"); + return nil; + } + NSTextCheckingResult *match = matches[0]; + + NSRange firstCaptureRange = [match rangeAtIndex:1]; + return [referenceNumberE164 substringWithRange:firstCaptureRange]; +} + +#pragma mark - + ++ (NSString *)removeFormattingCharacters:(NSString *)inputString { + char outputString[inputString.length + 1]; + + int outputLength = 0; + for (NSUInteger i = 0; i < inputString.length; i++) { + unichar c = [inputString characterAtIndex:i]; + if (c == '+' || (c >= '0' && c <= '9')) { + outputString[outputLength++] = (char)c; + } + } + + outputString[outputLength] = 0; + return [NSString stringWithUTF8String:(void *)outputString]; +} + ++ (nullable PhoneNumber *)tryParsePhoneNumberFromE164:(NSString *)text { + OWSAssertDebug(text != nil); + if (![text hasPrefix:COUNTRY_CODE_PREFIX]) { + return nil; + } + + return [self phoneNumberFromE164:text]; +} + +- (NSURL *)toSystemDialerURL { + NSString *link = [NSString stringWithFormat:@"telprompt://%@", self.e164]; + return [NSURL URLWithString:link]; +} + +- (NSString *)toE164 { + return self.e164; +} + +- (nullable NSNumber *)getCountryCode { + return self.phoneNumber.countryCode; +} + +- (nullable NSString *)nationalNumber +{ + NSError *error; + NSString *nationalNumber = [[PhoneNumberUtil sharedThreadLocal] format:self.phoneNumber + numberFormat:NBEPhoneNumberFormatNATIONAL + error:&error]; + if (error) { + OWSLogVerbose(@"error parsing number into national format: %@", error); + return nil; + } + + return nationalNumber; +} + +- (BOOL)isValid +{ + return [[PhoneNumberUtil sharedThreadLocal].nbPhoneNumberUtil isValidNumber:self.phoneNumber]; +} + +- (NSString *)description { + return self.e164; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeObject:self.phoneNumber forKey:RPDefaultsKeyPhoneNumberString]; + [encoder encodeObject:self.e164 forKey:RPDefaultsKeyPhoneNumberCanonical]; +} + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + _phoneNumber = [decoder decodeObjectForKey:RPDefaultsKeyPhoneNumberString]; + _e164 = [decoder decodeObjectForKey:RPDefaultsKeyPhoneNumberCanonical]; + } + return self; +} + +- (NSComparisonResult)compare:(PhoneNumber *)other +{ + return [self.toE164 compare:other.toE164]; +} + +- (BOOL)isEqual:(id)other +{ + if (![other isMemberOfClass:[self class]]) { + return NO; + } + PhoneNumber *otherPhoneNumber = (PhoneNumber *)other; + + return [self.phoneNumber isEqual:otherPhoneNumber.phoneNumber]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/PhoneNumberUtil.h b/SignalUtilitiesKit/PhoneNumberUtil.h new file mode 100644 index 000000000..377fa7498 --- /dev/null +++ b/SignalUtilitiesKit/PhoneNumberUtil.h @@ -0,0 +1,44 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "PhoneNumber.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PhoneNumberUtil : NSObject + +@property (nonatomic, retain) NBPhoneNumberUtil *nbPhoneNumberUtil; + +- (instancetype)init NS_UNAVAILABLE; + ++ (instancetype)sharedThreadLocal; + ++ (BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString; + ++ (NSString *)callingCodeFromCountryCode:(NSString *)countryCode; ++ (nullable NSString *)countryNameFromCountryCode:(NSString *)countryCode; ++ (NSArray *)countryCodesForSearchTerm:(nullable NSString *)searchTerm; + +// Returns a list of country codes for a calling code in descending +// order of population. +- (NSArray *)countryCodesFromCallingCode:(NSString *)callingCode; +// Returns the most likely country code for a calling code based on population. +- (NSString *)probableCountryCodeForCallingCode:(NSString *)callingCode; + ++ (NSUInteger)translateCursorPosition:(NSUInteger)offset + from:(NSString *)source + to:(NSString *)target + stickingRightward:(bool)preferHigh; + ++ (NSString *)examplePhoneNumberForCountryCode:(NSString *)countryCode; + +- (nullable NBPhoneNumber *)parse:(NSString *)numberToParse defaultRegion:(NSString *)defaultRegion error:(NSError **)error; +- (NSString *)format:(NBPhoneNumber *)phoneNumber + numberFormat:(NBEPhoneNumberFormat)numberFormat + error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/PhoneNumberUtil.m b/SignalUtilitiesKit/PhoneNumberUtil.m new file mode 100644 index 000000000..16bb85545 --- /dev/null +++ b/SignalUtilitiesKit/PhoneNumberUtil.m @@ -0,0 +1,610 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "PhoneNumberUtil.h" +#import "ContactsManagerProtocol.h" +#import "FunctionalUtil.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PhoneNumberUtil () + +@property (nonatomic, readonly) NSMutableDictionary *countryCodesFromCallingCodeCache; +@property (nonatomic, readonly) NSCache *parsedPhoneNumberCache; + +@end + +#pragma mark - + +@implementation PhoneNumberUtil + ++ (PhoneNumberUtil *)sharedThreadLocal +{ + NSString *key = PhoneNumberUtil.logTag; + PhoneNumberUtil *_Nullable threadLocal = NSThread.currentThread.threadDictionary[key]; + if (!threadLocal) { + threadLocal = [PhoneNumberUtil new]; + NSThread.currentThread.threadDictionary[key] = threadLocal; + } + return threadLocal; +} + +- (instancetype)init { + self = [super init]; + + if (self) { + _nbPhoneNumberUtil = [[NBPhoneNumberUtil alloc] init]; + _countryCodesFromCallingCodeCache = [NSMutableDictionary new]; + _parsedPhoneNumberCache = [NSCache new]; + } + + return self; +} + +- (nullable NBPhoneNumber *)parse:(NSString *)numberToParse + defaultRegion:(NSString *)defaultRegion + error:(NSError **)error +{ + NSString *hashKey = [NSString stringWithFormat:@"numberToParse:%@defaultRegion:%@", numberToParse, defaultRegion]; + + NBPhoneNumber *result = [self.parsedPhoneNumberCache objectForKey:hashKey]; + + if (!result) { + result = [self.nbPhoneNumberUtil parse:numberToParse defaultRegion:defaultRegion error:error]; + if (error && *error) { + OWSAssertDebug(!result); + return nil; + } + + OWSAssertDebug(result); + + if (result) { + [self.parsedPhoneNumberCache setObject:result forKey:hashKey]; + } else { + [self.parsedPhoneNumberCache setObject:[NSNull null] forKey:hashKey]; + } + } + + if ([result class] == [NSNull class]) { + return nil; + } else { + return result; + } +} + +- (NSString *)format:(NBPhoneNumber *)phoneNumber + numberFormat:(NBEPhoneNumberFormat)numberFormat + error:(NSError **)error +{ + return [self.nbPhoneNumberUtil format:phoneNumber numberFormat:numberFormat error:error]; +} + +// country code -> country name ++ (nullable NSString *)countryNameFromCountryCode:(NSString *)countryCode +{ + OWSAssertDebug(countryCode); + + NSDictionary *countryCodeComponent = @{NSLocaleCountryCode : countryCode}; + NSString *identifier = [NSLocale localeIdentifierFromComponents:countryCodeComponent]; + NSString *countryName = [NSLocale.currentLocale displayNameForKey:NSLocaleIdentifier value:identifier]; + if (countryName.length < 1) { + countryName = [NSLocale.systemLocale displayNameForKey:NSLocaleIdentifier value:identifier]; + } + if (countryName.length < 1) { + countryName = NSLocalizedString(@"UNKNOWN_VALUE", "Indicates an unknown or unrecognizable value."); + } + return countryName; +} + +// country code -> calling code ++ (NSString *)callingCodeFromCountryCode:(NSString *)countryCode +{ + if ([countryCode isEqualToString:@"AQ"]) { + // Antarctica + return @"+672"; + } else if ([countryCode isEqualToString:@"BV"]) { + // Bouvet Island + return @"+55"; + } else if ([countryCode isEqualToString:@"IC"]) { + // Canary Islands + return @"+34"; + } else if ([countryCode isEqualToString:@"EA"]) { + // Ceuta & Melilla + return @"+34"; + } else if ([countryCode isEqualToString:@"CP"]) { + // Clipperton Island + // + // This country code should be filtered - it does not appear to have a calling code. + return nil; + } else if ([countryCode isEqualToString:@"DG"]) { + // Diego Garcia + return @"+246"; + } else if ([countryCode isEqualToString:@"TF"]) { + // French Southern Territories + return @"+262"; + } else if ([countryCode isEqualToString:@"HM"]) { + // Heard & McDonald Islands + return @"+672"; + } else if ([countryCode isEqualToString:@"XK"]) { + // Kosovo + return @"+383"; + } else if ([countryCode isEqualToString:@"PN"]) { + // Pitcairn Islands + return @"+64"; + } else if ([countryCode isEqualToString:@"GS"]) { + // So. Georgia & So. Sandwich Isl. + return @"+500"; + } else if ([countryCode isEqualToString:@"UM"]) { + // U.S. Outlying Islands + return @"+1"; + } + + NSString *callingCode = + [NSString stringWithFormat:@"%@%@", + COUNTRY_CODE_PREFIX, + [[[self sharedThreadLocal] nbPhoneNumberUtil] getCountryCodeForRegion:countryCode]]; + return callingCode; +} + +- (NSDictionary *)countryCodeToPopulationMap +{ + static dispatch_once_t onceToken; + static NSDictionary *instance = nil; + dispatch_once(&onceToken, ^{ + instance = @{ + @"AD" : @(84000), + @"AE" : @(4975593), + @"AF" : @(29121286), + @"AG" : @(86754), + @"AI" : @(13254), + @"AL" : @(2986952), + @"AM" : @(2968000), + @"AN" : @(300000), + @"AO" : @(13068161), + @"AQ" : @(0), + @"AR" : @(41343201), + @"AS" : @(57881), + @"AT" : @(8205000), + @"AU" : @(21515754), + @"AW" : @(71566), + @"AX" : @(26711), + @"AZ" : @(8303512), + @"BA" : @(4590000), + @"BB" : @(285653), + @"BD" : @(156118464), + @"BE" : @(10403000), + @"BF" : @(16241811), + @"BG" : @(7148785), + @"BH" : @(738004), + @"BI" : @(9863117), + @"BJ" : @(9056010), + @"BL" : @(8450), + @"BM" : @(65365), + @"BN" : @(395027), + @"BO" : @(9947418), + @"BQ" : @(18012), + @"BR" : @(201103330), + @"BS" : @(301790), + @"BT" : @(699847), + @"BV" : @(0), + @"BW" : @(2029307), + @"BY" : @(9685000), + @"BZ" : @(314522), + @"CA" : @(33679000), + @"CC" : @(628), + @"CD" : @(70916439), + @"CF" : @(4844927), + @"CG" : @(3039126), + @"CH" : @(7581000), + @"CI" : @(21058798), + @"CK" : @(21388), + @"CL" : @(16746491), + @"CM" : @(19294149), + @"CN" : @(1330044000), + @"CO" : @(47790000), + @"CR" : @(4516220), + @"CS" : @(10829175), + @"CU" : @(11423000), + @"CV" : @(508659), + @"CW" : @(141766), + @"CX" : @(1500), + @"CY" : @(1102677), + @"CZ" : @(10476000), + @"DE" : @(81802257), + @"DJ" : @(740528), + @"DK" : @(5484000), + @"DM" : @(72813), + @"DO" : @(9823821), + @"DZ" : @(34586184), + @"EC" : @(14790608), + @"EE" : @(1291170), + @"EG" : @(80471869), + @"EH" : @(273008), + @"ER" : @(5792984), + @"ES" : @(46505963), + @"ET" : @(88013491), + @"FI" : @(5244000), + @"FJ" : @(875983), + @"FK" : @(2638), + @"FM" : @(107708), + @"FO" : @(48228), + @"FR" : @(64768389), + @"GA" : @(1545255), + @"GB" : @(62348447), + @"GD" : @(107818), + @"GE" : @(4630000), + @"GF" : @(195506), + @"GG" : @(65228), + @"GH" : @(24339838), + @"GI" : @(27884), + @"GL" : @(56375), + @"GM" : @(1593256), + @"GN" : @(10324025), + @"GP" : @(443000), + @"GQ" : @(1014999), + @"GR" : @(11000000), + @"GS" : @(30), + @"GT" : @(13550440), + @"GU" : @(159358), + @"GW" : @(1565126), + @"GY" : @(748486), + @"HK" : @(6898686), + @"HM" : @(0), + @"HN" : @(7989415), + @"HR" : @(4284889), + @"HT" : @(9648924), + @"HU" : @(9982000), + @"ID" : @(242968342), + @"IE" : @(4622917), + @"IL" : @(7353985), + @"IM" : @(75049), + @"IN" : @(1173108018), + @"IO" : @(4000), + @"IQ" : @(29671605), + @"IR" : @(76923300), + @"IS" : @(308910), + @"IT" : @(60340328), + @"JE" : @(90812), + @"JM" : @(2847232), + @"JO" : @(6407085), + @"JP" : @(127288000), + @"KE" : @(40046566), + @"KG" : @(5776500), + @"KH" : @(14453680), + @"KI" : @(92533), + @"KM" : @(773407), + @"KN" : @(51134), + @"KP" : @(22912177), + @"KR" : @(48422644), + @"KW" : @(2789132), + @"KY" : @(44270), + @"KZ" : @(15340000), + @"LA" : @(6368162), + @"LB" : @(4125247), + @"LC" : @(160922), + @"LI" : @(35000), + @"LK" : @(21513990), + @"LR" : @(3685076), + @"LS" : @(1919552), + @"LT" : @(2944459), + @"LU" : @(497538), + @"LV" : @(2217969), + @"LY" : @(6461454), + @"MA" : @(33848242), + @"MC" : @(32965), + @"MD" : @(4324000), + @"ME" : @(666730), + @"MF" : @(35925), + @"MG" : @(21281844), + @"MH" : @(65859), + @"MK" : @(2062294), + @"ML" : @(13796354), + @"MM" : @(53414374), + @"MN" : @(3086918), + @"MO" : @(449198), + @"MP" : @(53883), + @"MQ" : @(432900), + @"MR" : @(3205060), + @"MS" : @(9341), + @"MT" : @(403000), + @"MU" : @(1294104), + @"MV" : @(395650), + @"MW" : @(15447500), + @"MX" : @(112468855), + @"MY" : @(28274729), + @"MZ" : @(22061451), + @"NA" : @(2128471), + @"NC" : @(216494), + @"NE" : @(15878271), + @"NF" : @(1828), + @"NG" : @(154000000), + @"NI" : @(5995928), + @"NL" : @(16645000), + @"NO" : @(5009150), + @"NP" : @(28951852), + @"NR" : @(10065), + @"NU" : @(2166), + @"NZ" : @(4252277), + @"OM" : @(2967717), + @"PA" : @(3410676), + @"PE" : @(29907003), + @"PF" : @(270485), + @"PG" : @(6064515), + @"PH" : @(99900177), + @"PK" : @(184404791), + @"PL" : @(38500000), + @"PM" : @(7012), + @"PN" : @(46), + @"PR" : @(3916632), + @"PS" : @(3800000), + @"PT" : @(10676000), + @"PW" : @(19907), + @"PY" : @(6375830), + @"QA" : @(840926), + @"RE" : @(776948), + @"RO" : @(21959278), + @"RS" : @(7344847), + @"RU" : @(140702000), + @"RW" : @(11055976), + @"SA" : @(25731776), + @"SB" : @(559198), + @"SC" : @(88340), + @"SD" : @(35000000), + @"SE" : @(9828655), + @"SG" : @(4701069), + @"SH" : @(7460), + @"SI" : @(2007000), + @"SJ" : @(2550), + @"SK" : @(5455000), + @"SL" : @(5245695), + @"SM" : @(31477), + @"SN" : @(12323252), + @"SO" : @(10112453), + @"SR" : @(492829), + @"SS" : @(8260490), + @"ST" : @(175808), + @"SV" : @(6052064), + @"SX" : @(37429), + @"SY" : @(22198110), + @"SZ" : @(1354051), + @"TC" : @(20556), + @"TD" : @(10543464), + @"TF" : @(140), + @"TG" : @(6587239), + @"TH" : @(67089500), + @"TJ" : @(7487489), + @"TK" : @(1466), + @"TL" : @(1154625), + @"TM" : @(4940916), + @"TN" : @(10589025), + @"TO" : @(122580), + @"TR" : @(77804122), + @"TT" : @(1328019), + @"TV" : @(10472), + @"TW" : @(22894384), + @"TZ" : @(41892895), + @"UA" : @(45415596), + @"UG" : @(33398682), + @"UM" : @(0), + @"US" : @(310232863), + @"UY" : @(3477000), + @"UZ" : @(27865738), + @"VA" : @(921), + @"VC" : @(104217), + @"VE" : @(27223228), + @"VG" : @(21730), + @"VI" : @(108708), + @"VN" : @(89571130), + @"VU" : @(221552), + @"WF" : @(16025), + @"WS" : @(192001), + @"XK" : @(1800000), + @"YE" : @(23495361), + @"YT" : @(159042), + @"ZA" : @(49000000), + @"ZM" : @(13460305), + @"ZW" : @(13061000), + }; + }); + return instance; +} + +- (NSArray *)countryCodesSortedByPopulationDescending +{ + NSDictionary *countryCodeToPopulationMap = [self countryCodeToPopulationMap]; + NSArray *result = [NSLocale.ISOCountryCodes + sortedArrayUsingComparator:^NSComparisonResult(NSString *_Nonnull left, NSString *_Nonnull right) { + int leftPopulation = [countryCodeToPopulationMap[left] intValue]; + int rightPopulation = [countryCodeToPopulationMap[right] intValue]; + // Invert the values for a descending sort. + return [@(-leftPopulation) compare:@(-rightPopulation)]; + }]; + return result; +} + +- (NSArray *)countryCodesFromCallingCode:(NSString *)callingCode +{ + @synchronized(self) + { + OWSAssertDebug(callingCode.length > 0); + + NSArray *result = self.countryCodesFromCallingCodeCache[callingCode]; + if (!result) { + NSMutableArray *countryCodes = [NSMutableArray new]; + for (NSString *countryCode in [self countryCodesSortedByPopulationDescending]) { + NSString *callingCodeForCountryCode = [PhoneNumberUtil callingCodeFromCountryCode:countryCode]; + if ([callingCode isEqualToString:callingCodeForCountryCode]) { + [countryCodes addObject:countryCode]; + } + } + result = [countryCodes copy]; + self.countryCodesFromCallingCodeCache[callingCode] = result; + } + return result; + } +} + +- (NSString *)probableCountryCodeForCallingCode:(NSString *)callingCode +{ + OWSAssertDebug(callingCode.length > 0); + + NSArray *countryCodes = [self countryCodesFromCallingCode:callingCode]; + return (countryCodes.count > 0 ? countryCodes[0] : nil); +} + ++ (BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString { + NSCharacterSet *whitespaceSet = NSCharacterSet.whitespaceCharacterSet; + NSArray *queryStrings = [queryString componentsSeparatedByCharactersInSet:whitespaceSet]; + NSArray *nameStrings = [nameString componentsSeparatedByCharactersInSet:whitespaceSet]; + + return [queryStrings all:^int(NSString *query) { + if (query.length == 0) + return YES; + return [nameStrings any:^int(NSString *nameWord) { + NSStringCompareOptions searchOpts = NSCaseInsensitiveSearch | NSAnchoredSearch; + return [nameWord rangeOfString:query options:searchOpts].location != NSNotFound; + }]; + }]; +} + +// search term -> country codes ++ (NSArray *)countryCodesForSearchTerm:(nullable NSString *)searchTerm { + searchTerm = [searchTerm stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + NSArray *countryCodes = NSLocale.ISOCountryCodes; + + countryCodes = [countryCodes filter:^int(NSString *countryCode) { + NSString *countryName = [self countryNameFromCountryCode:countryCode]; + NSString *callingCode = [self callingCodeFromCountryCode:countryCode]; + + if (countryName.length < 1 || callingCode.length < 1 || [callingCode isEqualToString:@"+0"]) { + // Filter out countries without a valid calling code. + return NO; + } + + if (searchTerm.length < 1) { + return YES; + } + + if ([self name:countryName matchesQuery:searchTerm]) { + return YES; + } + + if ([self name:countryCode matchesQuery:searchTerm]) { + return YES; + } + + // We rely on the already internationalized string; as that is what + // the user would see entered (i.e. with COUNTRY_CODE_PREFIX). + + if ([callingCode containsString:searchTerm]) { + return YES; + } + + return NO; + }]; + + return [self sortedCountryCodesByName:countryCodes]; +} + ++ (NSArray *)sortedCountryCodesByName:(NSArray *)countryCodesByISOCode { + return [countryCodesByISOCode sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { + return [[self countryNameFromCountryCode:obj1] caseInsensitiveCompare:[self countryNameFromCountryCode:obj2]]; + }]; +} + +// black magic ++ (NSUInteger)translateCursorPosition:(NSUInteger)offset + from:(NSString *)source + to:(NSString *)target + stickingRightward:(bool)preferHigh { + OWSAssertDebug(source != nil); + OWSAssertDebug(target != nil); + OWSAssertDebug(offset <= source.length); + + NSUInteger n = source.length; + NSUInteger m = target.length; + + int moves[n + 1][m + 1]; + { + // Wagner-Fischer algorithm for computing edit distance, with a tweaks: + // - Tracks best moves at each location, to allow reconstruction of edit path + // - Does not allow substitutions + // - Over-values digits relative to other characters, so they're "harder" to delete or insert + const int DIGIT_VALUE = 10; + NSUInteger scores[n + 1][m + 1]; + moves[0][0] = 0; // (match) move up and left + scores[0][0] = 0; + for (NSUInteger i = 1; i <= n; i++) { + scores[i][0] = i; + moves[i][0] = -1; // (deletion) move left + } + for (NSUInteger j = 1; j <= m; j++) { + scores[0][j] = j; + moves[0][j] = +1; // (insertion) move up + } + + NSCharacterSet *digits = NSCharacterSet.decimalDigitCharacterSet; + for (NSUInteger i = 1; i <= n; i++) { + unichar c1 = [source characterAtIndex:i - 1]; + bool isDigit1 = [digits characterIsMember:c1]; + for (NSUInteger j = 1; j <= m; j++) { + unichar c2 = [target characterAtIndex:j - 1]; + bool isDigit2 = [digits characterIsMember:c2]; + if (c1 == c2) { + scores[i][j] = scores[i - 1][j - 1]; + moves[i][j] = 0; // move up-and-left + } else { + NSUInteger del = scores[i - 1][j] + (isDigit1 ? DIGIT_VALUE : 1); + NSUInteger ins = scores[i][j - 1] + (isDigit2 ? DIGIT_VALUE : 1); + bool isDel = del < ins; + scores[i][j] = isDel ? del : ins; + moves[i][j] = isDel ? -1 : +1; + } + } + } + } + + // Backtrack to find desired corresponding offset + for (NSUInteger i = n, j = m;; i -= 1) { + if (i == offset && preferHigh) + return j; // early exit + while (moves[i][j] == +1) + j -= 1; // zip upward + if (i == offset) + return j; // late exit + if (moves[i][j] == 0) + j -= 1; + } +} + ++ (NSString *)examplePhoneNumberForCountryCode:(NSString *)countryCode +{ + PhoneNumberUtil *sharedUtil = [self sharedThreadLocal]; + + // Signal users are very likely using mobile devices, so prefer that kind of example. + NSError *error; + NBPhoneNumber *nbPhoneNumber = + [sharedUtil.nbPhoneNumberUtil getExampleNumberForType:countryCode type:NBEPhoneNumberTypeMOBILE error:&error]; + OWSAssertDebug(!error); + if (!nbPhoneNumber) { + // For countries that with similar mobile and land lines, use "line or mobile" + // examples. + nbPhoneNumber = [sharedUtil.nbPhoneNumberUtil getExampleNumberForType:countryCode + type:NBEPhoneNumberTypeFIXED_LINE_OR_MOBILE + error:&error]; + OWSAssertDebug(!error); + } + NSString *result = (nbPhoneNumber + ? [sharedUtil.nbPhoneNumberUtil format:nbPhoneNumber numberFormat:NBEPhoneNumberFormatE164 error:&error] + : nil); + OWSAssertDebug(!error); + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Poller.swift b/SignalUtilitiesKit/Poller.swift new file mode 100644 index 000000000..1f5ef36c9 --- /dev/null +++ b/SignalUtilitiesKit/Poller.swift @@ -0,0 +1,115 @@ +import PromiseKit + +@objc(LKPoller) +public final class Poller : NSObject { + private let storage = OWSPrimaryStorage.shared() + private var isPolling = false + private var usedSnodes = Set() + private var pollCount = 0 + + // MARK: Settings + private static let pollInterval: TimeInterval = 1 + private static let retryInterval: TimeInterval = 0.25 + /// After polling a given snode this many times we always switch to a new one. + /// + /// The reason for doing this is that sometimes a snode will be giving us successful responses while + /// it isn't actually getting messages from other snodes. + private static let maxPollCount: UInt = 6 + + // MARK: Error + private enum Error : LocalizedError { + case pollLimitReached + + var localizedDescription: String { + switch self { + case .pollLimitReached: return "Poll limit reached for current snode." + } + } + } + + // MARK: Public API + @objc public func startIfNeeded() { + guard !isPolling else { return } + print("[Loki] Started polling.") + isPolling = true + setUpPolling() + } + + @objc public func stop() { + print("[Loki] Stopped polling.") + isPolling = false + usedSnodes.removeAll() + } + + // MARK: Private API + private func setUpPolling() { + guard isPolling else { return } + SnodeAPI.getSwarm(for: getUserHexEncodedPublicKey(), isForcedReload: true).then2 { [weak self] _ -> Promise in + guard let strongSelf = self else { return Promise { $0.fulfill(()) } } + strongSelf.usedSnodes.removeAll() + let (promise, seal) = Promise.pending() + strongSelf.pollNextSnode(seal: seal) + return promise + }.ensure(on: DispatchQueue.main) { [weak self] in // Timers don't do well on background queues + guard let strongSelf = self, strongSelf.isPolling else { return } + Timer.scheduledTimer(withTimeInterval: Poller.retryInterval, repeats: false) { _ in + guard let strongSelf = self else { return } + strongSelf.setUpPolling() + } + } + } + + private func pollNextSnode(seal: Resolver) { + let userPublicKey = getUserHexEncodedPublicKey() + let swarm = SnodeAPI.swarmCache[userPublicKey] ?? [] + let unusedSnodes = Set(swarm).subtracting(usedSnodes) + if !unusedSnodes.isEmpty { + // randomElement() uses the system's default random generator, which is cryptographically secure + let nextSnode = unusedSnodes.randomElement()! + usedSnodes.insert(nextSnode) + poll(nextSnode, seal: seal).done2 { + seal.fulfill(()) + }.catch2 { [weak self] error in + if let error = error as? Error, error == .pollLimitReached { + self?.pollCount = 0 + } else { + print("[Loki] Polling \(nextSnode) failed; dropping it and switching to next snode.") + SnodeAPI.dropSnodeFromSwarmIfNeeded(nextSnode, publicKey: userPublicKey) + } + self?.pollNextSnode(seal: seal) + } + } else { + seal.fulfill(()) + } + } + + private func poll(_ snode: Snode, seal longTermSeal: Resolver) -> Promise { + guard isPolling else { return Promise { $0.fulfill(()) } } + let userPublicKey = getUserHexEncodedPublicKey() + return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: DispatchQueue.main) { [weak self] rawResponse -> Promise in + guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } } + let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey) + if !messages.isEmpty { + print("[Loki] Received \(messages.count) new message(s).") + } + messages.forEach { json in + guard let envelope = SSKProtoEnvelope.from(json) else { return } + do { + let data = try envelope.serializedData() + SSKEnvironment.shared.messageReceiver.handleReceivedEnvelopeData(data) + } catch { + print("[Loki] Failed to deserialize envelope due to error: \(error).") + } + } + strongSelf.pollCount += 1 + if strongSelf.pollCount == Poller.maxPollCount { + throw Error.pollLimitReached + } else { + return withDelay(Poller.pollInterval, completionQueue: SnodeAPI.workQueue) { + guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } } + return strongSelf.poll(snode, seal: longTermSeal) + } + } + } + } +} diff --git a/SignalUtilitiesKit/PreKeyBundle+jsonDict.h b/SignalUtilitiesKit/PreKeyBundle+jsonDict.h new file mode 100644 index 000000000..c0ca91121 --- /dev/null +++ b/SignalUtilitiesKit/PreKeyBundle+jsonDict.h @@ -0,0 +1,15 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PreKeyBundle (jsonDict) + ++ (nullable PreKeyBundle *)preKeyBundleFromDictionary:(NSDictionary *)dictionary forDeviceNumber:(NSNumber *)number; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/PreKeyBundle+jsonDict.m b/SignalUtilitiesKit/PreKeyBundle+jsonDict.m new file mode 100644 index 000000000..306f477c5 --- /dev/null +++ b/SignalUtilitiesKit/PreKeyBundle+jsonDict.m @@ -0,0 +1,110 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "PreKeyBundle+jsonDict.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation PreKeyBundle (jsonDict) + ++ (nullable PreKeyBundle *)preKeyBundleFromDictionary:(NSDictionary *)dictionary forDeviceNumber:(NSNumber *)number +{ + PreKeyBundle *bundle = nil; + + id identityKeyObject = [dictionary objectForKey:@"identityKey"]; + if (![identityKeyObject isKindOfClass:[NSString class]]) { + OWSFailDebug(@"Unexpected identityKeyObject: %@", [identityKeyObject class]); + return nil; + } + NSString *identityKeyString = (NSString *)identityKeyObject; + + id devicesObject = [dictionary objectForKey:@"devices"]; + if (![devicesObject isKindOfClass:[NSArray class]]) { + OWSFailDebug(@"Unexpected devicesObject: %@", [devicesObject class]); + return nil; + } + NSArray *devicesArray = (NSArray *)devicesObject; + + NSData *identityKey = [NSData dataFromBase64StringNoPadding:identityKeyString]; + + for (NSDictionary *deviceDict in devicesArray) { + NSNumber *registrationIdString = [deviceDict objectForKey:@"registrationId"]; + NSNumber *deviceIdString = [deviceDict objectForKey:@"deviceId"]; + + if (!(registrationIdString && deviceIdString)) { + OWSLogError(@"Failed to get the registration id and device id"); + return nil; + } + + if (![deviceIdString isEqualToNumber:number]) { + OWSLogWarn(@"Got a keyid for another device"); + return nil; + } + + int registrationId = [registrationIdString intValue]; + int deviceId = [deviceIdString intValue]; + + NSDictionary *_Nullable preKeyDict; + id optionalPreKeyDict = [deviceDict objectForKey:@"preKey"]; + // JSON parsing might give us NSNull, so we can't simply check for non-nil. + if ([optionalPreKeyDict isKindOfClass:[NSDictionary class]]) { + preKeyDict = (NSDictionary *)optionalPreKeyDict; + } + + int prekeyId; + NSData *_Nullable preKeyPublic; + + if (!preKeyDict) { + OWSLogInfo(@"No one-time prekey included in the bundle."); + prekeyId = -1; + } else { + prekeyId = [[preKeyDict objectForKey:@"keyId"] intValue]; + NSString *preKeyPublicString = [preKeyDict objectForKey:@"publicKey"]; + preKeyPublic = [NSData dataFromBase64StringNoPadding:preKeyPublicString]; + } + + NSDictionary *signedPrekey = [deviceDict objectForKey:@"signedPreKey"]; + + if (![signedPrekey isKindOfClass:[NSDictionary class]]) { + OWSLogError(@"Device doesn't have signed prekeys registered"); + return nil; + } + + NSNumber *signedKeyIdNumber = [signedPrekey objectForKey:@"keyId"]; + NSString *signedSignatureString = [signedPrekey objectForKey:@"signature"]; + NSString *signedPublicKeyString = [signedPrekey objectForKey:@"publicKey"]; + + + if (!(signedKeyIdNumber && signedPublicKeyString && signedSignatureString)) { + OWSLogError(@"Missing signed key material"); + return nil; + } + + NSData *signedPrekeyPublic = [NSData dataFromBase64StringNoPadding:signedPublicKeyString]; + NSData *signedPreKeySignature = [NSData dataFromBase64StringNoPadding:signedSignatureString]; + + if (!(signedPrekeyPublic && signedPreKeySignature)) { + OWSLogError(@"Failed to parse signed keying material"); + return nil; + } + + int signedPreKeyId = [signedKeyIdNumber intValue]; + + bundle = [[self alloc] initWithRegistrationId:registrationId + deviceId:deviceId + preKeyId:prekeyId + preKeyPublic:preKeyPublic + signedPreKeyPublic:signedPrekeyPublic + signedPreKeyId:signedPreKeyId + signedPreKeySignature:signedPreKeySignature + identityKey:identityKey]; + } + + return bundle; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/PreKeyRefreshOperation.swift b/SignalUtilitiesKit/PreKeyRefreshOperation.swift new file mode 100644 index 000000000..a23349651 --- /dev/null +++ b/SignalUtilitiesKit/PreKeyRefreshOperation.swift @@ -0,0 +1,105 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +// We generate 100 one-time prekeys at a time. We should replenish +// whenever ~2/3 of them have been consumed. +let kEphemeralPreKeysMinimumCount: UInt = 35 + +@objc(SSKRefreshPreKeysOperation) +public class RefreshPreKeysOperation: OWSOperation { + + private var tsAccountManager: TSAccountManager { + return TSAccountManager.sharedInstance() + } + + private var accountServiceClient: AccountServiceClient { + return AccountServiceClient.shared + } + + private var primaryStorage: OWSPrimaryStorage { + return OWSPrimaryStorage.shared() + } + + private var identityKeyManager: OWSIdentityManager { + return OWSIdentityManager.shared() + } + + public override func run() { + Logger.debug("") + + guard tsAccountManager.isRegistered() else { + Logger.debug("Skipping pre key refresh; user isn't registered.") + return + } + + // Loki: Doing this on the global queue to match Signal + DispatchQueue.global().async { + SessionManagementProtocol.refreshSignedPreKey() + self.reportSuccess() + } + + /* Loki: Original code + * ================ + firstly { + self.accountServiceClient.getPreKeysCount() + }.then(on: DispatchQueue.global()) { preKeysCount -> Promise in + Logger.debug("preKeysCount: \(preKeysCount)") + guard preKeysCount < kEphemeralPreKeysMinimumCount || self.primaryStorage.currentSignedPrekeyId() == nil else { + Logger.debug("Available keys sufficient: \(preKeysCount)") + return Promise.value(()) + } + + let identityKey: Data = self.identityKeyManager.identityKeyPair()!.publicKey + let signedPreKeyRecord: SignedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord() + let preKeyRecords: [PreKeyRecord] = self.primaryStorage.generatePreKeyRecords() + + self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + self.primaryStorage.storePreKeyRecords(preKeyRecords) + + return firstly { + self.accountServiceClient.setPreKeys(identityKey: identityKey, signedPreKeyRecord: signedPreKeyRecord, preKeyRecords: preKeyRecords) + }.done { + signedPreKeyRecord.markAsAcceptedByService() + self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id) + + TSPreKeyManager.clearPreKeyUpdateFailureCount() + TSPreKeyManager.clearSignedPreKeyRecords() + } + }.done { + Logger.debug("done") + self.reportSuccess() + }.catch { error in + self.reportError(error) + }.retainUntilComplete() + * ================ + */ + } + + public override func didSucceed() { + TSPreKeyManager.refreshPreKeysDidSucceed() + } + + override public func didFail(error: Error) { + switch error { + case let networkManagerError as NetworkManagerError: + guard !networkManagerError.isNetworkError else { + Logger.debug("Don't report SPK rotation failure w/ network error") + return + } + + guard networkManagerError.statusCode >= 400 && networkManagerError.statusCode <= 599 else { + Logger.debug("Don't report SPK rotation failure w/ non application error") + return + } + + TSPreKeyManager.incrementPreKeyUpdateFailureCount() + default: + Logger.debug("Don't report SPK rotation failure w/ non NetworkManager error: \(error)") + } + } +} diff --git a/SignalUtilitiesKit/ProfileManagerProtocol.h b/SignalUtilitiesKit/ProfileManagerProtocol.h new file mode 100644 index 000000000..01694420a --- /dev/null +++ b/SignalUtilitiesKit/ProfileManagerProtocol.h @@ -0,0 +1,46 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +@class OWSAES256Key; +@class TSThread; +@class YapDatabaseReadWriteTransaction; +@class OWSUserProfile; + +NS_ASSUME_NONNULL_BEGIN + +@protocol ProfileManagerProtocol + +- (OWSAES256Key *)localProfileKey; + +- (nullable NSString *)localProfileName; +- (nullable NSString *)profileNameForRecipientWithID:(NSString *)recipientID avoidingWriteTransaction:(BOOL)avoidWriteTransaction; +- (nullable NSString *)profileNameForRecipientWithID:(NSString *)recipientID; +- (nullable NSString *)profileNameForRecipientWithID:(NSString *)recipientID transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (nullable NSString *)profilePictureURL; + +- (nullable NSData *)profileKeyDataForRecipientId:(NSString *)recipientId; +- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId; +- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId avatarURL:(nullable NSString *)avatarURL; + +- (BOOL)isUserInProfileWhitelist:(NSString *)recipientId; + +- (BOOL)isThreadInProfileWhitelist:(TSThread *)thread; + +- (void)addUserToProfileWhitelist:(NSString *)recipientId; +- (void)addGroupIdToProfileWhitelist:(NSData *)groupId; +- (void)addThreadToProfileWhitelist:(TSThread *)thread; + +- (void)fetchLocalUsersProfile; + +- (void)fetchProfileForRecipientId:(NSString *)recipientId; + +- (void)updateProfileForContactWithID:(NSString *)contactID displayName:(NSString *)displayName with:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName avatarURL:(nullable NSString *)avatarURL; + +- (void)ensureLocalProfileCached; +- (void)ensureProfileCachedForContactWithID:(NSString *)contactID with:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Promise+retainUntilComplete.swift b/SignalUtilitiesKit/Promise+retainUntilComplete.swift new file mode 100644 index 000000000..34a9f60c0 --- /dev/null +++ b/SignalUtilitiesKit/Promise+retainUntilComplete.swift @@ -0,0 +1,63 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import PromiseKit + +@objc +public extension AnyPromise { + /** + * Sometimes there isn't a straight forward candidate to retain a promise, in that case we tell the + * promise to self retain, until it completes to avoid the risk it's GC'd before completion. + */ + @objc + func retainUntilComplete() { + var retainCycle: AnyPromise? = self + _ = self.ensure { + assert(retainCycle != nil) + retainCycle = nil + } + } +} + +public extension PMKFinalizer { + /** + * Sometimes there isn't a straight forward candidate to retain a promise, in that case we tell the + * promise to self retain, until it completes to avoid the risk it's GC'd before completion. + */ + func retainUntilComplete() { + var retainCycle: PMKFinalizer? = self + _ = self.finally { + assert(retainCycle != nil) + retainCycle = nil + } + } +} + +public extension Promise { + /** + * Sometimes there isn't a straight forward candidate to retain a promise, in that case we tell the + * promise to self retain, until it completes to avoid the risk it's GC'd before completion. + */ + func retainUntilComplete() { + var retainCycle: Promise? = self + _ = self.ensure { + assert(retainCycle != nil) + retainCycle = nil + } + } +} + +public extension Guarantee { + /** + * Sometimes there isn't a straight forward candidate to retain a promise, in that case we tell the + * promise to self retain, until it completes to avoid the risk it's GC'd before completion. + */ + func retainUntilComplete() { + var retainCycle: Guarantee? = self + _ = self.done { _ in + assert(retainCycle != nil) + retainCycle = nil + } + } +} diff --git a/SignalUtilitiesKit/ProofOfWork.swift b/SignalUtilitiesKit/ProofOfWork.swift new file mode 100644 index 000000000..cbcfb5e4e --- /dev/null +++ b/SignalUtilitiesKit/ProofOfWork.swift @@ -0,0 +1,109 @@ +import CryptoSwift + +private extension UInt64 { + + fileprivate init(_ decimal: Decimal) { + self.init(truncating: decimal as NSDecimalNumber) + } + + // Convert a UInt8 array to a UInt64 + fileprivate init(_ bytes: [UInt8]) { + precondition(bytes.count <= MemoryLayout.size) + var value: UInt64 = 0 + for byte in bytes { + value <<= 8 + value |= UInt64(byte) + } + self.init(value) + } +} + +private extension MutableCollection where Element == UInt8, Index == Int { + + /// Increment every element by the given amount + /// + /// - Parameter amount: The amount to increment by + /// - Returns: The incremented collection + fileprivate func increment(by amount: Int) -> Self { + var result = self + var increment = amount + for i in (0.. 0 else { break } + let sum = Int(result[i]) + increment + result[i] = UInt8(sum % 256) + increment = sum / 256 + } + return result + } +} + +/** + * The main proof of work logic. + * + * This was copied from the desktop messenger. + * Ref: libloki/proof-of-work.js + */ +public enum ProofOfWork { + + // If this changes then we also have to use something other than UInt64 to support the new length + private static let nonceLength = 8 + + /// Calculate a proof of work with the given configuration + /// + /// Ref: https://bitmessage.org/wiki/Proof_of_work + /// + /// - Parameters: + /// - data: The message data + /// - pubKey: The message recipient + /// - timestamp: The timestamp + /// - ttl: The message time to live in milliseconds + /// - Returns: A nonce string or `nil` if it failed + public static func calculate(data: String, pubKey: String, timestamp: UInt64, ttl: UInt64) -> String? { + let payload = createPayload(pubKey: pubKey, data: data, timestamp: timestamp, ttl: ttl) + let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: Int(SnodeAPI.powDifficulty)) + + // Start with the max value + var trialValue = UInt64.max + + let initialHash = payload.sha512() + var nonce = [UInt8](repeating: 0, count: nonceLength) + + while trialValue > target { + nonce = nonce.increment(by: 1) + + // This is different to the bitmessage PoW + // resultHash = hash(nonce + hash(data)) ==> hash(nonce + initialHash) + let resultHash = (nonce + initialHash).sha512() + let trialValueArray = Array(resultHash[0..<8]) + trialValue = UInt64(trialValueArray) + } + + return nonce.toBase64() + } + + /// Get the proof of work payload + private static func createPayload(pubKey: String, data: String, timestamp: UInt64, ttl: UInt64) -> [UInt8] { + let timestampString = String(timestamp) + let ttlString = String(ttl) + let payloadString = timestampString + ttlString + pubKey + data + return payloadString.bytes + } + + /// Calculate the target we need to reach + private static func calcTarget(ttl: UInt64, payloadLength: Int, nonceTrials: Int) -> UInt64 { + let two16 = UInt64(pow(2, 16) - 1) + let two64 = UInt64(pow(2, 64) - 1) + + // Do all the calculations + let totalLength = UInt64(payloadLength + nonceLength) + let ttlInSeconds = ttl / 1000 + let ttlMult = ttlInSeconds * totalLength + + // UInt64 values + let innerFrac = ttlMult / two16 + let lenPlusInnerFrac = totalLength + innerFrac + let denominator = UInt64(nonceTrials) * lenPlusInnerFrac + + return two64 / denominator + } +} diff --git a/SignalUtilitiesKit/ProtoUtils.h b/SignalUtilitiesKit/ProtoUtils.h new file mode 100644 index 000000000..b8a466fc7 --- /dev/null +++ b/SignalUtilitiesKit/ProtoUtils.h @@ -0,0 +1,29 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoCallMessageBuilder; +@class SSKProtoDataMessageBuilder; +@class TSThread; + +@interface ProtoUtils : NSObject + +- (instancetype)init NS_UNAVAILABLE; + ++ (void)addLocalProfileKeyIfNecessary:(TSThread *)thread + recipientId:(NSString *_Nullable)recipientId + dataMessageBuilder:(SSKProtoDataMessageBuilder *)dataMessageBuilder; + ++ (void)addLocalProfileKeyToDataMessageBuilder:(SSKProtoDataMessageBuilder *)dataMessageBuilder; + ++ (void)addLocalProfileKeyIfNecessary:(TSThread *)thread + recipientId:(NSString *)recipientId + callMessageBuilder:(SSKProtoCallMessageBuilder *)callMessageBuilder; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/ProtoUtils.m b/SignalUtilitiesKit/ProtoUtils.m new file mode 100644 index 000000000..e02d6917f --- /dev/null +++ b/SignalUtilitiesKit/ProtoUtils.m @@ -0,0 +1,97 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "ProtoUtils.h" +#import "ProfileManagerProtocol.h" +#import "SSKEnvironment.h" +#import "TSThread.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation ProtoUtils + +#pragma mark - Dependencies + ++ (id)profileManager { + return SSKEnvironment.shared.profileManager; +} + ++ (OWSAES256Key *)localProfileKey +{ + return self.profileManager.localProfileKey; +} + +#pragma mark - + ++ (BOOL)shouldMessageHaveLocalProfileKey:(TSThread *)thread recipientId:(NSString *_Nullable)recipientId +{ + OWSAssertDebug(thread); + + // For 1:1 threads, we want to include the profile key IFF the + // contact is in the whitelist. + // + // For Group threads, we want to include the profile key IFF the + // recipient OR the group is in the whitelist. + if (recipientId.length > 0 && [self.profileManager isUserInProfileWhitelist:recipientId]) { + return YES; + } else if ([self.profileManager isThreadInProfileWhitelist:thread]) { + return YES; + } + + return NO; +} + ++ (void)addLocalProfileKeyIfNecessary:(TSThread *)thread + recipientId:(NSString *_Nullable)recipientId + dataMessageBuilder:(SSKProtoDataMessageBuilder *)dataMessageBuilder +{ + OWSAssertDebug(thread); + OWSAssertDebug(dataMessageBuilder); + + if ([self shouldMessageHaveLocalProfileKey:thread recipientId:recipientId]) { + [dataMessageBuilder setProfileKey:self.localProfileKey.keyData]; + + if (recipientId.length > 0) { + // Once we've shared our profile key with a user (perhaps due to being + // a member of a whitelisted group), make sure they're whitelisted. + // FIXME PERF avoid this dispatch. It's going to happen for *each* recipient in a group message. + dispatch_async(dispatch_get_main_queue(), ^{ + [self.profileManager addUserToProfileWhitelist:recipientId]; + }); + } + } +} + ++ (void)addLocalProfileKeyToDataMessageBuilder:(SSKProtoDataMessageBuilder *)dataMessageBuilder +{ + OWSAssertDebug(dataMessageBuilder); + + [dataMessageBuilder setProfileKey:self.localProfileKey.keyData]; +} + ++ (void)addLocalProfileKeyIfNecessary:(TSThread *)thread + recipientId:(NSString *)recipientId + callMessageBuilder:(SSKProtoCallMessageBuilder *)callMessageBuilder +{ + OWSAssertDebug(thread); + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(callMessageBuilder); + + if ([self shouldMessageHaveLocalProfileKey:thread recipientId:recipientId]) { + [callMessageBuilder setProfileKey:self.localProfileKey.keyData]; + + // Once we've shared our profile key with a user (perhaps due to being + // a member of a whitelisted group), make sure they're whitelisted. + // FIXME PERF avoid this dispatch. It's going to happen for *each* recipient in a group message. + dispatch_async(dispatch_get_main_queue(), ^{ + [self.profileManager addUserToProfileWhitelist:recipientId]; + }); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Provisioning.pb.swift b/SignalUtilitiesKit/Provisioning.pb.swift new file mode 100644 index 000000000..713fb00c9 --- /dev/null +++ b/SignalUtilitiesKit/Provisioning.pb.swift @@ -0,0 +1,254 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Provisioning.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +//* +// Copyright (C) 2014-2016 Open Whisper Systems +// +// Licensed according to the LICENSE file in this repository. + +/// iOS - since we use a modern proto-compiler, we must specify +/// the legacy proto format. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct ProvisioningProtos_ProvisionEnvelope { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var publicKey: Data { + get {return _publicKey ?? SwiftProtobuf.Internal.emptyData} + set {_publicKey = newValue} + } + /// Returns true if `publicKey` has been explicitly set. + var hasPublicKey: Bool {return self._publicKey != nil} + /// Clears the value of `publicKey`. Subsequent reads from it will return its default value. + mutating func clearPublicKey() {self._publicKey = nil} + + /// @required + var body: Data { + get {return _body ?? SwiftProtobuf.Internal.emptyData} + set {_body = newValue} + } + /// Returns true if `body` has been explicitly set. + var hasBody: Bool {return self._body != nil} + /// Clears the value of `body`. Subsequent reads from it will return its default value. + mutating func clearBody() {self._body = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _publicKey: Data? = nil + fileprivate var _body: Data? = nil +} + +struct ProvisioningProtos_ProvisionMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var identityKeyPublic: Data { + get {return _identityKeyPublic ?? SwiftProtobuf.Internal.emptyData} + set {_identityKeyPublic = newValue} + } + /// Returns true if `identityKeyPublic` has been explicitly set. + var hasIdentityKeyPublic: Bool {return self._identityKeyPublic != nil} + /// Clears the value of `identityKeyPublic`. Subsequent reads from it will return its default value. + mutating func clearIdentityKeyPublic() {self._identityKeyPublic = nil} + + /// @required + var identityKeyPrivate: Data { + get {return _identityKeyPrivate ?? SwiftProtobuf.Internal.emptyData} + set {_identityKeyPrivate = newValue} + } + /// Returns true if `identityKeyPrivate` has been explicitly set. + var hasIdentityKeyPrivate: Bool {return self._identityKeyPrivate != nil} + /// Clears the value of `identityKeyPrivate`. Subsequent reads from it will return its default value. + mutating func clearIdentityKeyPrivate() {self._identityKeyPrivate = nil} + + /// @required + var number: String { + get {return _number ?? String()} + set {_number = newValue} + } + /// Returns true if `number` has been explicitly set. + var hasNumber: Bool {return self._number != nil} + /// Clears the value of `number`. Subsequent reads from it will return its default value. + mutating func clearNumber() {self._number = nil} + + /// @required + var provisioningCode: String { + get {return _provisioningCode ?? String()} + set {_provisioningCode = newValue} + } + /// Returns true if `provisioningCode` has been explicitly set. + var hasProvisioningCode: Bool {return self._provisioningCode != nil} + /// Clears the value of `provisioningCode`. Subsequent reads from it will return its default value. + mutating func clearProvisioningCode() {self._provisioningCode = nil} + + /// @required + var userAgent: String { + get {return _userAgent ?? String()} + set {_userAgent = newValue} + } + /// Returns true if `userAgent` has been explicitly set. + var hasUserAgent: Bool {return self._userAgent != nil} + /// Clears the value of `userAgent`. Subsequent reads from it will return its default value. + mutating func clearUserAgent() {self._userAgent = nil} + + /// @required + var profileKey: Data { + get {return _profileKey ?? SwiftProtobuf.Internal.emptyData} + set {_profileKey = newValue} + } + /// Returns true if `profileKey` has been explicitly set. + var hasProfileKey: Bool {return self._profileKey != nil} + /// Clears the value of `profileKey`. Subsequent reads from it will return its default value. + mutating func clearProfileKey() {self._profileKey = nil} + + /// @required + var readReceipts: Bool { + get {return _readReceipts ?? false} + set {_readReceipts = newValue} + } + /// Returns true if `readReceipts` has been explicitly set. + var hasReadReceipts: Bool {return self._readReceipts != nil} + /// Clears the value of `readReceipts`. Subsequent reads from it will return its default value. + mutating func clearReadReceipts() {self._readReceipts = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _identityKeyPublic: Data? = nil + fileprivate var _identityKeyPrivate: Data? = nil + fileprivate var _number: String? = nil + fileprivate var _provisioningCode: String? = nil + fileprivate var _userAgent: String? = nil + fileprivate var _profileKey: Data? = nil + fileprivate var _readReceipts: Bool? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "ProvisioningProtos" + +extension ProvisioningProtos_ProvisionEnvelope: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ProvisionEnvelope" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "publicKey"), + 2: .same(proto: "body"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._publicKey) + case 2: try decoder.decodeSingularBytesField(value: &self._body) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._publicKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._body { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ProvisioningProtos_ProvisionEnvelope, rhs: ProvisioningProtos_ProvisionEnvelope) -> Bool { + if lhs._publicKey != rhs._publicKey {return false} + if lhs._body != rhs._body {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ProvisioningProtos_ProvisionMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ProvisionMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "identityKeyPublic"), + 2: .same(proto: "identityKeyPrivate"), + 3: .same(proto: "number"), + 4: .same(proto: "provisioningCode"), + 5: .same(proto: "userAgent"), + 6: .same(proto: "profileKey"), + 7: .same(proto: "readReceipts"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._identityKeyPublic) + case 2: try decoder.decodeSingularBytesField(value: &self._identityKeyPrivate) + case 3: try decoder.decodeSingularStringField(value: &self._number) + case 4: try decoder.decodeSingularStringField(value: &self._provisioningCode) + case 5: try decoder.decodeSingularStringField(value: &self._userAgent) + case 6: try decoder.decodeSingularBytesField(value: &self._profileKey) + case 7: try decoder.decodeSingularBoolField(value: &self._readReceipts) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._identityKeyPublic { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._identityKeyPrivate { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + if let v = self._number { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if let v = self._provisioningCode { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } + if let v = self._userAgent { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } + if let v = self._profileKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 6) + } + if let v = self._readReceipts { + try visitor.visitSingularBoolField(value: v, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ProvisioningProtos_ProvisionMessage, rhs: ProvisioningProtos_ProvisionMessage) -> Bool { + if lhs._identityKeyPublic != rhs._identityKeyPublic {return false} + if lhs._identityKeyPrivate != rhs._identityKeyPrivate {return false} + if lhs._number != rhs._number {return false} + if lhs._provisioningCode != rhs._provisioningCode {return false} + if lhs._userAgent != rhs._userAgent {return false} + if lhs._profileKey != rhs._profileKey {return false} + if lhs._readReceipts != rhs._readReceipts {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/SignalUtilitiesKit/ProvisioningProto.swift b/SignalUtilitiesKit/ProvisioningProto.swift new file mode 100644 index 000000000..4b1f7c948 --- /dev/null +++ b/SignalUtilitiesKit/ProvisioningProto.swift @@ -0,0 +1,310 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +// WARNING: This code is generated. Only edit within the markers. + +public enum ProvisioningProtoError: Error { + case invalidProtobuf(description: String) +} + +// MARK: - ProvisioningProtoProvisionEnvelope + +@objc public class ProvisioningProtoProvisionEnvelope: NSObject { + + // MARK: - ProvisioningProtoProvisionEnvelopeBuilder + + @objc public class func builder(publicKey: Data, body: Data) -> ProvisioningProtoProvisionEnvelopeBuilder { + return ProvisioningProtoProvisionEnvelopeBuilder(publicKey: publicKey, body: body) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> ProvisioningProtoProvisionEnvelopeBuilder { + let builder = ProvisioningProtoProvisionEnvelopeBuilder(publicKey: publicKey, body: body) + return builder + } + + @objc public class ProvisioningProtoProvisionEnvelopeBuilder: NSObject { + + private var proto = ProvisioningProtos_ProvisionEnvelope() + + @objc fileprivate override init() {} + + @objc fileprivate init(publicKey: Data, body: Data) { + super.init() + + setPublicKey(publicKey) + setBody(body) + } + + @objc public func setPublicKey(_ valueParam: Data) { + proto.publicKey = valueParam + } + + @objc public func setBody(_ valueParam: Data) { + proto.body = valueParam + } + + @objc public func build() throws -> ProvisioningProtoProvisionEnvelope { + return try ProvisioningProtoProvisionEnvelope.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try ProvisioningProtoProvisionEnvelope.parseProto(proto).serializedData() + } + } + + fileprivate let proto: ProvisioningProtos_ProvisionEnvelope + + @objc public let publicKey: Data + + @objc public let body: Data + + private init(proto: ProvisioningProtos_ProvisionEnvelope, + publicKey: Data, + body: Data) { + self.proto = proto + self.publicKey = publicKey + self.body = body + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> ProvisioningProtoProvisionEnvelope { + let proto = try ProvisioningProtos_ProvisionEnvelope(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: ProvisioningProtos_ProvisionEnvelope) throws -> ProvisioningProtoProvisionEnvelope { + guard proto.hasPublicKey else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: publicKey") + } + let publicKey = proto.publicKey + + guard proto.hasBody else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: body") + } + let body = proto.body + + // MARK: - Begin Validation Logic for ProvisioningProtoProvisionEnvelope - + + // MARK: - End Validation Logic for ProvisioningProtoProvisionEnvelope - + + let result = ProvisioningProtoProvisionEnvelope(proto: proto, + publicKey: publicKey, + body: body) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension ProvisioningProtoProvisionEnvelope { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension ProvisioningProtoProvisionEnvelope.ProvisioningProtoProvisionEnvelopeBuilder { + @objc public func buildIgnoringErrors() -> ProvisioningProtoProvisionEnvelope? { + return try! self.build() + } +} + +#endif + +// MARK: - ProvisioningProtoProvisionMessage + +@objc public class ProvisioningProtoProvisionMessage: NSObject { + + // MARK: - ProvisioningProtoProvisionMessageBuilder + + @objc public class func builder(identityKeyPublic: Data, identityKeyPrivate: Data, number: String, provisioningCode: String, userAgent: String, profileKey: Data, readReceipts: Bool) -> ProvisioningProtoProvisionMessageBuilder { + return ProvisioningProtoProvisionMessageBuilder(identityKeyPublic: identityKeyPublic, identityKeyPrivate: identityKeyPrivate, number: number, provisioningCode: provisioningCode, userAgent: userAgent, profileKey: profileKey, readReceipts: readReceipts) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> ProvisioningProtoProvisionMessageBuilder { + let builder = ProvisioningProtoProvisionMessageBuilder(identityKeyPublic: identityKeyPublic, identityKeyPrivate: identityKeyPrivate, number: number, provisioningCode: provisioningCode, userAgent: userAgent, profileKey: profileKey, readReceipts: readReceipts) + return builder + } + + @objc public class ProvisioningProtoProvisionMessageBuilder: NSObject { + + private var proto = ProvisioningProtos_ProvisionMessage() + + @objc fileprivate override init() {} + + @objc fileprivate init(identityKeyPublic: Data, identityKeyPrivate: Data, number: String, provisioningCode: String, userAgent: String, profileKey: Data, readReceipts: Bool) { + super.init() + + setIdentityKeyPublic(identityKeyPublic) + setIdentityKeyPrivate(identityKeyPrivate) + setNumber(number) + setProvisioningCode(provisioningCode) + setUserAgent(userAgent) + setProfileKey(profileKey) + setReadReceipts(readReceipts) + } + + @objc public func setIdentityKeyPublic(_ valueParam: Data) { + proto.identityKeyPublic = valueParam + } + + @objc public func setIdentityKeyPrivate(_ valueParam: Data) { + proto.identityKeyPrivate = valueParam + } + + @objc public func setNumber(_ valueParam: String) { + proto.number = valueParam + } + + @objc public func setProvisioningCode(_ valueParam: String) { + proto.provisioningCode = valueParam + } + + @objc public func setUserAgent(_ valueParam: String) { + proto.userAgent = valueParam + } + + @objc public func setProfileKey(_ valueParam: Data) { + proto.profileKey = valueParam + } + + @objc public func setReadReceipts(_ valueParam: Bool) { + proto.readReceipts = valueParam + } + + @objc public func build() throws -> ProvisioningProtoProvisionMessage { + return try ProvisioningProtoProvisionMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try ProvisioningProtoProvisionMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: ProvisioningProtos_ProvisionMessage + + @objc public let identityKeyPublic: Data + + @objc public let identityKeyPrivate: Data + + @objc public let number: String + + @objc public let provisioningCode: String + + @objc public let userAgent: String + + @objc public let profileKey: Data + + @objc public let readReceipts: Bool + + private init(proto: ProvisioningProtos_ProvisionMessage, + identityKeyPublic: Data, + identityKeyPrivate: Data, + number: String, + provisioningCode: String, + userAgent: String, + profileKey: Data, + readReceipts: Bool) { + self.proto = proto + self.identityKeyPublic = identityKeyPublic + self.identityKeyPrivate = identityKeyPrivate + self.number = number + self.provisioningCode = provisioningCode + self.userAgent = userAgent + self.profileKey = profileKey + self.readReceipts = readReceipts + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> ProvisioningProtoProvisionMessage { + let proto = try ProvisioningProtos_ProvisionMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: ProvisioningProtos_ProvisionMessage) throws -> ProvisioningProtoProvisionMessage { + guard proto.hasIdentityKeyPublic else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: identityKeyPublic") + } + let identityKeyPublic = proto.identityKeyPublic + + guard proto.hasIdentityKeyPrivate else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: identityKeyPrivate") + } + let identityKeyPrivate = proto.identityKeyPrivate + + guard proto.hasNumber else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: number") + } + let number = proto.number + + guard proto.hasProvisioningCode else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: provisioningCode") + } + let provisioningCode = proto.provisioningCode + + guard proto.hasUserAgent else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: userAgent") + } + let userAgent = proto.userAgent + + guard proto.hasProfileKey else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: profileKey") + } + let profileKey = proto.profileKey + + guard proto.hasReadReceipts else { + throw ProvisioningProtoError.invalidProtobuf(description: "\(logTag) missing required field: readReceipts") + } + let readReceipts = proto.readReceipts + + // MARK: - Begin Validation Logic for ProvisioningProtoProvisionMessage - + + // MARK: - End Validation Logic for ProvisioningProtoProvisionMessage - + + let result = ProvisioningProtoProvisionMessage(proto: proto, + identityKeyPublic: identityKeyPublic, + identityKeyPrivate: identityKeyPrivate, + number: number, + provisioningCode: provisioningCode, + userAgent: userAgent, + profileKey: profileKey, + readReceipts: readReceipts) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension ProvisioningProtoProvisionMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension ProvisioningProtoProvisionMessage.ProvisioningProtoProvisionMessageBuilder { + @objc public func buildIgnoringErrors() -> ProvisioningProtoProvisionMessage? { + return try! self.build() + } +} + +#endif diff --git a/SignalUtilitiesKit/ProxiedContentDownloader.swift b/SignalUtilitiesKit/ProxiedContentDownloader.swift new file mode 100644 index 000000000..5b6d2f23e --- /dev/null +++ b/SignalUtilitiesKit/ProxiedContentDownloader.swift @@ -0,0 +1,932 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import ObjectiveC + +// Stills should be loaded before full GIFs. +public enum ProxiedContentRequestPriority { + case low, high +} + +// MARK: - + +@objc +open class ProxiedContentAssetDescription: NSObject { + @objc + public let url: NSURL + + @objc + public let fileExtension: String + + public init?(url: NSURL, + fileExtension: String? = nil) { + self.url = url + + if let fileExtension = fileExtension { + self.fileExtension = fileExtension + } else { + guard let pathExtension = url.pathExtension else { + owsFailDebug("URL has not path extension.") + return nil + } + self.fileExtension = pathExtension + } + } +} + +// MARK: - + +public enum ProxiedContentAssetSegmentState: UInt { + case waiting + case downloading + case complete + case failed +} + +// MARK: - + +public class ProxiedContentAssetSegment: NSObject { + + public let index: UInt + public let segmentStart: UInt + public let segmentLength: UInt + // The amount of the segment that is overlap. + // The overlap lies in the _first_ n bytes of the segment data. + public let redundantLength: UInt + + // This state should only be accessed on the main thread. + public var state: ProxiedContentAssetSegmentState = .waiting { + didSet { + AssertIsOnMainThread() + } + } + + // This state is accessed off the main thread. + // + // * During downloads it will be accessed on the task delegate queue. + // * After downloads it will be accessed on a worker queue. + private var segmentData = Data() + + // This state should only be accessed on the main thread. + public weak var task: URLSessionDataTask? + + init(index: UInt, + segmentStart: UInt, + segmentLength: UInt, + redundantLength: UInt) { + self.index = index + self.segmentStart = segmentStart + self.segmentLength = segmentLength + self.redundantLength = redundantLength + } + + public func totalDataSize() -> UInt { + return UInt(segmentData.count) + } + + public func append(data: Data) { + guard state == .downloading else { + owsFailDebug("appending data in invalid state: \(state)") + return + } + + segmentData.append(data) + } + + public func mergeData(assetData: inout Data) -> Bool { + guard state == .complete else { + owsFailDebug("merging data in invalid state: \(state)") + return false + } + guard UInt(segmentData.count) == segmentLength else { + owsFailDebug("segment data length: \(segmentData.count) doesn't match expected length: \(segmentLength)") + return false + } + + // In some cases the last two segments will overlap. + // In that case, we only want to append the non-overlapping + // tail of the segment data. + let bytesToIgnore = Int(redundantLength) + if bytesToIgnore > 0 { + let subdata = segmentData.subdata(in: bytesToIgnore.. Void)? + private var failure: ((ProxiedContentAssetRequest) -> Void)? + + var wasCancelled = false + // This property is an internal implementation detail of the download process. + var assetFilePath: String? + + // This state should only be accessed on the main thread. + private var segments = [ProxiedContentAssetSegment]() + public var state: ProxiedContentAssetRequestState = .waiting + public var contentLength: Int = 0 { + didSet { + AssertIsOnMainThread() + assert(oldValue == 0) + assert(contentLength > 0) + } + } + public weak var contentLengthTask: URLSessionDataTask? + + init(assetDescription: ProxiedContentAssetDescription, + priority: ProxiedContentRequestPriority, + success:@escaping ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void), + failure:@escaping ((ProxiedContentAssetRequest) -> Void)) { + self.assetDescription = assetDescription + self.priority = priority + self.success = success + self.failure = failure + + super.init() + } + + private func segmentSize() -> UInt { + AssertIsOnMainThread() + + let contentLength = UInt(self.contentLength) + guard contentLength > 0 else { + owsFailDebug("asset missing contentLength") + requestDidFail() + return 0 + } + + let k1MB: UInt = 1024 * 1024 + let k500KB: UInt = 500 * 1024 + let k100KB: UInt = 100 * 1024 + let k50KB: UInt = 50 * 1024 + let k10KB: UInt = 10 * 1024 + let k1KB: UInt = 1 * 1024 + for segmentSize in [k1MB, k500KB, k100KB, k50KB, k10KB, k1KB ] { + if contentLength >= segmentSize { + return segmentSize + } + } + return contentLength + } + + fileprivate func createSegments(withInitialData initialData: Data) { + AssertIsOnMainThread() + + let segmentLength = segmentSize() + guard segmentLength > 0 else { + return + } + let contentLength = UInt(self.contentLength) + + // Make the initial segment. + let assetSegment = ProxiedContentAssetSegment(index: 0, + segmentStart: 0, + segmentLength: UInt(initialData.count), + redundantLength: 0) + // "Download" the initial segment using the initialData. + assetSegment.state = .downloading + assetSegment.append(data: initialData) + // Mark the initial segment as complete. + assetSegment.state = .complete + segments.append(assetSegment) + + var nextSegmentStart = UInt(initialData.count) + var index: UInt = 1 + while nextSegmentStart < contentLength { + var segmentStart: UInt = nextSegmentStart + var redundantLength: UInt = 0 + // The last segment may overlap the penultimate segment + // in order to keep the segment sizes uniform. + if segmentStart + segmentLength > contentLength { + redundantLength = segmentStart + segmentLength - contentLength + segmentStart = contentLength - segmentLength + } + let assetSegment = ProxiedContentAssetSegment(index: index, + segmentStart: segmentStart, + segmentLength: segmentLength, + redundantLength: redundantLength) + segments.append(assetSegment) + nextSegmentStart = segmentStart + segmentLength + index += 1 + } + } + + private func firstSegmentWithState(state: ProxiedContentAssetSegmentState) -> ProxiedContentAssetSegment? { + AssertIsOnMainThread() + + for segment in segments { + guard segment.state != .failed else { + owsFailDebug("unexpected failed segment.") + continue + } + if segment.state == state { + return segment + } + } + return nil + } + + public func firstWaitingSegment() -> ProxiedContentAssetSegment? { + AssertIsOnMainThread() + + return firstSegmentWithState(state: .waiting) + } + + public func downloadingSegmentsCount() -> UInt { + AssertIsOnMainThread() + + var result: UInt = 0 + for segment in segments { + guard segment.state != .failed else { + owsFailDebug("unexpected failed segment.") + continue + } + if segment.state == .downloading { + result += 1 + } + } + return result + } + + public func areAllSegmentsComplete() -> Bool { + AssertIsOnMainThread() + + for segment in segments { + guard segment.state == .complete else { + return false + } + } + return true + } + + public func writeAssetToFile(downloadFolderPath: String) -> ProxiedContentAsset? { + + var assetData = Data() + for segment in segments { + guard segment.state == .complete else { + owsFailDebug("unexpected incomplete segment.") + return nil + } + guard segment.totalDataSize() > 0 else { + owsFailDebug("could not merge empty segment.") + return nil + } + guard segment.mergeData(assetData: &assetData) else { + owsFailDebug("failed to merge segment data.") + return nil + } + } + + guard assetData.count == contentLength else { + owsFailDebug("asset data has unexpected length.") + return nil + } + + guard assetData.count > 0 else { + owsFailDebug("could not write empty asset to disk.") + return nil + } + + let fileExtension = assetDescription.fileExtension + let fileName = (NSUUID().uuidString as NSString).appendingPathExtension(fileExtension)! + let filePath = (downloadFolderPath as NSString).appendingPathComponent(fileName) + + Logger.verbose("filePath: \(filePath).") + + do { + try assetData.write(to: NSURL.fileURL(withPath: filePath), options: .atomicWrite) + let asset = ProxiedContentAsset(assetDescription: assetDescription, filePath: filePath) + return asset + } catch let error as NSError { + owsFailDebug("file write failed: \(filePath), \(error)") + return nil + } + } + + public func cancel() { + AssertIsOnMainThread() + + wasCancelled = true + contentLengthTask?.cancel() + contentLengthTask = nil + for segment in segments { + segment.task?.cancel() + segment.task = nil + } + + // Don't call the callbacks if the request is cancelled. + clearCallbacks() + } + + private func clearCallbacks() { + AssertIsOnMainThread() + + success = nil + failure = nil + } + + public func requestDidSucceed(asset: ProxiedContentAsset) { + AssertIsOnMainThread() + + success?(self, asset) + + // Only one of the callbacks should be called, and only once. + clearCallbacks() + } + + public func requestDidFail() { + AssertIsOnMainThread() + + failure?(self) + + // Only one of the callbacks should be called, and only once. + clearCallbacks() + } +} + +// MARK: - + +// Represents a downloaded asset. +// +// The blob on disk is cleaned up when this instance is deallocated, +// so consumers of this resource should retain a strong reference to +// this instance as long as they are using the asset. +@objc +public class ProxiedContentAsset: NSObject { + + @objc + public let assetDescription: ProxiedContentAssetDescription + + @objc + public let filePath: String + + init(assetDescription: ProxiedContentAssetDescription, + filePath: String) { + self.assetDescription = assetDescription + self.filePath = filePath + } + + deinit { + // Clean up on the asset on disk. + let filePathCopy = filePath + DispatchQueue.global().async { + do { + let fileManager = FileManager.default + try fileManager.removeItem(atPath: filePathCopy) + } catch let error as NSError { + owsFailDebug("file cleanup failed: \(filePathCopy), \(error)") + } + } + } +} + +// MARK: - + +private var URLSessionTaskProxiedContentAssetRequest: UInt8 = 0 +private var URLSessionTaskProxiedContentAssetSegment: UInt8 = 0 + +// This extension is used to punch an asset request onto a download task. +extension URLSessionTask { + var assetRequest: ProxiedContentAssetRequest { + get { + return objc_getAssociatedObject(self, &URLSessionTaskProxiedContentAssetRequest) as! ProxiedContentAssetRequest + } + set { + objc_setAssociatedObject(self, &URLSessionTaskProxiedContentAssetRequest, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + var assetSegment: ProxiedContentAssetSegment { + get { + return objc_getAssociatedObject(self, &URLSessionTaskProxiedContentAssetSegment) as! ProxiedContentAssetSegment + } + set { + objc_setAssociatedObject(self, &URLSessionTaskProxiedContentAssetSegment, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } +} + +// MARK: - + +@objc +open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate { + + // MARK: - Properties + + @objc + public static let defaultDownloader = ProxiedContentDownloader(downloadFolderName: "proxiedContent") + + private let downloadFolderName: String + + private var downloadFolderPath: String? + + // Force usage as a singleton + public required init(downloadFolderName: String) { + AssertIsOnMainThread() + + self.downloadFolderName = downloadFolderName + + super.init() + + SwiftSingletons.register(self) + + ensureDownloadFolder() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + private lazy var downloadSession: URLSession = { + AssertIsOnMainThread() + + let configuration = ContentProxy.sessionConfiguration() + + // Don't use any caching to protect privacy of these requests. + configuration.urlCache = nil + configuration.requestCachePolicy = .reloadIgnoringCacheData + + configuration.httpMaximumConnectionsPerHost = 10 + let session = URLSession(configuration: configuration, + delegate: self, + delegateQueue: nil) + return session + }() + + // 100 entries of which at least half will probably be stills. + // Actual animated GIFs will usually be less than 3 MB so the + // max size of the cache on disk should be ~150 MB. Bear in mind + // that assets are not always deleted on disk as soon as they are + // evacuated from the cache; if a cache consumer (e.g. view) is + // still using the asset, the asset won't be deleted on disk until + // it is no longer in use. + private var assetMap = LRUCache(maxSize: 100) + // TODO: We could use a proper queue, e.g. implemented with a linked + // list. + private var assetRequestQueue = [ProxiedContentAssetRequest]() + + // The success and failure callbacks are always called on main queue. + // + // The success callbacks may be called synchronously on cache hit, in + // which case the ProxiedContentAssetRequest parameter will be nil. + public func requestAsset(assetDescription: ProxiedContentAssetDescription, + priority: ProxiedContentRequestPriority, + success:@escaping ((ProxiedContentAssetRequest?, ProxiedContentAsset) -> Void), + failure:@escaping ((ProxiedContentAssetRequest) -> Void)) -> ProxiedContentAssetRequest? { + AssertIsOnMainThread() + + if let asset = assetMap.get(key: assetDescription.url) { + // Synchronous cache hit. + Logger.verbose("asset cache hit: \(assetDescription.url)") + success(nil, asset) + return nil + } + + // Cache miss. + // + // Asset requests are done queued and performed asynchronously. + Logger.verbose("asset cache miss: \(assetDescription.url)") + let assetRequest = ProxiedContentAssetRequest(assetDescription: assetDescription, + priority: priority, + success: success, + failure: failure) + assetRequestQueue.append(assetRequest) + // Process the queue (which may start this request) + // asynchronously so that the caller has time to store + // a reference to the asset request returned by this + // method before its success/failure handler is called. + processRequestQueueAsync() + return assetRequest + } + + public func cancelAllRequests() { + AssertIsOnMainThread() + + Logger.verbose("cancelAllRequests") + + self.assetRequestQueue.forEach { $0.cancel() } + self.assetRequestQueue = [] + } + + private func segmentRequestDidSucceed(assetRequest: ProxiedContentAssetRequest, assetSegment: ProxiedContentAssetSegment) { + DispatchQueue.main.async { + assetSegment.state = .complete + + if !self.tryToCompleteRequest(assetRequest: assetRequest) { + self.processRequestQueueSync() + } + } + } + + // Returns true if the request is completed. + private func tryToCompleteRequest(assetRequest: ProxiedContentAssetRequest) -> Bool { + AssertIsOnMainThread() + + guard assetRequest.areAllSegmentsComplete() else { + return false + } + + // If the asset request has completed all of its segments, + // try to write the asset to file. + assetRequest.state = .complete + + // Move write off main thread. + DispatchQueue.global().async { + guard let downloadFolderPath = self.downloadFolderPath else { + owsFailDebug("Missing downloadFolderPath") + return + } + guard let asset = assetRequest.writeAssetToFile(downloadFolderPath: downloadFolderPath) else { + self.segmentRequestDidFail(assetRequest: assetRequest) + return + } + self.assetRequestDidSucceed(assetRequest: assetRequest, asset: asset) + } + return true + } + + private func assetRequestDidSucceed(assetRequest: ProxiedContentAssetRequest, asset: ProxiedContentAsset) { + DispatchQueue.main.async { + self.assetMap.set(key: assetRequest.assetDescription.url, value: asset) + self.removeAssetRequestFromQueue(assetRequest: assetRequest) + assetRequest.requestDidSucceed(asset: asset) + } + } + + private func segmentRequestDidFail(assetRequest: ProxiedContentAssetRequest, assetSegment: ProxiedContentAssetSegment? = nil) { + DispatchQueue.main.async { + if let assetSegment = assetSegment { + assetSegment.state = .failed + + // TODO: If we wanted to implement segment retry, we'd do so here. + // For now, we just fail the entire asset request. + } + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + } + } + + private func assetRequestDidFail(assetRequest: ProxiedContentAssetRequest) { + + DispatchQueue.main.async { + self.removeAssetRequestFromQueue(assetRequest: assetRequest) + assetRequest.requestDidFail() + } + } + + private func removeAssetRequestFromQueue(assetRequest: ProxiedContentAssetRequest) { + AssertIsOnMainThread() + + guard assetRequestQueue.contains(assetRequest) else { + Logger.warn("could not remove asset request from queue: \(assetRequest.assetDescription.url)") + return + } + + assetRequestQueue = assetRequestQueue.filter { $0 != assetRequest } + // Process the queue async to ensure that state in the downloader + // classes is consistent before we try to start a new request. + processRequestQueueAsync() + } + + private func processRequestQueueAsync() { + DispatchQueue.main.async { + self.processRequestQueueSync() + } + } + + // * Start a segment request or content length request if possible. + // * Complete/cancel asset requests if possible. + // + private func processRequestQueueSync() { + AssertIsOnMainThread() + + guard let assetRequest = popNextAssetRequest() else { + return + } + guard !assetRequest.wasCancelled else { + // Discard the cancelled asset request and try again. + removeAssetRequestFromQueue(assetRequest: assetRequest) + return + } + guard CurrentAppContext().isMainAppAndActive else { + // If app is not active, fail the asset request. + assetRequest.state = .failed + assetRequestDidFail(assetRequest: assetRequest) + processRequestQueueSync() + return + } + + if let asset = assetMap.get(key: assetRequest.assetDescription.url) { + // Deferred cache hit, avoids re-downloading assets that were + // downloaded while this request was queued. + + assetRequest.state = .complete + assetRequestDidSucceed(assetRequest: assetRequest, asset: asset) + return + } + + if assetRequest.state == .waiting { + // If asset request hasn't yet determined the resource size, + // try to do so now, by requesting a small initial segment. + assetRequest.state = .requestingSize + + let segmentStart: UInt = 0 + // Vary the initial segment size to obscure the length of the response headers. + let segmentLength: UInt = 1024 + UInt(arc4random_uniform(1024)) + var request = URLRequest(url: assetRequest.assetDescription.url as URL) + request.httpShouldUsePipelining = true + let rangeHeaderValue = "bytes=\(segmentStart)-\(segmentStart + segmentLength - 1)" + request.addValue(rangeHeaderValue, forHTTPHeaderField: "Range") + + guard ContentProxy.configureProxiedRequest(request: &request) else { + assetRequest.state = .failed + assetRequestDidFail(assetRequest: assetRequest) + processRequestQueueSync() + return + } + + let task = downloadSession.dataTask(with: request, completionHandler: { data, response, error -> Void in + self.handleAssetSizeResponse(assetRequest: assetRequest, data: data, response: response, error: error) + }) + + assetRequest.contentLengthTask = task + task.resume() + } else { + // Start a download task. + + guard let assetSegment = assetRequest.firstWaitingSegment() else { + owsFailDebug("queued asset request does not have a waiting segment.") + return + } + assetSegment.state = .downloading + + var request = URLRequest(url: assetRequest.assetDescription.url as URL) + request.httpShouldUsePipelining = true + let rangeHeaderValue = "bytes=\(assetSegment.segmentStart)-\(assetSegment.segmentStart + assetSegment.segmentLength - 1)" + request.addValue(rangeHeaderValue, forHTTPHeaderField: "Range") + + guard ContentProxy.configureProxiedRequest(request: &request) else { + assetRequest.state = .failed + assetRequestDidFail(assetRequest: assetRequest) + processRequestQueueSync() + return + } + + let task: URLSessionDataTask = downloadSession.dataTask(with: request) + task.assetRequest = assetRequest + task.assetSegment = assetSegment + assetSegment.task = task + task.resume() + } + + // Recurse; we may be able to start multiple downloads. + processRequestQueueSync() + } + + private func handleAssetSizeResponse(assetRequest: ProxiedContentAssetRequest, data: Data?, response: URLResponse?, error: Error?) { + guard error == nil else { + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + return + } + guard let data = data, + data.count > 0 else { + owsFailDebug("Asset size response missing data.") + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + return + } + guard let httpResponse = response as? HTTPURLResponse else { + owsFailDebug("Asset size response is invalid.") + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + return + } + var firstContentRangeString: String? + for header in httpResponse.allHeaderFields.keys { + guard let headerString = header as? String else { + owsFailDebug("Invalid header: \(header)") + continue + } + if headerString.lowercased() == "content-range" { + firstContentRangeString = httpResponse.allHeaderFields[header] as? String + } + } + guard let contentRangeString = firstContentRangeString else { + owsFailDebug("Asset size response is missing content range.") + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + return + } + + // Example: content-range: bytes 0-1023/7630 + guard let contentLengthString = NSRegularExpression.parseFirstMatch(pattern: "^bytes \\d+\\-\\d+/(\\d+)$", + text: contentRangeString) else { + owsFailDebug("Asset size response has invalid content range.") + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + return + } + guard contentLengthString.count > 0, + let contentLength = Int(contentLengthString) else { + owsFailDebug("Asset size response has unparsable content length.") + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + return + } + guard contentLength > 0 else { + owsFailDebug("Asset size response has invalid content length.") + assetRequest.state = .failed + self.assetRequestDidFail(assetRequest: assetRequest) + return + } + + DispatchQueue.main.async { + assetRequest.contentLength = contentLength + assetRequest.createSegments(withInitialData: data) + assetRequest.state = .active + + if !self.tryToCompleteRequest(assetRequest: assetRequest) { + self.processRequestQueueSync() + } + } + } + + // Return the first asset request for which we either: + // + // * Need to download the content length. + // * Need to download at least one of its segments. + private func popNextAssetRequest() -> ProxiedContentAssetRequest? { + AssertIsOnMainThread() + + let kMaxAssetRequestCount: UInt = 3 + let kMaxAssetRequestsPerAssetCount: UInt = kMaxAssetRequestCount - 1 + + // Prefer the first "high" priority request; + // fall back to the first "low" priority request. + var activeAssetRequestsCount: UInt = 0 + for priority in [ProxiedContentRequestPriority.high, ProxiedContentRequestPriority.low] { + for assetRequest in assetRequestQueue where assetRequest.priority == priority { + switch assetRequest.state { + case .waiting: + // This asset request needs its content length. + return assetRequest + case .requestingSize: + activeAssetRequestsCount += 1 + // Ensure that only N requests are active at a time. + guard activeAssetRequestsCount < kMaxAssetRequestCount else { + return nil + } + continue + case .active: + break + case .complete: + continue + case .failed: + continue + } + + let downloadingSegmentsCount = assetRequest.downloadingSegmentsCount() + activeAssetRequestsCount += downloadingSegmentsCount + // Ensure that only N segment requests are active per asset at a time. + guard downloadingSegmentsCount < kMaxAssetRequestsPerAssetCount else { + continue + } + // Ensure that only N requests are active at a time. + guard activeAssetRequestsCount < kMaxAssetRequestCount else { + return nil + } + guard assetRequest.firstWaitingSegment() != nil else { + /// Asset request does not have a waiting segment. + continue + } + return assetRequest + } + } + + return nil + } + + // MARK: URLSessionDataDelegate + + @nonobjc + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + + completionHandler(.allow) + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + let assetRequest = dataTask.assetRequest + let assetSegment = dataTask.assetSegment + guard !assetRequest.wasCancelled else { + dataTask.cancel() + segmentRequestDidFail(assetRequest: assetRequest, assetSegment: assetSegment) + return + } + assetSegment.append(data: data) + } + + public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) { + completionHandler(nil) + } + + // MARK: URLSessionTaskDelegate + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + + let assetRequest = task.assetRequest + let assetSegment = task.assetSegment + guard !assetRequest.wasCancelled else { + task.cancel() + segmentRequestDidFail(assetRequest: assetRequest, assetSegment: assetSegment) + return + } + if let error = error { + Logger.error("download failed with error: \(error)") + segmentRequestDidFail(assetRequest: assetRequest, assetSegment: assetSegment) + return + } + guard let httpResponse = task.response as? HTTPURLResponse else { + Logger.error("missing or unexpected response: \(String(describing: task.response))") + segmentRequestDidFail(assetRequest: assetRequest, assetSegment: assetSegment) + return + } + let statusCode = httpResponse.statusCode + guard statusCode >= 200 && statusCode < 400 else { + Logger.error("response has invalid status code: \(statusCode)") + segmentRequestDidFail(assetRequest: assetRequest, assetSegment: assetSegment) + return + } + guard assetSegment.totalDataSize() == assetSegment.segmentLength else { + Logger.error("segment is missing data: \(statusCode)") + segmentRequestDidFail(assetRequest: assetRequest, assetSegment: assetSegment) + return + } + + segmentRequestDidSucceed(assetRequest: assetRequest, assetSegment: assetSegment) + } + + // MARK: Temp Directory + + public func ensureDownloadFolder() { + // We write assets to the temporary directory so that iOS can clean them up. + // We try to eagerly clean up these assets when they are no longer in use. + + let tempDirPath = OWSTemporaryDirectory() + let dirPath = (tempDirPath as NSString).appendingPathComponent(downloadFolderName) + do { + let fileManager = FileManager.default + + // Try to delete existing folder if necessary. + if fileManager.fileExists(atPath: dirPath) { + try fileManager.removeItem(atPath: dirPath) + downloadFolderPath = dirPath + } + // Try to create folder if necessary. + if !fileManager.fileExists(atPath: dirPath) { + try fileManager.createDirectory(atPath: dirPath, + withIntermediateDirectories: true, + attributes: nil) + downloadFolderPath = dirPath + } + + // Don't back up ProxiedContent downloads. + OWSFileSystem.protectFileOrFolder(atPath: dirPath) + } catch let error as NSError { + owsFailDebug("ensureTempFolder failed: \(dirPath), \(error)") + downloadFolderPath = tempDirPath + } + } +} diff --git a/SignalUtilitiesKit/PublicChatManager.swift b/SignalUtilitiesKit/PublicChatManager.swift new file mode 100644 index 000000000..d6ed42553 --- /dev/null +++ b/SignalUtilitiesKit/PublicChatManager.swift @@ -0,0 +1,133 @@ +import PromiseKit + +// TODO: Clean + +@objc(LKPublicChatManager) +public final class PublicChatManager : NSObject { + private let storage = OWSPrimaryStorage.shared() + @objc public var chats: [String:OpenGroup] = [:] + private var pollers: [String:PublicChatPoller] = [:] + private var isPolling = false + + private var userHexEncodedPublicKey: String? { + return OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey + } + + public enum Error : Swift.Error { + case chatCreationFailed + case userPublicKeyNotFound + } + + @objc public static let shared = PublicChatManager() + + private override init() { + super.init() + NotificationCenter.default.addObserver(self, selector: #selector(onThreadDeleted(_:)), name: .threadDeleted, object: nil) + refreshChatsAndPollers() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc public func startPollersIfNeeded() { + for (threadID, publicChat) in chats { + if let poller = pollers[threadID] { + poller.startIfNeeded() + } else { + let poller = PublicChatPoller(for: publicChat) + poller.startIfNeeded() + pollers[threadID] = poller + } + } + isPolling = true + } + + @objc public func stopPollers() { + for poller in pollers.values { poller.stop() } + isPolling = false + } + + public func addChat(server: String, channel: UInt64) -> Promise { + if let existingChat = getChat(server: server, channel: channel) { + if let newChat = self.addChat(server: server, channel: channel, name: existingChat.displayName) { + return Promise.value(newChat) + } else { + return Promise(error: Error.chatCreationFailed) + } + } + return OpenGroupAPI.getInfo(for: channel, on: server).map2 { channelInfo -> OpenGroup in + guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed } + return chat + } + } + + @discardableResult + @objc(addChatWithServer:channel:name:) + public func addChat(server: String, channel: UInt64, name: String) -> OpenGroup? { + guard let chat = OpenGroup(channel: channel, server: server, displayName: name, isDeletable: true) else { return nil } + let model = TSGroupModel(title: chat.displayName, memberIds: [userHexEncodedPublicKey!, chat.server], image: nil, groupId: LKGroupUtilities.getEncodedOpenGroupIDAsData(chat.id), groupType: .openGroup, adminIds: []) + + // Store the group chat mapping + Storage.writeSync { transaction in + let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) + + // Save the group chat + LokiDatabaseUtilities.setPublicChat(chat, for: thread.uniqueId!, in: transaction) + } + + // Update chats and pollers + self.refreshChatsAndPollers() + + return chat + } + + @objc(addChatWithServer:channel:) + public func objc_addChat(server: String, channel: UInt64) -> AnyPromise { + return AnyPromise.from(addChat(server: server, channel: channel)) + } + + @objc func refreshChatsAndPollers() { + storage.dbReadConnection.read { transaction in + let newChats = LokiDatabaseUtilities.getAllPublicChats(in: transaction) + + // Remove any chats that don't exist in the database + let removedChatThreadIds = self.chats.keys.filter { !newChats.keys.contains($0) } + removedChatThreadIds.forEach { threadID in + let poller = self.pollers.removeValue(forKey: threadID) + poller?.stop() + } + + // Only append to chats if we have a thread for the chat + self.chats = newChats.filter { (threadID, group) in + return TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) != nil + } + } + + if (isPolling) { startPollersIfNeeded() } + } + + @objc private func onThreadDeleted(_ notification: Notification) { + guard let threadId = notification.userInfo?["threadId"] as? String else { return } + + // Reset the last message cache + if let chat = self.chats[threadId] { + Storage.write { transaction in + Storage.clearAllData(for: chat.channel, on: chat.server, using: transaction) + } + } + + // Remove the chat from the db + Storage.writeSync { transaction in + LokiDatabaseUtilities.removePublicChat(for: threadId, in: transaction) + } + + refreshChatsAndPollers() + } + + public func getChat(server: String, channel: UInt64) -> OpenGroup? { + return chats.values.first { chat in + return chat.server == server && chat.channel == channel + } + } +} diff --git a/SignalUtilitiesKit/PublicChatPoller.swift b/SignalUtilitiesKit/PublicChatPoller.swift new file mode 100644 index 000000000..dfd03a974 --- /dev/null +++ b/SignalUtilitiesKit/PublicChatPoller.swift @@ -0,0 +1,248 @@ +import PromiseKit + +@objc(LKPublicChatPoller) +public final class PublicChatPoller : NSObject { + private let publicChat: OpenGroup + private var pollForNewMessagesTimer: Timer? = nil + private var pollForDeletedMessagesTimer: Timer? = nil + private var pollForModeratorsTimer: Timer? = nil + private var pollForDisplayNamesTimer: Timer? = nil + private var hasStarted = false + private var isPolling = false + + // MARK: Settings + private let pollForNewMessagesInterval: TimeInterval = 4 + private let pollForDeletedMessagesInterval: TimeInterval = 60 + private let pollForModeratorsInterval: TimeInterval = 10 * 60 + private let pollForDisplayNamesInterval: TimeInterval = 60 + + // MARK: Lifecycle + @objc(initForPublicChat:) + public init(for publicChat: OpenGroup) { + self.publicChat = publicChat + super.init() + } + + @objc public func startIfNeeded() { + if hasStarted { return } + DispatchQueue.main.async { [weak self] in // Timers don't do well on background queues + guard let strongSelf = self else { return } + strongSelf.pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForNewMessagesInterval, repeats: true) { _ in self?.pollForNewMessages() } + strongSelf.pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForDeletedMessagesInterval, repeats: true) { _ in self?.pollForDeletedMessages() } + strongSelf.pollForModeratorsTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForModeratorsInterval, repeats: true) { _ in self?.pollForModerators() } + strongSelf.pollForDisplayNamesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForDisplayNamesInterval, repeats: true) { _ in self?.pollForDisplayNames() } + // Perform initial updates + strongSelf.pollForNewMessages() + strongSelf.pollForDeletedMessages() + strongSelf.pollForModerators() + strongSelf.pollForDisplayNames() + strongSelf.hasStarted = true + } + } + + @objc public func stop() { + pollForNewMessagesTimer?.invalidate() + pollForDeletedMessagesTimer?.invalidate() + pollForModeratorsTimer?.invalidate() + pollForDisplayNamesTimer?.invalidate() + hasStarted = false + } + + // MARK: Polling + @objc(pollForNewMessages) + public func objc_pollForNewMessages() -> AnyPromise { + AnyPromise.from(pollForNewMessages()) + } + + public func pollForNewMessages() -> Promise { + guard !self.isPolling else { return Promise.value(()) } + self.isPolling = true + let publicChat = self.publicChat + let userPublicKey = getUserHexEncodedPublicKey() + return OpenGroupAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global(qos: .default)) { messages in + self.isPolling = false + let uniquePublicKeys = Set(messages.map { $0.senderPublicKey }) + func proceed() { + let storage = OWSPrimaryStorage.shared() + /* + var newDisplayNameUpdatees: Set = [] + storage.dbReadConnection.read { transaction in + newDisplayNameUpdatees = Set(uniquePublicKeys.filter { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) != $0 }.compactMap { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) }) + } + if !newDisplayNameUpdatees.isEmpty { + let displayNameUpdatees = OpenGroupAPI.displayNameUpdatees[publicChat.id] ?? [] + OpenGroupAPI.displayNameUpdatees[publicChat.id] = displayNameUpdatees.union(newDisplayNameUpdatees) + } + */ + // Sorting the messages by timestamp before importing them fixes an issue where messages that quote older messages can't find those older messages + messages.sorted { $0.serverTimestamp < $1.serverTimestamp }.forEach { message in + var wasSentByCurrentUser = false + OWSPrimaryStorage.shared().dbReadConnection.read { transaction in + wasSentByCurrentUser = LokiDatabaseUtilities.isUserLinkedDevice(message.senderPublicKey, transaction: transaction) + } + var masterPublicKey: String? = nil + storage.dbReadConnection.read { transaction in + masterPublicKey = storage.getMasterHexEncodedPublicKey(for: message.senderPublicKey, in: transaction) + } + let senderPublicKey = masterPublicKey ?? message.senderPublicKey + func generateDisplayName(from rawDisplayName: String) -> String { + let endIndex = senderPublicKey.endIndex + let cutoffIndex = senderPublicKey.index(endIndex, offsetBy: -8) + return "\(rawDisplayName) (...\(senderPublicKey[cutoffIndex.. 0) { + SSKEnvironment.shared.profileManager.updateProfileForContact(withID: masterPublicKey!, displayName: message.displayName, with: transaction) + } + SSKEnvironment.shared.profileManager.updateService(withProfileName: message.displayName, avatarURL: profilePicture.url) + SSKEnvironment.shared.profileManager.setProfileKeyData(profilePicture.profileKey, forRecipientId: masterPublicKey!, avatarURL: profilePicture.url) + } + } + } + } + /* + let hexEncodedPublicKeysToUpdate = uniquePublicKeys.filter { hexEncodedPublicKey in + let timeSinceLastUpdate: TimeInterval + if let lastDeviceLinkUpdate = MultiDeviceProtocol.lastDeviceLinkUpdate[hexEncodedPublicKey] { + timeSinceLastUpdate = Date().timeIntervalSince(lastDeviceLinkUpdate) + } else { + timeSinceLastUpdate = .infinity + } + return timeSinceLastUpdate > MultiDeviceProtocol.deviceLinkUpdateInterval + } + if !hexEncodedPublicKeysToUpdate.isEmpty { + FileServerAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global(qos: .default)) { _ in + proceed() + hexEncodedPublicKeysToUpdate.forEach { + MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy + } + }.catch(on: DispatchQueue.global(qos: .default)) { error in + if (error as? DotNetAPI.DotNetAPIError) == DotNetAPI.DotNetAPIError.parsingFailed { + // Don't immediately re-fetch in case of failure due to a parsing error + hexEncodedPublicKeysToUpdate.forEach { + MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy + } + } + proceed() + } + } else { + */ + DispatchQueue.global(qos: .default).async { + proceed() + } + /* + } + */ + } + } + + private func pollForDeletedMessages() { + let publicChat = self.publicChat + let _ = OpenGroupAPI.getDeletedMessageServerIDs(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global(qos: .default)) { deletedMessageServerIDs in + Storage.writeSync { transaction in + let deletedMessageIDs = deletedMessageServerIDs.compactMap { OWSPrimaryStorage.shared().getIDForMessage(withServerID: UInt($0), in: transaction) } + deletedMessageIDs.forEach { messageID in + TSMessage.fetch(uniqueId: messageID)?.remove(with: transaction) + } + } + } + } + + private func pollForModerators() { + let _ = OpenGroupAPI.getModerators(for: publicChat.channel, on: publicChat.server) + } + + private func pollForDisplayNames() { + let _ = OpenGroupAPI.getDisplayNames(for: publicChat.channel, on: publicChat.server) + } +} diff --git a/SignalUtilitiesKit/ReachabilityManager.swift b/SignalUtilitiesKit/ReachabilityManager.swift new file mode 100644 index 000000000..bea630d5c --- /dev/null +++ b/SignalUtilitiesKit/ReachabilityManager.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import Reachability + +@objc(SSKReachabilityType) +public enum ReachabilityType: Int { + case any, wifi, cellular +} + +@objc +public protocol SSKReachabilityManager { + var observationContext: AnyObject { get } + func setup() + + var isReachable: Bool { get } + func isReachable(via reachabilityType: ReachabilityType) -> Bool +} + +@objc +public class SSKReachabilityManagerImpl: NSObject, SSKReachabilityManager { + + public let reachability: Reachability + public var observationContext: AnyObject { + return self.reachability + } + + public var isReachable: Bool { + return isReachable(via: .any) + } + + public func isReachable(via reachabilityType: ReachabilityType) -> Bool { + switch reachabilityType { + case .any: + return reachability.isReachable() + case .wifi: + return reachability.isReachableViaWiFi() + case .cellular: + return reachability.isReachableViaWWAN() + } + } + + @objc + override public init() { + self.reachability = Reachability.forInternetConnection() + } + + @objc + public func setup() { + guard reachability.startNotifier() else { + owsFailDebug("failed to start notifier") + return + } + Logger.debug("started notifier") + } +} diff --git a/SignalUtilitiesKit/ReverseDispatchQueue.swift b/SignalUtilitiesKit/ReverseDispatchQueue.swift new file mode 100644 index 000000000..1133f4384 --- /dev/null +++ b/SignalUtilitiesKit/ReverseDispatchQueue.swift @@ -0,0 +1,75 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +// This is intended to be a drop-in replacement for DispatchQueue +// that processes its queue in reverse order. +@objc +public class ReverseDispatchQueue: NSObject { + + private static let isVerbose: Bool = false + + private let label: String + private let serialQueue: DispatchQueue + + // TODO: We could allow creation with various QOS. + @objc + public required init(label: String) { + self.label = label + serialQueue = DispatchQueue(label: label) + + super.init() + } + + public typealias WorkBlock = () -> Void + + private class Item { + let workBlock: WorkBlock + let index: UInt64 + + required init(workBlock : @escaping WorkBlock, index: UInt64) { + self.workBlock = workBlock + self.index = index + } + } + + // These properties should only be accessed on serialQueue. + private var items = [Item]() + private var indexCounter: UInt64 = 0 + + @objc + public func async(workBlock : @escaping WorkBlock) { + serialQueue.async { + self.indexCounter = self.indexCounter + 1 + let index = self.indexCounter + let item = Item(workBlock: workBlock, index: index ) + self.items.append(item) + + if ReverseDispatchQueue.isVerbose { + Logger.verbose("Enqueued[\(self.label)]: \(item.index)") + } + + self.process() + } + } + + private func process() { + serialQueue.async { + // Note that we popLast() so that we process + // the queue in the _reverse_ order from + // which it was enqueued. + guard let item = self.items.popLast() else { + // No enqueued work to do. + return + } + if ReverseDispatchQueue.isVerbose { + Logger.verbose("Processing[\(self.label)]: \(item.index)") + } + item.workBlock() + + self.process() + } + } +} diff --git a/SignalUtilitiesKit/RotateSignedKeyOperation.swift b/SignalUtilitiesKit/RotateSignedKeyOperation.swift new file mode 100644 index 000000000..1fc9368dd --- /dev/null +++ b/SignalUtilitiesKit/RotateSignedKeyOperation.swift @@ -0,0 +1,79 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +@objc(SSKRotateSignedPreKeyOperation) +public class RotateSignedPreKeyOperation: OWSOperation { + private var tsAccountManager: TSAccountManager { + return TSAccountManager.sharedInstance() + } + + private var accountServiceClient: AccountServiceClient { + return AccountServiceClient.shared + } + + private var primaryStorage: OWSPrimaryStorage { + return OWSPrimaryStorage.shared() + } + + public override func run() { + Logger.debug("") + + guard tsAccountManager.isRegistered() else { + Logger.debug("skipping - not registered") + return + } + + // Loki: Doing this on the global queue to match Signal + DispatchQueue.global().async { + SessionManagementProtocol.rotateSignedPreKey() + self.reportSuccess() + } + + /* Loki: Original code + * ================ + let signedPreKeyRecord: SignedPreKeyRecord = self.primaryStorage.generateRandomSignedRecord() + + self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + firstly { + return self.accountServiceClient.setSignedPreKey(signedPreKeyRecord) + }.done(on: DispatchQueue.global()) { + Logger.info("Successfully uploaded signed PreKey") + signedPreKeyRecord.markAsAcceptedByService() + self.primaryStorage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + self.primaryStorage.setCurrentSignedPrekeyId(signedPreKeyRecord.id) + + TSPreKeyManager.clearPreKeyUpdateFailureCount() + TSPreKeyManager.clearSignedPreKeyRecords() + + Logger.debug("done") + self.reportSuccess() + }.catch { error in + self.reportError(error) + }.retainUntilComplete() + * ================ + */ + } + + override public func didFail(error: Error) { + switch error { + case let networkManagerError as NetworkManagerError: + guard !networkManagerError.isNetworkError else { + Logger.debug("don't report SPK rotation failure w/ network error") + return + } + + guard networkManagerError.statusCode >= 400 && networkManagerError.statusCode <= 599 else { + Logger.debug("don't report SPK rotation failure w/ non application error") + return + } + + TSPreKeyManager.incrementPreKeyUpdateFailureCount() + default: + Logger.debug("don't report SPK rotation failure w/ non NetworkManager error: \(error)") + } + } +} diff --git a/SignalUtilitiesKit/SSKAsserts.h b/SignalUtilitiesKit/SSKAsserts.h new file mode 100755 index 000000000..e05bd1f8e --- /dev/null +++ b/SignalUtilitiesKit/SSKAsserts.h @@ -0,0 +1,68 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "AppContext.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Singleton Asserts + +// The "singleton asserts" can be used to ensure +// that we only create a singleton once. +// +// The simplest way to use them is the OWSSingletonAssert() macro. +// It is intended to be used inside the singleton's initializer. +// +// If, however, a singleton has multiple possible initializers, +// you need to: +// +// 1. Use OWSSingletonAssertFlag() outside the class definition. +// 2. Use OWSSingletonAssertInit() in each initializer. + +#ifdef DEBUG + +#define ENFORCE_SINGLETONS + +#endif + +#ifdef ENFORCE_SINGLETONS + +#define OWSSingletonAssertFlag() static BOOL _isSingletonCreated = NO; + +#define OWSSingletonAssertInit() \ + @synchronized([self class]) { \ + if (!CurrentAppContext().isRunningTests) { \ + OWSAssertDebug(!_isSingletonCreated); \ + _isSingletonCreated = YES; \ + } \ + } + +#define OWSSingletonAssert() OWSSingletonAssertFlag() OWSSingletonAssertInit() + +#else + +#define OWSSingletonAssertFlag() +#define OWSSingletonAssertInit() +#define OWSSingletonAssert() + +#endif + +#define OWSFailDebugUnlessRunningTests(_messageFormat, ...) \ + do { \ + if (!CurrentAppContext().isRunningTests) { \ + OWSFailDebug(_messageFormat, ##__VA_ARGS__); \ + } \ + } while (0) + +#define OWSCFailDebugUnlessRunningTests(_messageFormat, ...) \ + do { \ + if (!CurrentAppContext().isRunningTests) { \ + OWSCFailDebug(_messageFormat, ##__VA_ARGS__); \ + } \ + } while (NO) + + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SSKEnvironment.h b/SignalUtilitiesKit/SSKEnvironment.h new file mode 100644 index 000000000..a81965968 --- /dev/null +++ b/SignalUtilitiesKit/SSKEnvironment.h @@ -0,0 +1,118 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ContactDiscoveryService; +@class ContactsUpdater; +@class OWS2FAManager; +@class OWSAttachmentDownloads; +@class OWSBatchMessageProcessor; +@class OWSBlockingManager; +@class OWSDisappearingMessagesJob; +@class OWSIdentityManager; +@class OWSMessageDecrypter; +@class OWSMessageManager; +@class OWSMessageReceiver; +@class OWSMessageSender; +@class OWSOutgoingReceiptManager; +@class OWSPrimaryStorage; +@class OWSReadReceiptManager; +@class SSKMessageSenderJobQueue; +@class TSAccountManager; +@class TSNetworkManager; +@class TSSocketManager; +@class YapDatabaseConnection; + +@protocol ContactsManagerProtocol; +@protocol NotificationsProtocol; +@protocol OWSCallMessageHandler; +@protocol ProfileManagerProtocol; +@protocol OWSUDManager; +@protocol SSKReachabilityManager; +@protocol OWSSyncManagerProtocol; +@protocol OWSTypingIndicators; + +@interface SSKEnvironment : NSObject + +- (instancetype)initWithContactsManager:(id)contactsManager + messageSender:(OWSMessageSender *)messageSender + messageSenderJobQueue:(SSKMessageSenderJobQueue *)messageSenderJobQueue + profileManager:(id)profileManager + primaryStorage:(OWSPrimaryStorage *)primaryStorage + contactsUpdater:(ContactsUpdater *)contactsUpdater + networkManager:(TSNetworkManager *)networkManager + messageManager:(OWSMessageManager *)messageManager + blockingManager:(OWSBlockingManager *)blockingManager + identityManager:(OWSIdentityManager *)identityManager + udManager:(id)udManager + messageDecrypter:(OWSMessageDecrypter *)messageDecrypter + batchMessageProcessor:(OWSBatchMessageProcessor *)batchMessageProcessor + messageReceiver:(OWSMessageReceiver *)messageReceiver + socketManager:(TSSocketManager *)socketManager + tsAccountManager:(TSAccountManager *)tsAccountManager + ows2FAManager:(OWS2FAManager *)ows2FAManager + disappearingMessagesJob:(OWSDisappearingMessagesJob *)disappearingMessagesJob + contactDiscoveryService:(ContactDiscoveryService *)contactDiscoveryService + readReceiptManager:(OWSReadReceiptManager *)readReceiptManager + outgoingReceiptManager:(OWSOutgoingReceiptManager *)outgoingReceiptManager + reachabilityManager:(id)reachabilityManager + syncManager:(id)syncManager + typingIndicators:(id)typingIndicators + attachmentDownloads:(OWSAttachmentDownloads *)attachmentDownloads NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@property (nonatomic, readonly, class) SSKEnvironment *shared; + ++ (void)setShared:(SSKEnvironment *)env; + +#ifdef DEBUG +// Should only be called by tests. ++ (void)clearSharedForTests; +#endif + +@property (nonatomic, readonly) id contactsManager; +@property (nonatomic, readonly) OWSMessageSender *messageSender; +@property (nonatomic, readonly) SSKMessageSenderJobQueue *messageSenderJobQueue; +@property (nonatomic, readonly) id profileManager; +@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage; +@property (nonatomic, readonly) ContactsUpdater *contactsUpdater; +@property (nonatomic, readonly) TSNetworkManager *networkManager; +@property (nonatomic, readonly) OWSMessageManager *messageManager; +@property (nonatomic, readonly) OWSBlockingManager *blockingManager; +@property (nonatomic, readonly) OWSIdentityManager *identityManager; +@property (nonatomic, readonly) id udManager; +@property (nonatomic, readonly) OWSMessageDecrypter *messageDecrypter; +@property (nonatomic, readonly) OWSBatchMessageProcessor *batchMessageProcessor; +@property (nonatomic, readonly) OWSMessageReceiver *messageReceiver; +@property (nonatomic, readonly) TSSocketManager *socketManager; +@property (nonatomic, readonly) TSAccountManager *tsAccountManager; +@property (nonatomic, readonly) OWS2FAManager *ows2FAManager; +@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob; +@property (nonatomic, readonly) ContactDiscoveryService *contactDiscoveryService; +@property (nonatomic, readonly) OWSReadReceiptManager *readReceiptManager; +@property (nonatomic, readonly) OWSOutgoingReceiptManager *outgoingReceiptManager; +@property (nonatomic, readonly) id syncManager; +@property (nonatomic, readonly) id reachabilityManager; +@property (nonatomic, readonly) id typingIndicators; +@property (nonatomic, readonly) OWSAttachmentDownloads *attachmentDownloads; + +// This property is configured after Environment is created. +@property (atomic, nullable) id callMessageHandler; +// This property is configured after Environment is created. +@property (atomic, nullable) id notificationsManager; + +@property (atomic, readonly) YapDatabaseConnection *objectReadWriteConnection; +@property (atomic, readonly) YapDatabaseConnection *sessionStoreDBConnection; +@property (atomic, readonly) YapDatabaseConnection *migrationDBConnection; +@property (atomic, readonly) YapDatabaseConnection *analyticsDBConnection; + +- (BOOL)isComplete; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SSKEnvironment.m b/SignalUtilitiesKit/SSKEnvironment.m new file mode 100644 index 000000000..f690a5391 --- /dev/null +++ b/SignalUtilitiesKit/SSKEnvironment.m @@ -0,0 +1,245 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "SSKEnvironment.h" +#import "AppContext.h" +#import "OWSPrimaryStorage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +static SSKEnvironment *sharedSSKEnvironment; + +@interface SSKEnvironment () + +@property (nonatomic) id contactsManager; +@property (nonatomic) OWSMessageSender *messageSender; +@property (nonatomic) id profileManager; +@property (nonatomic) OWSPrimaryStorage *primaryStorage; +@property (nonatomic) ContactsUpdater *contactsUpdater; +@property (nonatomic) TSNetworkManager *networkManager; +@property (nonatomic) OWSMessageManager *messageManager; +@property (nonatomic) OWSBlockingManager *blockingManager; +@property (nonatomic) OWSIdentityManager *identityManager; +@property (nonatomic) id udManager; +@property (nonatomic) OWSMessageDecrypter *messageDecrypter; +@property (nonatomic) OWSBatchMessageProcessor *batchMessageProcessor; +@property (nonatomic) OWSMessageReceiver *messageReceiver; +@property (nonatomic) TSSocketManager *socketManager; +@property (nonatomic) TSAccountManager *tsAccountManager; +@property (nonatomic) OWS2FAManager *ows2FAManager; +@property (nonatomic) OWSDisappearingMessagesJob *disappearingMessagesJob; +@property (nonatomic) ContactDiscoveryService *contactDiscoveryService; +@property (nonatomic) OWSReadReceiptManager *readReceiptManager; +@property (nonatomic) OWSOutgoingReceiptManager *outgoingReceiptManager; +@property (nonatomic) id syncManager; +@property (nonatomic) id reachabilityManager; +@property (nonatomic) id typingIndicators; +@property (nonatomic) OWSAttachmentDownloads *attachmentDownloads; + +@end + +#pragma mark - + +@implementation SSKEnvironment + +@synthesize callMessageHandler = _callMessageHandler; +@synthesize notificationsManager = _notificationsManager; +@synthesize objectReadWriteConnection = _objectReadWriteConnection; +@synthesize sessionStoreDBConnection = _sessionStoreDBConnection; +@synthesize migrationDBConnection = _migrationDBConnection; +@synthesize analyticsDBConnection = _analyticsDBConnection; + +- (instancetype)initWithContactsManager:(id)contactsManager + messageSender:(OWSMessageSender *)messageSender + messageSenderJobQueue:(SSKMessageSenderJobQueue *)messageSenderJobQueue + profileManager:(id)profileManager + primaryStorage:(OWSPrimaryStorage *)primaryStorage + contactsUpdater:(ContactsUpdater *)contactsUpdater + networkManager:(TSNetworkManager *)networkManager + messageManager:(OWSMessageManager *)messageManager + blockingManager:(OWSBlockingManager *)blockingManager + identityManager:(OWSIdentityManager *)identityManager + udManager:(id)udManager + messageDecrypter:(OWSMessageDecrypter *)messageDecrypter + batchMessageProcessor:(OWSBatchMessageProcessor *)batchMessageProcessor + messageReceiver:(OWSMessageReceiver *)messageReceiver + socketManager:(TSSocketManager *)socketManager + tsAccountManager:(TSAccountManager *)tsAccountManager + ows2FAManager:(OWS2FAManager *)ows2FAManager + disappearingMessagesJob:(OWSDisappearingMessagesJob *)disappearingMessagesJob + contactDiscoveryService:(ContactDiscoveryService *)contactDiscoveryService + readReceiptManager:(OWSReadReceiptManager *)readReceiptManager + outgoingReceiptManager:(OWSOutgoingReceiptManager *)outgoingReceiptManager + reachabilityManager:(id)reachabilityManager + syncManager:(id)syncManager + typingIndicators:(id)typingIndicators + attachmentDownloads:(OWSAttachmentDownloads *)attachmentDownloads +{ + self = [super init]; + if (!self) { + return self; + } + + OWSAssertDebug(contactsManager); + OWSAssertDebug(messageSender); + OWSAssertDebug(messageSenderJobQueue); + OWSAssertDebug(profileManager); + OWSAssertDebug(primaryStorage); + OWSAssertDebug(contactsUpdater); + OWSAssertDebug(networkManager); + OWSAssertDebug(messageManager); + OWSAssertDebug(blockingManager); + OWSAssertDebug(identityManager); + OWSAssertDebug(udManager); + OWSAssertDebug(messageDecrypter); + OWSAssertDebug(batchMessageProcessor); + OWSAssertDebug(messageReceiver); + OWSAssertDebug(socketManager); + OWSAssertDebug(tsAccountManager); + OWSAssertDebug(ows2FAManager); + OWSAssertDebug(disappearingMessagesJob); + OWSAssertDebug(contactDiscoveryService); + OWSAssertDebug(readReceiptManager); + OWSAssertDebug(outgoingReceiptManager); + OWSAssertDebug(syncManager); + OWSAssertDebug(reachabilityManager); + OWSAssertDebug(typingIndicators); + OWSAssertDebug(attachmentDownloads); + + _contactsManager = contactsManager; + _messageSender = messageSender; + _messageSenderJobQueue = messageSenderJobQueue; + _profileManager = profileManager; + _primaryStorage = primaryStorage; + _contactsUpdater = contactsUpdater; + _networkManager = networkManager; + _messageManager = messageManager; + _blockingManager = blockingManager; + _identityManager = identityManager; + _udManager = udManager; + _messageDecrypter = messageDecrypter; + _batchMessageProcessor = batchMessageProcessor; + _messageReceiver = messageReceiver; + _socketManager = socketManager; + _tsAccountManager = tsAccountManager; + _ows2FAManager = ows2FAManager; + _disappearingMessagesJob = disappearingMessagesJob; + _contactDiscoveryService = contactDiscoveryService; + _readReceiptManager = readReceiptManager; + _outgoingReceiptManager = outgoingReceiptManager; + _syncManager = syncManager; + _reachabilityManager = reachabilityManager; + _typingIndicators = typingIndicators; + _attachmentDownloads = attachmentDownloads; + + return self; +} + ++ (instancetype)shared +{ + OWSAssertDebug(sharedSSKEnvironment); + + return sharedSSKEnvironment; +} + ++ (void)setShared:(SSKEnvironment *)env +{ + OWSAssertDebug(env); + OWSAssertDebug(!sharedSSKEnvironment || CurrentAppContext().isRunningTests); + + sharedSSKEnvironment = env; +} + ++ (void)clearSharedForTests +{ + sharedSSKEnvironment = nil; +} + +#pragma mark - Mutable Accessors +/* +- (nullable id)callMessageHandler +{ + @synchronized(self) { + OWSAssertDebug(_callMessageHandler); + + return _callMessageHandler; + } +} + +- (void)setCallMessageHandler:(nullable id)callMessageHandler +{ + @synchronized(self) { + OWSAssertDebug(callMessageHandler); + OWSAssertDebug(!_callMessageHandler); + + _callMessageHandler = callMessageHandler; + } +} + */ + +- (nullable id)notificationsManager +{ + @synchronized(self) { + OWSAssertDebug(_notificationsManager); + + return _notificationsManager; + } +} + +- (void)setNotificationsManager:(nullable id)notificationsManager +{ + @synchronized(self) { + OWSAssertDebug(notificationsManager); + OWSAssertDebug(!_notificationsManager); + + _notificationsManager = notificationsManager; + } +} + +- (BOOL)isComplete +{ + return self.notificationsManager != nil; +} + +- (YapDatabaseConnection *)objectReadWriteConnection +{ + @synchronized(self) { + if (!_objectReadWriteConnection) { + _objectReadWriteConnection = self.primaryStorage.newDatabaseConnection; + } + return _objectReadWriteConnection; + } +} + +- (YapDatabaseConnection *)sessionStoreDBConnection { + @synchronized(self) { + if (!_sessionStoreDBConnection) { + _sessionStoreDBConnection = self.primaryStorage.newDatabaseConnection; + } + return _sessionStoreDBConnection; + } +} + +- (YapDatabaseConnection *)migrationDBConnection { + @synchronized(self) { + if (!_migrationDBConnection) { + _migrationDBConnection = self.primaryStorage.newDatabaseConnection; + } + return _migrationDBConnection; + } +} + +- (YapDatabaseConnection *)analyticsDBConnection { + @synchronized(self) { + if (!_analyticsDBConnection) { + _analyticsDBConnection = self.primaryStorage.newDatabaseConnection; + } + return _analyticsDBConnection; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SSKIncrementingIdFinder.swift b/SignalUtilitiesKit/SSKIncrementingIdFinder.swift new file mode 100644 index 000000000..84e6a33af --- /dev/null +++ b/SignalUtilitiesKit/SSKIncrementingIdFinder.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class SSKIncrementingIdFinder: NSObject { + + @objc + public static let collectionName = "IncrementingIdCollection" + + @objc + public class func previousId(key: String, transaction: YapDatabaseReadTransaction) -> UInt64 { + let previousId: UInt64 = transaction.object(forKey: key, inCollection: collectionName) as? UInt64 ?? 0 + return previousId + } + + @objc + public class func nextId(key: String, transaction: YapDatabaseReadWriteTransaction) -> UInt64 { + let previousId: UInt64 = transaction.object(forKey: key, inCollection: collectionName) as? UInt64 ?? 0 + let nextId: UInt64 = previousId + 1 + + transaction.setObject(nextId, forKey: key, inCollection: collectionName) + return nextId + } +} diff --git a/SignalUtilitiesKit/SSKJobRecord.h b/SignalUtilitiesKit/SSKJobRecord.h new file mode 100644 index 000000000..c14a04bca --- /dev/null +++ b/SignalUtilitiesKit/SSKJobRecord.h @@ -0,0 +1,57 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSErrorDomain const SSKJobRecordErrorDomain; + +typedef NS_ERROR_ENUM(SSKJobRecordErrorDomain, JobRecordError){ + JobRecordError_AssertionError = 100, + JobRecordError_IllegalStateTransition, +}; + +typedef NS_ENUM(NSUInteger, SSKJobRecordStatus) { + SSKJobRecordStatus_Unknown, + SSKJobRecordStatus_Ready, + SSKJobRecordStatus_Running, + SSKJobRecordStatus_PermanentlyFailed, + SSKJobRecordStatus_Obsolete +}; + +#pragma mark - + +@interface SSKJobRecord : TSYapDatabaseObject + +@property (nonatomic) NSUInteger failureCount; +@property (nonatomic) NSString *label; + +- (instancetype)initWithLabel:(NSString *)label NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; + +@property (readonly, nonatomic) SSKJobRecordStatus status; +@property (nonatomic, readonly) UInt64 sortId; + +- (BOOL)saveAsStartedWithTransaction:(YapDatabaseReadWriteTransaction *)transaction + error:(NSError **)outError NS_SWIFT_NAME(saveAsStarted(transaction:)); + +- (void)saveAsPermanentlyFailedWithTransaction:(YapDatabaseReadWriteTransaction *)transaction + NS_SWIFT_NAME(saveAsPermanentlyFailed(transaction:)); + +- (void)saveAsObsoleteWithTransaction:(YapDatabaseReadWriteTransaction *)transaction + NS_SWIFT_NAME(saveAsObsolete(transaction:)); + +- (BOOL)saveRunningAsReadyWithTransaction:(YapDatabaseReadWriteTransaction *)transaction + error:(NSError **)outError NS_SWIFT_NAME(saveRunningAsReady(transaction:)); + +- (BOOL)addFailureWithWithTransaction:(YapDatabaseReadWriteTransaction *)transaction + error:(NSError **)outError NS_SWIFT_NAME(addFailure(transaction:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SSKJobRecord.m b/SignalUtilitiesKit/SSKJobRecord.m new file mode 100644 index 000000000..2285b567f --- /dev/null +++ b/SignalUtilitiesKit/SSKJobRecord.m @@ -0,0 +1,127 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "SSKJobRecord.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSErrorDomain const SSKJobRecordErrorDomain = @"SignalServiceKit.JobRecord"; + +#pragma mark - +@interface SSKJobRecord () + +@property (nonatomic) SSKJobRecordStatus status; +@property (nonatomic) UInt64 sortId; + +@end + +@implementation SSKJobRecord + +- (instancetype)initWithLabel:(NSString *)label +{ + self = [super init]; + if (!self) { + return self; + } + + _status = SSKJobRecordStatus_Ready; + _label = label; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +#pragma mark - TSYapDatabaseObject Overrides + ++ (NSString *)collection +{ + // To avoid a plethora of identical JobRecord subclasses, all job records share + // a common collection and JobQueue's distinguish their behavior by the job's + // `label` + return @"JobRecord"; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (self.sortId == 0) { + self.sortId = [SSKIncrementingIdFinder nextIdWithKey:self.class.collection transaction:transaction]; + } + [super saveWithTransaction:transaction]; +} + +#pragma mark - + +- (BOOL)saveAsStartedWithTransaction:(YapDatabaseReadWriteTransaction *)transaction error:(NSError **)outError +{ + if (self.status != SSKJobRecordStatus_Ready) { + *outError = + [NSError errorWithDomain:SSKJobRecordErrorDomain code:JobRecordError_IllegalStateTransition userInfo:nil]; + return NO; + } + self.status = SSKJobRecordStatus_Running; + [self saveWithTransaction:transaction]; + + return YES; +} + +- (void)saveAsPermanentlyFailedWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + self.status = SSKJobRecordStatus_PermanentlyFailed; + [self saveWithTransaction:transaction]; +} + +- (void)saveAsObsoleteWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + self.status = SSKJobRecordStatus_Obsolete; + [self saveWithTransaction:transaction]; +} + +- (BOOL)saveRunningAsReadyWithTransaction:(YapDatabaseReadWriteTransaction *)transaction error:(NSError **)outError +{ + switch (self.status) { + case SSKJobRecordStatus_Running: { + self.status = SSKJobRecordStatus_Ready; + [self saveWithTransaction:transaction]; + return YES; + } + case SSKJobRecordStatus_Ready: + case SSKJobRecordStatus_PermanentlyFailed: + case SSKJobRecordStatus_Obsolete: + case SSKJobRecordStatus_Unknown: { + *outError = [NSError errorWithDomain:SSKJobRecordErrorDomain + code:JobRecordError_IllegalStateTransition + userInfo:nil]; + return NO; + } + } +} + +- (BOOL)addFailureWithWithTransaction:(YapDatabaseReadWriteTransaction *)transaction error:(NSError **)outError +{ + switch (self.status) { + case SSKJobRecordStatus_Running: { + self.failureCount++; + [self saveWithTransaction:transaction]; + return YES; + } + case SSKJobRecordStatus_Ready: + case SSKJobRecordStatus_PermanentlyFailed: + case SSKJobRecordStatus_Obsolete: + case SSKJobRecordStatus_Unknown: { + *outError = [NSError errorWithDomain:SSKJobRecordErrorDomain + code:JobRecordError_IllegalStateTransition + userInfo:nil]; + return NO; + } + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SSKKeychainStorage.swift b/SignalUtilitiesKit/SSKKeychainStorage.swift new file mode 100644 index 000000000..168609bed --- /dev/null +++ b/SignalUtilitiesKit/SSKKeychainStorage.swift @@ -0,0 +1,108 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SAMKeychain + +public enum KeychainStorageError: Error { + case failure(description: String) +} + +// MARK: - + +@objc public protocol SSKKeychainStorage: class { + + @objc func string(forService service: String, key: String) throws -> String + + @objc(setString:service:key:error:) func set(string: String, service: String, key: String) throws + + @objc func data(forService service: String, key: String) throws -> Data + + @objc func set(data: Data, service: String, key: String) throws + + @objc func remove(service: String, key: String) throws +} + +// MARK: - + +@objc +public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage { + + @objc public static let shared = SSKDefaultKeychainStorage() + + // Force usage as a singleton + override private init() { + super.init() + + SwiftSingletons.register(self) + } + + @objc public func string(forService service: String, key: String) throws -> String { + var error: NSError? + let result = SAMKeychain.password(forService: service, account: key, error: &error) + if let error = error { + throw KeychainStorageError.failure(description: "\(logTag) error retrieving string: \(error)") + } + guard let string = result else { + throw KeychainStorageError.failure(description: "\(logTag) could not retrieve string") + } + return string + } + + @objc public func set(string: String, service: String, key: String) throws { + + SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + + var error: NSError? + let result = SAMKeychain.setPassword(string, forService: service, account: key, error: &error) + if let error = error { + throw KeychainStorageError.failure(description: "\(logTag) error setting string: \(error)") + } + guard result else { + throw KeychainStorageError.failure(description: "\(logTag) could not set string") + } + } + + @objc public func data(forService service: String, key: String) throws -> Data { + var error: NSError? + let result = SAMKeychain.passwordData(forService: service, account: key, error: &error) + if let error = error { + throw KeychainStorageError.failure(description: "\(logTag) error retrieving data: \(error)") + } + guard let data = result else { + throw KeychainStorageError.failure(description: "\(logTag) could not retrieve data") + } + return data + } + + @objc public func set(data: Data, service: String, key: String) throws { + + SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + + var error: NSError? + let result = SAMKeychain.setPasswordData(data, forService: service, account: key, error: &error) + if let error = error { + throw KeychainStorageError.failure(description: "\(logTag) error setting data: \(error)") + } + guard result else { + throw KeychainStorageError.failure(description: "\(logTag) could not set data") + } + } + + @objc public func remove(service: String, key: String) throws { + var error: NSError? + let result = SAMKeychain.deletePassword(forService: service, account: key, error: &error) + if let error = error { + // If deletion failed because the specified item could not be found in the keychain, consider it success. + if error.code == errSecItemNotFound { + Logger.info("Keychain delete failed; item not found.") + return + } + throw KeychainStorageError.failure(description: "\(logTag) error removing data: \(error)") + } + guard result else { + throw KeychainStorageError.failure(description: "\(logTag) could not remove data") + } + } +} diff --git a/SignalUtilitiesKit/SSKMessageSenderJobRecord.h b/SignalUtilitiesKit/SSKMessageSenderJobRecord.h new file mode 100644 index 000000000..039bbd9f8 --- /dev/null +++ b/SignalUtilitiesKit/SSKMessageSenderJobRecord.h @@ -0,0 +1,29 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "SSKJobRecord.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSOutgoingMessage; + +@interface SSKMessageSenderJobRecord : SSKJobRecord + +@property (nonatomic, readonly, nullable) NSString *messageId; +@property (nonatomic, readonly, nullable) NSString *threadId; +@property (nonatomic, readonly, nullable) TSOutgoingMessage *invisibleMessage; +@property (nonatomic, readonly) BOOL removeMessageAfterSending; + +- (nullable instancetype)initWithMessage:(TSOutgoingMessage *)message + removeMessageAfterSending:(BOOL)removeMessageAfterSending + label:(NSString *)label + error:(NSError **)outError NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithLabel:(nullable NSString *)label NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SSKMessageSenderJobRecord.m b/SignalUtilitiesKit/SSKMessageSenderJobRecord.m new file mode 100644 index 000000000..d7d92a480 --- /dev/null +++ b/SignalUtilitiesKit/SSKMessageSenderJobRecord.m @@ -0,0 +1,51 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "SSKMessageSenderJobRecord.h" +#import "TSOutgoingMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SSKMessageSenderJobRecord + +#pragma mark + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (nullable instancetype)initWithMessage:(TSOutgoingMessage *)message + removeMessageAfterSending:(BOOL)removeMessageAfterSending + label:(NSString *)label + error:(NSError **)outError; +{ + self = [super initWithLabel:label]; + if (!self) { + return self; + } + + if (message.shouldBeSaved) { + _messageId = message.uniqueId; + if (_messageId == nil) { + *outError = [NSError errorWithDomain:SSKJobRecordErrorDomain + code:JobRecordError_AssertionError + userInfo:@{ NSDebugDescriptionErrorKey : @"messageId wasn't set" }]; + return nil; + } + _invisibleMessage = nil; + } else { + _messageId = nil; + _invisibleMessage = message; + } + + _removeMessageAfterSending = removeMessageAfterSending; + _threadId = message.uniqueThreadId; + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SSKPreferences.swift b/SignalUtilitiesKit/SSKPreferences.swift new file mode 100644 index 000000000..655903fd2 --- /dev/null +++ b/SignalUtilitiesKit/SSKPreferences.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class SSKPreferences: NSObject { + // Never instantiate this class. + private override init() {} + + private static let collection = "SSKPreferences" + + // MARK: - + + private static let areLinkPreviewsEnabledKey = "areLinkPreviewsEnabled" + + @objc + public static var areLinkPreviewsEnabled: Bool { + get { + return getBool(key: areLinkPreviewsEnabledKey, defaultValue: false) + } + set { + setBool(newValue, key: areLinkPreviewsEnabledKey) + + SSKEnvironment.shared.syncManager.sendConfigurationSyncMessage() + } + } + + // MARK: - + + private static let hasSavedThreadKey = "hasSavedThread" + + @objc + public static var hasSavedThread: Bool { + get { + return getBool(key: hasSavedThreadKey) + } + set { + setBool(newValue, key: hasSavedThreadKey) + } + } + + @objc + public class func setHasSavedThread(value: Bool, transaction: YapDatabaseReadWriteTransaction) { + transaction.setBool(value, + forKey: hasSavedThreadKey, + inCollection: collection) + } + + // MARK: - + + private class func getBool(key: String, defaultValue: Bool = false) -> Bool { + return OWSPrimaryStorage.dbReadConnection().bool(forKey: key, inCollection: collection, defaultValue: defaultValue) + } + + private class func setBool(_ value: Bool, key: String) { + OWSPrimaryStorage.dbReadWriteConnection().setBool(value, forKey: key, inCollection: collection) + } +} diff --git a/SignalUtilitiesKit/SSKProto.swift b/SignalUtilitiesKit/SSKProto.swift new file mode 100644 index 000000000..6b7e07e7f --- /dev/null +++ b/SignalUtilitiesKit/SSKProto.swift @@ -0,0 +1,7075 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +// WARNING: This code is generated. Only edit within the markers. + +public enum SSKProtoError: Error { + case invalidProtobuf(description: String) +} + +// MARK: - SSKProtoEnvelope + +@objc public class SSKProtoEnvelope: NSObject { + + // MARK: - SSKProtoEnvelopeType + + @objc public enum SSKProtoEnvelopeType: Int32 { + case unknown = 0 + case ciphertext = 1 + case keyExchange = 2 + case prekeyBundle = 3 + case receipt = 5 + case unidentifiedSender = 6 + case closedGroupCiphertext = 7 + case fallbackMessage = 101 + } + + private class func SSKProtoEnvelopeTypeWrap(_ value: SignalServiceProtos_Envelope.TypeEnum) -> SSKProtoEnvelopeType { + switch value { + case .unknown: return .unknown + case .ciphertext: return .ciphertext + case .keyExchange: return .keyExchange + case .prekeyBundle: return .prekeyBundle + case .receipt: return .receipt + case .unidentifiedSender: return .unidentifiedSender + case .closedGroupCiphertext: return .closedGroupCiphertext + case .fallbackMessage: return .fallbackMessage + } + } + + private class func SSKProtoEnvelopeTypeUnwrap(_ value: SSKProtoEnvelopeType) -> SignalServiceProtos_Envelope.TypeEnum { + switch value { + case .unknown: return .unknown + case .ciphertext: return .ciphertext + case .keyExchange: return .keyExchange + case .prekeyBundle: return .prekeyBundle + case .receipt: return .receipt + case .unidentifiedSender: return .unidentifiedSender + case .closedGroupCiphertext: return .closedGroupCiphertext + case .fallbackMessage: return .fallbackMessage + } + } + + // MARK: - SSKProtoEnvelopeBuilder + + @objc public class func builder(type: SSKProtoEnvelopeType, timestamp: UInt64) -> SSKProtoEnvelopeBuilder { + return SSKProtoEnvelopeBuilder(type: type, timestamp: timestamp) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoEnvelopeBuilder { + let builder = SSKProtoEnvelopeBuilder(type: type, timestamp: timestamp) + if let _value = source { + builder.setSource(_value) + } + if hasSourceDevice { + builder.setSourceDevice(sourceDevice) + } + if let _value = relay { + builder.setRelay(_value) + } + if let _value = legacyMessage { + builder.setLegacyMessage(_value) + } + if let _value = content { + builder.setContent(_value) + } + if let _value = serverGuid { + builder.setServerGuid(_value) + } + if hasServerTimestamp { + builder.setServerTimestamp(serverTimestamp) + } + return builder + } + + @objc public class SSKProtoEnvelopeBuilder: NSObject { + + private var proto = SignalServiceProtos_Envelope() + + @objc fileprivate override init() {} + + @objc fileprivate init(type: SSKProtoEnvelopeType, timestamp: UInt64) { + super.init() + + setType(type) + setTimestamp(timestamp) + } + + @objc public func setType(_ valueParam: SSKProtoEnvelopeType) { + proto.type = SSKProtoEnvelopeTypeUnwrap(valueParam) + } + + @objc public func setSource(_ valueParam: String) { + proto.source = valueParam + } + + @objc public func setSourceDevice(_ valueParam: UInt32) { + proto.sourceDevice = valueParam + } + + @objc public func setRelay(_ valueParam: String) { + proto.relay = valueParam + } + + @objc public func setTimestamp(_ valueParam: UInt64) { + proto.timestamp = valueParam + } + + @objc public func setLegacyMessage(_ valueParam: Data) { + proto.legacyMessage = valueParam + } + + @objc public func setContent(_ valueParam: Data) { + proto.content = valueParam + } + + @objc public func setServerGuid(_ valueParam: String) { + proto.serverGuid = valueParam + } + + @objc public func setServerTimestamp(_ valueParam: UInt64) { + proto.serverTimestamp = valueParam + } + + @objc public func build() throws -> SSKProtoEnvelope { + return try SSKProtoEnvelope.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoEnvelope.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_Envelope + + @objc public let type: SSKProtoEnvelopeType + + @objc public let timestamp: UInt64 + + @objc public var source: String? { + guard proto.hasSource else { + return nil + } + return proto.source + } + @objc public var hasSource: Bool { + return proto.hasSource + } + + @objc public var sourceDevice: UInt32 { + return proto.sourceDevice + } + @objc public var hasSourceDevice: Bool { + return proto.hasSourceDevice + } + + @objc public var relay: String? { + guard proto.hasRelay else { + return nil + } + return proto.relay + } + @objc public var hasRelay: Bool { + return proto.hasRelay + } + + @objc public var legacyMessage: Data? { + guard proto.hasLegacyMessage else { + return nil + } + return proto.legacyMessage + } + @objc public var hasLegacyMessage: Bool { + return proto.hasLegacyMessage + } + + @objc public var content: Data? { + guard proto.hasContent else { + return nil + } + return proto.content + } + @objc public var hasContent: Bool { + return proto.hasContent + } + + @objc public var serverGuid: String? { + guard proto.hasServerGuid else { + return nil + } + return proto.serverGuid + } + @objc public var hasServerGuid: Bool { + return proto.hasServerGuid + } + + @objc public var serverTimestamp: UInt64 { + return proto.serverTimestamp + } + @objc public var hasServerTimestamp: Bool { + return proto.hasServerTimestamp + } + + private init(proto: SignalServiceProtos_Envelope, + type: SSKProtoEnvelopeType, + timestamp: UInt64) { + self.proto = proto + self.type = type + self.timestamp = timestamp + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoEnvelope { + let proto = try SignalServiceProtos_Envelope(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_Envelope) throws -> SSKProtoEnvelope { + guard proto.hasType else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: type") + } + let type = SSKProtoEnvelopeTypeWrap(proto.type) + + guard proto.hasTimestamp else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: timestamp") + } + let timestamp = proto.timestamp + + // MARK: - Begin Validation Logic for SSKProtoEnvelope - + + // MARK: - End Validation Logic for SSKProtoEnvelope - + + let result = SSKProtoEnvelope(proto: proto, + type: type, + timestamp: timestamp) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoEnvelope { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoEnvelope.SSKProtoEnvelopeBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoEnvelope? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoTypingMessage + +@objc public class SSKProtoTypingMessage: NSObject { + + // MARK: - SSKProtoTypingMessageAction + + @objc public enum SSKProtoTypingMessageAction: Int32 { + case started = 0 + case stopped = 1 + } + + private class func SSKProtoTypingMessageActionWrap(_ value: SignalServiceProtos_TypingMessage.Action) -> SSKProtoTypingMessageAction { + switch value { + case .started: return .started + case .stopped: return .stopped + } + } + + private class func SSKProtoTypingMessageActionUnwrap(_ value: SSKProtoTypingMessageAction) -> SignalServiceProtos_TypingMessage.Action { + switch value { + case .started: return .started + case .stopped: return .stopped + } + } + + // MARK: - SSKProtoTypingMessageBuilder + + @objc public class func builder(timestamp: UInt64, action: SSKProtoTypingMessageAction) -> SSKProtoTypingMessageBuilder { + return SSKProtoTypingMessageBuilder(timestamp: timestamp, action: action) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoTypingMessageBuilder { + let builder = SSKProtoTypingMessageBuilder(timestamp: timestamp, action: action) + if let _value = groupID { + builder.setGroupID(_value) + } + return builder + } + + @objc public class SSKProtoTypingMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_TypingMessage() + + @objc fileprivate override init() {} + + @objc fileprivate init(timestamp: UInt64, action: SSKProtoTypingMessageAction) { + super.init() + + setTimestamp(timestamp) + setAction(action) + } + + @objc public func setTimestamp(_ valueParam: UInt64) { + proto.timestamp = valueParam + } + + @objc public func setAction(_ valueParam: SSKProtoTypingMessageAction) { + proto.action = SSKProtoTypingMessageActionUnwrap(valueParam) + } + + @objc public func setGroupID(_ valueParam: Data) { + proto.groupID = valueParam + } + + @objc public func build() throws -> SSKProtoTypingMessage { + return try SSKProtoTypingMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoTypingMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_TypingMessage + + @objc public let timestamp: UInt64 + + @objc public let action: SSKProtoTypingMessageAction + + @objc public var groupID: Data? { + guard proto.hasGroupID else { + return nil + } + return proto.groupID + } + @objc public var hasGroupID: Bool { + return proto.hasGroupID + } + + private init(proto: SignalServiceProtos_TypingMessage, + timestamp: UInt64, + action: SSKProtoTypingMessageAction) { + self.proto = proto + self.timestamp = timestamp + self.action = action + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoTypingMessage { + let proto = try SignalServiceProtos_TypingMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_TypingMessage) throws -> SSKProtoTypingMessage { + guard proto.hasTimestamp else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: timestamp") + } + let timestamp = proto.timestamp + + guard proto.hasAction else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: action") + } + let action = SSKProtoTypingMessageActionWrap(proto.action) + + // MARK: - Begin Validation Logic for SSKProtoTypingMessage - + + // MARK: - End Validation Logic for SSKProtoTypingMessage - + + let result = SSKProtoTypingMessage(proto: proto, + timestamp: timestamp, + action: action) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoTypingMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoTypingMessage.SSKProtoTypingMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoTypingMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoContent + +@objc public class SSKProtoContent: NSObject { + + // MARK: - SSKProtoContentBuilder + + @objc public class func builder() -> SSKProtoContentBuilder { + return SSKProtoContentBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoContentBuilder { + let builder = SSKProtoContentBuilder() + if let _value = dataMessage { + builder.setDataMessage(_value) + } + if let _value = syncMessage { + builder.setSyncMessage(_value) + } + if let _value = callMessage { + builder.setCallMessage(_value) + } + if let _value = nullMessage { + builder.setNullMessage(_value) + } + if let _value = receiptMessage { + builder.setReceiptMessage(_value) + } + if let _value = typingMessage { + builder.setTypingMessage(_value) + } + if let _value = prekeyBundleMessage { + builder.setPrekeyBundleMessage(_value) + } + if let _value = lokiDeviceLinkMessage { + builder.setLokiDeviceLinkMessage(_value) + } + return builder + } + + @objc public class SSKProtoContentBuilder: NSObject { + + private var proto = SignalServiceProtos_Content() + + @objc fileprivate override init() {} + + @objc public func setDataMessage(_ valueParam: SSKProtoDataMessage) { + proto.dataMessage = valueParam.proto + } + + @objc public func setSyncMessage(_ valueParam: SSKProtoSyncMessage) { + proto.syncMessage = valueParam.proto + } + + @objc public func setCallMessage(_ valueParam: SSKProtoCallMessage) { + proto.callMessage = valueParam.proto + } + + @objc public func setNullMessage(_ valueParam: SSKProtoNullMessage) { + proto.nullMessage = valueParam.proto + } + + @objc public func setReceiptMessage(_ valueParam: SSKProtoReceiptMessage) { + proto.receiptMessage = valueParam.proto + } + + @objc public func setTypingMessage(_ valueParam: SSKProtoTypingMessage) { + proto.typingMessage = valueParam.proto + } + + @objc public func setPrekeyBundleMessage(_ valueParam: SSKProtoPrekeyBundleMessage) { + proto.prekeyBundleMessage = valueParam.proto + } + + @objc public func setLokiDeviceLinkMessage(_ valueParam: SSKProtoLokiDeviceLinkMessage) { + proto.lokiDeviceLinkMessage = valueParam.proto + } + + @objc public func build() throws -> SSKProtoContent { + return try SSKProtoContent.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoContent.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_Content + + @objc public let dataMessage: SSKProtoDataMessage? + + @objc public let syncMessage: SSKProtoSyncMessage? + + @objc public let callMessage: SSKProtoCallMessage? + + @objc public let nullMessage: SSKProtoNullMessage? + + @objc public let receiptMessage: SSKProtoReceiptMessage? + + @objc public let typingMessage: SSKProtoTypingMessage? + + @objc public let prekeyBundleMessage: SSKProtoPrekeyBundleMessage? + + @objc public let lokiDeviceLinkMessage: SSKProtoLokiDeviceLinkMessage? + + private init(proto: SignalServiceProtos_Content, + dataMessage: SSKProtoDataMessage?, + syncMessage: SSKProtoSyncMessage?, + callMessage: SSKProtoCallMessage?, + nullMessage: SSKProtoNullMessage?, + receiptMessage: SSKProtoReceiptMessage?, + typingMessage: SSKProtoTypingMessage?, + prekeyBundleMessage: SSKProtoPrekeyBundleMessage?, + lokiDeviceLinkMessage: SSKProtoLokiDeviceLinkMessage?) { + self.proto = proto + self.dataMessage = dataMessage + self.syncMessage = syncMessage + self.callMessage = callMessage + self.nullMessage = nullMessage + self.receiptMessage = receiptMessage + self.typingMessage = typingMessage + self.prekeyBundleMessage = prekeyBundleMessage + self.lokiDeviceLinkMessage = lokiDeviceLinkMessage + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoContent { + let proto = try SignalServiceProtos_Content(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_Content) throws -> SSKProtoContent { + var dataMessage: SSKProtoDataMessage? = nil + if proto.hasDataMessage { + dataMessage = try SSKProtoDataMessage.parseProto(proto.dataMessage) + } + + var syncMessage: SSKProtoSyncMessage? = nil + if proto.hasSyncMessage { + syncMessage = try SSKProtoSyncMessage.parseProto(proto.syncMessage) + } + + var callMessage: SSKProtoCallMessage? = nil + if proto.hasCallMessage { + callMessage = try SSKProtoCallMessage.parseProto(proto.callMessage) + } + + var nullMessage: SSKProtoNullMessage? = nil + if proto.hasNullMessage { + nullMessage = try SSKProtoNullMessage.parseProto(proto.nullMessage) + } + + var receiptMessage: SSKProtoReceiptMessage? = nil + if proto.hasReceiptMessage { + receiptMessage = try SSKProtoReceiptMessage.parseProto(proto.receiptMessage) + } + + var typingMessage: SSKProtoTypingMessage? = nil + if proto.hasTypingMessage { + typingMessage = try SSKProtoTypingMessage.parseProto(proto.typingMessage) + } + + var prekeyBundleMessage: SSKProtoPrekeyBundleMessage? = nil + if proto.hasPrekeyBundleMessage { + prekeyBundleMessage = try SSKProtoPrekeyBundleMessage.parseProto(proto.prekeyBundleMessage) + } + + var lokiDeviceLinkMessage: SSKProtoLokiDeviceLinkMessage? = nil + if proto.hasLokiDeviceLinkMessage { + lokiDeviceLinkMessage = try SSKProtoLokiDeviceLinkMessage.parseProto(proto.lokiDeviceLinkMessage) + } + + // MARK: - Begin Validation Logic for SSKProtoContent - + + // MARK: - End Validation Logic for SSKProtoContent - + + let result = SSKProtoContent(proto: proto, + dataMessage: dataMessage, + syncMessage: syncMessage, + callMessage: callMessage, + nullMessage: nullMessage, + receiptMessage: receiptMessage, + typingMessage: typingMessage, + prekeyBundleMessage: prekeyBundleMessage, + lokiDeviceLinkMessage: lokiDeviceLinkMessage) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoContent { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoContent.SSKProtoContentBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoContent? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoPrekeyBundleMessage + +@objc public class SSKProtoPrekeyBundleMessage: NSObject { + + // MARK: - SSKProtoPrekeyBundleMessageBuilder + + @objc public class func builder() -> SSKProtoPrekeyBundleMessageBuilder { + return SSKProtoPrekeyBundleMessageBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoPrekeyBundleMessageBuilder { + let builder = SSKProtoPrekeyBundleMessageBuilder() + if let _value = identityKey { + builder.setIdentityKey(_value) + } + if hasDeviceID { + builder.setDeviceID(deviceID) + } + if hasPrekeyID { + builder.setPrekeyID(prekeyID) + } + if hasSignedKeyID { + builder.setSignedKeyID(signedKeyID) + } + if let _value = prekey { + builder.setPrekey(_value) + } + if let _value = signedKey { + builder.setSignedKey(_value) + } + if let _value = signature { + builder.setSignature(_value) + } + return builder + } + + @objc public class SSKProtoPrekeyBundleMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_PrekeyBundleMessage() + + @objc fileprivate override init() {} + + @objc public func setIdentityKey(_ valueParam: Data) { + proto.identityKey = valueParam + } + + @objc public func setDeviceID(_ valueParam: UInt32) { + proto.deviceID = valueParam + } + + @objc public func setPrekeyID(_ valueParam: UInt32) { + proto.prekeyID = valueParam + } + + @objc public func setSignedKeyID(_ valueParam: UInt32) { + proto.signedKeyID = valueParam + } + + @objc public func setPrekey(_ valueParam: Data) { + proto.prekey = valueParam + } + + @objc public func setSignedKey(_ valueParam: Data) { + proto.signedKey = valueParam + } + + @objc public func setSignature(_ valueParam: Data) { + proto.signature = valueParam + } + + @objc public func build() throws -> SSKProtoPrekeyBundleMessage { + return try SSKProtoPrekeyBundleMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoPrekeyBundleMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_PrekeyBundleMessage + + @objc public var identityKey: Data? { + guard proto.hasIdentityKey else { + return nil + } + return proto.identityKey + } + @objc public var hasIdentityKey: Bool { + return proto.hasIdentityKey + } + + @objc public var deviceID: UInt32 { + return proto.deviceID + } + @objc public var hasDeviceID: Bool { + return proto.hasDeviceID + } + + @objc public var prekeyID: UInt32 { + return proto.prekeyID + } + @objc public var hasPrekeyID: Bool { + return proto.hasPrekeyID + } + + @objc public var signedKeyID: UInt32 { + return proto.signedKeyID + } + @objc public var hasSignedKeyID: Bool { + return proto.hasSignedKeyID + } + + @objc public var prekey: Data? { + guard proto.hasPrekey else { + return nil + } + return proto.prekey + } + @objc public var hasPrekey: Bool { + return proto.hasPrekey + } + + @objc public var signedKey: Data? { + guard proto.hasSignedKey else { + return nil + } + return proto.signedKey + } + @objc public var hasSignedKey: Bool { + return proto.hasSignedKey + } + + @objc public var signature: Data? { + guard proto.hasSignature else { + return nil + } + return proto.signature + } + @objc public var hasSignature: Bool { + return proto.hasSignature + } + + private init(proto: SignalServiceProtos_PrekeyBundleMessage) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoPrekeyBundleMessage { + let proto = try SignalServiceProtos_PrekeyBundleMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_PrekeyBundleMessage) throws -> SSKProtoPrekeyBundleMessage { + // MARK: - Begin Validation Logic for SSKProtoPrekeyBundleMessage - + + // MARK: - End Validation Logic for SSKProtoPrekeyBundleMessage - + + let result = SSKProtoPrekeyBundleMessage(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoPrekeyBundleMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoPrekeyBundleMessage.SSKProtoPrekeyBundleMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoPrekeyBundleMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoLokiDeviceLinkMessage + +@objc public class SSKProtoLokiDeviceLinkMessage: NSObject { + + // MARK: - SSKProtoLokiDeviceLinkMessageBuilder + + @objc public class func builder() -> SSKProtoLokiDeviceLinkMessageBuilder { + return SSKProtoLokiDeviceLinkMessageBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoLokiDeviceLinkMessageBuilder { + let builder = SSKProtoLokiDeviceLinkMessageBuilder() + if let _value = masterPublicKey { + builder.setMasterPublicKey(_value) + } + if let _value = slavePublicKey { + builder.setSlavePublicKey(_value) + } + if let _value = slaveSignature { + builder.setSlaveSignature(_value) + } + if let _value = masterSignature { + builder.setMasterSignature(_value) + } + return builder + } + + @objc public class SSKProtoLokiDeviceLinkMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_LokiDeviceLinkMessage() + + @objc fileprivate override init() {} + + @objc public func setMasterPublicKey(_ valueParam: String) { + proto.masterPublicKey = valueParam + } + + @objc public func setSlavePublicKey(_ valueParam: String) { + proto.slavePublicKey = valueParam + } + + @objc public func setSlaveSignature(_ valueParam: Data) { + proto.slaveSignature = valueParam + } + + @objc public func setMasterSignature(_ valueParam: Data) { + proto.masterSignature = valueParam + } + + @objc public func build() throws -> SSKProtoLokiDeviceLinkMessage { + return try SSKProtoLokiDeviceLinkMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoLokiDeviceLinkMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_LokiDeviceLinkMessage + + @objc public var masterPublicKey: String? { + guard proto.hasMasterPublicKey else { + return nil + } + return proto.masterPublicKey + } + @objc public var hasMasterPublicKey: Bool { + return proto.hasMasterPublicKey + } + + @objc public var slavePublicKey: String? { + guard proto.hasSlavePublicKey else { + return nil + } + return proto.slavePublicKey + } + @objc public var hasSlavePublicKey: Bool { + return proto.hasSlavePublicKey + } + + @objc public var slaveSignature: Data? { + guard proto.hasSlaveSignature else { + return nil + } + return proto.slaveSignature + } + @objc public var hasSlaveSignature: Bool { + return proto.hasSlaveSignature + } + + @objc public var masterSignature: Data? { + guard proto.hasMasterSignature else { + return nil + } + return proto.masterSignature + } + @objc public var hasMasterSignature: Bool { + return proto.hasMasterSignature + } + + private init(proto: SignalServiceProtos_LokiDeviceLinkMessage) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoLokiDeviceLinkMessage { + let proto = try SignalServiceProtos_LokiDeviceLinkMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_LokiDeviceLinkMessage) throws -> SSKProtoLokiDeviceLinkMessage { + // MARK: - Begin Validation Logic for SSKProtoLokiDeviceLinkMessage - + + // MARK: - End Validation Logic for SSKProtoLokiDeviceLinkMessage - + + let result = SSKProtoLokiDeviceLinkMessage(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoLokiDeviceLinkMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoLokiDeviceLinkMessage.SSKProtoLokiDeviceLinkMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoLokiDeviceLinkMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoCallMessageOffer + +@objc public class SSKProtoCallMessageOffer: NSObject { + + // MARK: - SSKProtoCallMessageOfferBuilder + + @objc public class func builder(id: UInt64, sessionDescription: String) -> SSKProtoCallMessageOfferBuilder { + return SSKProtoCallMessageOfferBuilder(id: id, sessionDescription: sessionDescription) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoCallMessageOfferBuilder { + let builder = SSKProtoCallMessageOfferBuilder(id: id, sessionDescription: sessionDescription) + return builder + } + + @objc public class SSKProtoCallMessageOfferBuilder: NSObject { + + private var proto = SignalServiceProtos_CallMessage.Offer() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64, sessionDescription: String) { + super.init() + + setId(id) + setSessionDescription(sessionDescription) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func setSessionDescription(_ valueParam: String) { + proto.sessionDescription = valueParam + } + + @objc public func build() throws -> SSKProtoCallMessageOffer { + return try SSKProtoCallMessageOffer.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoCallMessageOffer.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_CallMessage.Offer + + @objc public let id: UInt64 + + @objc public let sessionDescription: String + + private init(proto: SignalServiceProtos_CallMessage.Offer, + id: UInt64, + sessionDescription: String) { + self.proto = proto + self.id = id + self.sessionDescription = sessionDescription + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoCallMessageOffer { + let proto = try SignalServiceProtos_CallMessage.Offer(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_CallMessage.Offer) throws -> SSKProtoCallMessageOffer { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + guard proto.hasSessionDescription else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sessionDescription") + } + let sessionDescription = proto.sessionDescription + + // MARK: - Begin Validation Logic for SSKProtoCallMessageOffer - + + // MARK: - End Validation Logic for SSKProtoCallMessageOffer - + + let result = SSKProtoCallMessageOffer(proto: proto, + id: id, + sessionDescription: sessionDescription) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoCallMessageOffer { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoCallMessageOffer.SSKProtoCallMessageOfferBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoCallMessageOffer? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoCallMessageAnswer + +@objc public class SSKProtoCallMessageAnswer: NSObject { + + // MARK: - SSKProtoCallMessageAnswerBuilder + + @objc public class func builder(id: UInt64, sessionDescription: String) -> SSKProtoCallMessageAnswerBuilder { + return SSKProtoCallMessageAnswerBuilder(id: id, sessionDescription: sessionDescription) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoCallMessageAnswerBuilder { + let builder = SSKProtoCallMessageAnswerBuilder(id: id, sessionDescription: sessionDescription) + return builder + } + + @objc public class SSKProtoCallMessageAnswerBuilder: NSObject { + + private var proto = SignalServiceProtos_CallMessage.Answer() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64, sessionDescription: String) { + super.init() + + setId(id) + setSessionDescription(sessionDescription) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func setSessionDescription(_ valueParam: String) { + proto.sessionDescription = valueParam + } + + @objc public func build() throws -> SSKProtoCallMessageAnswer { + return try SSKProtoCallMessageAnswer.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoCallMessageAnswer.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_CallMessage.Answer + + @objc public let id: UInt64 + + @objc public let sessionDescription: String + + private init(proto: SignalServiceProtos_CallMessage.Answer, + id: UInt64, + sessionDescription: String) { + self.proto = proto + self.id = id + self.sessionDescription = sessionDescription + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoCallMessageAnswer { + let proto = try SignalServiceProtos_CallMessage.Answer(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_CallMessage.Answer) throws -> SSKProtoCallMessageAnswer { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + guard proto.hasSessionDescription else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sessionDescription") + } + let sessionDescription = proto.sessionDescription + + // MARK: - Begin Validation Logic for SSKProtoCallMessageAnswer - + + // MARK: - End Validation Logic for SSKProtoCallMessageAnswer - + + let result = SSKProtoCallMessageAnswer(proto: proto, + id: id, + sessionDescription: sessionDescription) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoCallMessageAnswer { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoCallMessageAnswer.SSKProtoCallMessageAnswerBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoCallMessageAnswer? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoCallMessageIceUpdate + +@objc public class SSKProtoCallMessageIceUpdate: NSObject { + + // MARK: - SSKProtoCallMessageIceUpdateBuilder + + @objc public class func builder(id: UInt64, sdpMid: String, sdpMlineIndex: UInt32, sdp: String) -> SSKProtoCallMessageIceUpdateBuilder { + return SSKProtoCallMessageIceUpdateBuilder(id: id, sdpMid: sdpMid, sdpMlineIndex: sdpMlineIndex, sdp: sdp) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoCallMessageIceUpdateBuilder { + let builder = SSKProtoCallMessageIceUpdateBuilder(id: id, sdpMid: sdpMid, sdpMlineIndex: sdpMlineIndex, sdp: sdp) + return builder + } + + @objc public class SSKProtoCallMessageIceUpdateBuilder: NSObject { + + private var proto = SignalServiceProtos_CallMessage.IceUpdate() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64, sdpMid: String, sdpMlineIndex: UInt32, sdp: String) { + super.init() + + setId(id) + setSdpMid(sdpMid) + setSdpMlineIndex(sdpMlineIndex) + setSdp(sdp) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func setSdpMid(_ valueParam: String) { + proto.sdpMid = valueParam + } + + @objc public func setSdpMlineIndex(_ valueParam: UInt32) { + proto.sdpMlineIndex = valueParam + } + + @objc public func setSdp(_ valueParam: String) { + proto.sdp = valueParam + } + + @objc public func build() throws -> SSKProtoCallMessageIceUpdate { + return try SSKProtoCallMessageIceUpdate.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoCallMessageIceUpdate.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_CallMessage.IceUpdate + + @objc public let id: UInt64 + + @objc public let sdpMid: String + + @objc public let sdpMlineIndex: UInt32 + + @objc public let sdp: String + + private init(proto: SignalServiceProtos_CallMessage.IceUpdate, + id: UInt64, + sdpMid: String, + sdpMlineIndex: UInt32, + sdp: String) { + self.proto = proto + self.id = id + self.sdpMid = sdpMid + self.sdpMlineIndex = sdpMlineIndex + self.sdp = sdp + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoCallMessageIceUpdate { + let proto = try SignalServiceProtos_CallMessage.IceUpdate(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_CallMessage.IceUpdate) throws -> SSKProtoCallMessageIceUpdate { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + guard proto.hasSdpMid else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sdpMid") + } + let sdpMid = proto.sdpMid + + guard proto.hasSdpMlineIndex else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sdpMlineIndex") + } + let sdpMlineIndex = proto.sdpMlineIndex + + guard proto.hasSdp else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sdp") + } + let sdp = proto.sdp + + // MARK: - Begin Validation Logic for SSKProtoCallMessageIceUpdate - + + // MARK: - End Validation Logic for SSKProtoCallMessageIceUpdate - + + let result = SSKProtoCallMessageIceUpdate(proto: proto, + id: id, + sdpMid: sdpMid, + sdpMlineIndex: sdpMlineIndex, + sdp: sdp) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoCallMessageIceUpdate { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoCallMessageIceUpdate.SSKProtoCallMessageIceUpdateBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoCallMessageIceUpdate? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoCallMessageBusy + +@objc public class SSKProtoCallMessageBusy: NSObject { + + // MARK: - SSKProtoCallMessageBusyBuilder + + @objc public class func builder(id: UInt64) -> SSKProtoCallMessageBusyBuilder { + return SSKProtoCallMessageBusyBuilder(id: id) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoCallMessageBusyBuilder { + let builder = SSKProtoCallMessageBusyBuilder(id: id) + return builder + } + + @objc public class SSKProtoCallMessageBusyBuilder: NSObject { + + private var proto = SignalServiceProtos_CallMessage.Busy() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64) { + super.init() + + setId(id) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func build() throws -> SSKProtoCallMessageBusy { + return try SSKProtoCallMessageBusy.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoCallMessageBusy.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_CallMessage.Busy + + @objc public let id: UInt64 + + private init(proto: SignalServiceProtos_CallMessage.Busy, + id: UInt64) { + self.proto = proto + self.id = id + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoCallMessageBusy { + let proto = try SignalServiceProtos_CallMessage.Busy(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_CallMessage.Busy) throws -> SSKProtoCallMessageBusy { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + // MARK: - Begin Validation Logic for SSKProtoCallMessageBusy - + + // MARK: - End Validation Logic for SSKProtoCallMessageBusy - + + let result = SSKProtoCallMessageBusy(proto: proto, + id: id) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoCallMessageBusy { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoCallMessageBusy.SSKProtoCallMessageBusyBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoCallMessageBusy? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoCallMessageHangup + +@objc public class SSKProtoCallMessageHangup: NSObject { + + // MARK: - SSKProtoCallMessageHangupBuilder + + @objc public class func builder(id: UInt64) -> SSKProtoCallMessageHangupBuilder { + return SSKProtoCallMessageHangupBuilder(id: id) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoCallMessageHangupBuilder { + let builder = SSKProtoCallMessageHangupBuilder(id: id) + return builder + } + + @objc public class SSKProtoCallMessageHangupBuilder: NSObject { + + private var proto = SignalServiceProtos_CallMessage.Hangup() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64) { + super.init() + + setId(id) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func build() throws -> SSKProtoCallMessageHangup { + return try SSKProtoCallMessageHangup.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoCallMessageHangup.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_CallMessage.Hangup + + @objc public let id: UInt64 + + private init(proto: SignalServiceProtos_CallMessage.Hangup, + id: UInt64) { + self.proto = proto + self.id = id + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoCallMessageHangup { + let proto = try SignalServiceProtos_CallMessage.Hangup(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_CallMessage.Hangup) throws -> SSKProtoCallMessageHangup { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + // MARK: - Begin Validation Logic for SSKProtoCallMessageHangup - + + // MARK: - End Validation Logic for SSKProtoCallMessageHangup - + + let result = SSKProtoCallMessageHangup(proto: proto, + id: id) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoCallMessageHangup { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoCallMessageHangup.SSKProtoCallMessageHangupBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoCallMessageHangup? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoCallMessage + +@objc public class SSKProtoCallMessage: NSObject { + + // MARK: - SSKProtoCallMessageBuilder + + @objc public class func builder() -> SSKProtoCallMessageBuilder { + return SSKProtoCallMessageBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoCallMessageBuilder { + let builder = SSKProtoCallMessageBuilder() + if let _value = offer { + builder.setOffer(_value) + } + if let _value = answer { + builder.setAnswer(_value) + } + builder.setIceUpdate(iceUpdate) + if let _value = hangup { + builder.setHangup(_value) + } + if let _value = busy { + builder.setBusy(_value) + } + if let _value = profileKey { + builder.setProfileKey(_value) + } + return builder + } + + @objc public class SSKProtoCallMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_CallMessage() + + @objc fileprivate override init() {} + + @objc public func setOffer(_ valueParam: SSKProtoCallMessageOffer) { + proto.offer = valueParam.proto + } + + @objc public func setAnswer(_ valueParam: SSKProtoCallMessageAnswer) { + proto.answer = valueParam.proto + } + + @objc public func addIceUpdate(_ valueParam: SSKProtoCallMessageIceUpdate) { + var items = proto.iceUpdate + items.append(valueParam.proto) + proto.iceUpdate = items + } + + @objc public func setIceUpdate(_ wrappedItems: [SSKProtoCallMessageIceUpdate]) { + proto.iceUpdate = wrappedItems.map { $0.proto } + } + + @objc public func setHangup(_ valueParam: SSKProtoCallMessageHangup) { + proto.hangup = valueParam.proto + } + + @objc public func setBusy(_ valueParam: SSKProtoCallMessageBusy) { + proto.busy = valueParam.proto + } + + @objc public func setProfileKey(_ valueParam: Data) { + proto.profileKey = valueParam + } + + @objc public func build() throws -> SSKProtoCallMessage { + return try SSKProtoCallMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoCallMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_CallMessage + + @objc public let offer: SSKProtoCallMessageOffer? + + @objc public let answer: SSKProtoCallMessageAnswer? + + @objc public let iceUpdate: [SSKProtoCallMessageIceUpdate] + + @objc public let hangup: SSKProtoCallMessageHangup? + + @objc public let busy: SSKProtoCallMessageBusy? + + @objc public var profileKey: Data? { + guard proto.hasProfileKey else { + return nil + } + return proto.profileKey + } + @objc public var hasProfileKey: Bool { + return proto.hasProfileKey + } + + private init(proto: SignalServiceProtos_CallMessage, + offer: SSKProtoCallMessageOffer?, + answer: SSKProtoCallMessageAnswer?, + iceUpdate: [SSKProtoCallMessageIceUpdate], + hangup: SSKProtoCallMessageHangup?, + busy: SSKProtoCallMessageBusy?) { + self.proto = proto + self.offer = offer + self.answer = answer + self.iceUpdate = iceUpdate + self.hangup = hangup + self.busy = busy + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoCallMessage { + let proto = try SignalServiceProtos_CallMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_CallMessage) throws -> SSKProtoCallMessage { + var offer: SSKProtoCallMessageOffer? = nil + if proto.hasOffer { + offer = try SSKProtoCallMessageOffer.parseProto(proto.offer) + } + + var answer: SSKProtoCallMessageAnswer? = nil + if proto.hasAnswer { + answer = try SSKProtoCallMessageAnswer.parseProto(proto.answer) + } + + var iceUpdate: [SSKProtoCallMessageIceUpdate] = [] + iceUpdate = try proto.iceUpdate.map { try SSKProtoCallMessageIceUpdate.parseProto($0) } + + var hangup: SSKProtoCallMessageHangup? = nil + if proto.hasHangup { + hangup = try SSKProtoCallMessageHangup.parseProto(proto.hangup) + } + + var busy: SSKProtoCallMessageBusy? = nil + if proto.hasBusy { + busy = try SSKProtoCallMessageBusy.parseProto(proto.busy) + } + + // MARK: - Begin Validation Logic for SSKProtoCallMessage - + + // MARK: - End Validation Logic for SSKProtoCallMessage - + + let result = SSKProtoCallMessage(proto: proto, + offer: offer, + answer: answer, + iceUpdate: iceUpdate, + hangup: hangup, + busy: busy) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoCallMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoCallMessage.SSKProtoCallMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoCallMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoClosedGroupCiphertextMessageWrapper + +@objc public class SSKProtoClosedGroupCiphertextMessageWrapper: NSObject { + + // MARK: - SSKProtoClosedGroupCiphertextMessageWrapperBuilder + + @objc public class func builder(ciphertext: Data, ephemeralPublicKey: Data) -> SSKProtoClosedGroupCiphertextMessageWrapperBuilder { + return SSKProtoClosedGroupCiphertextMessageWrapperBuilder(ciphertext: ciphertext, ephemeralPublicKey: ephemeralPublicKey) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoClosedGroupCiphertextMessageWrapperBuilder { + let builder = SSKProtoClosedGroupCiphertextMessageWrapperBuilder(ciphertext: ciphertext, ephemeralPublicKey: ephemeralPublicKey) + return builder + } + + @objc public class SSKProtoClosedGroupCiphertextMessageWrapperBuilder: NSObject { + + private var proto = SignalServiceProtos_ClosedGroupCiphertextMessageWrapper() + + @objc fileprivate override init() {} + + @objc fileprivate init(ciphertext: Data, ephemeralPublicKey: Data) { + super.init() + + setCiphertext(ciphertext) + setEphemeralPublicKey(ephemeralPublicKey) + } + + @objc public func setCiphertext(_ valueParam: Data) { + proto.ciphertext = valueParam + } + + @objc public func setEphemeralPublicKey(_ valueParam: Data) { + proto.ephemeralPublicKey = valueParam + } + + @objc public func build() throws -> SSKProtoClosedGroupCiphertextMessageWrapper { + return try SSKProtoClosedGroupCiphertextMessageWrapper.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoClosedGroupCiphertextMessageWrapper.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_ClosedGroupCiphertextMessageWrapper + + @objc public let ciphertext: Data + + @objc public let ephemeralPublicKey: Data + + private init(proto: SignalServiceProtos_ClosedGroupCiphertextMessageWrapper, + ciphertext: Data, + ephemeralPublicKey: Data) { + self.proto = proto + self.ciphertext = ciphertext + self.ephemeralPublicKey = ephemeralPublicKey + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoClosedGroupCiphertextMessageWrapper { + let proto = try SignalServiceProtos_ClosedGroupCiphertextMessageWrapper(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_ClosedGroupCiphertextMessageWrapper) throws -> SSKProtoClosedGroupCiphertextMessageWrapper { + guard proto.hasCiphertext else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: ciphertext") + } + let ciphertext = proto.ciphertext + + guard proto.hasEphemeralPublicKey else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: ephemeralPublicKey") + } + let ephemeralPublicKey = proto.ephemeralPublicKey + + // MARK: - Begin Validation Logic for SSKProtoClosedGroupCiphertextMessageWrapper - + + // MARK: - End Validation Logic for SSKProtoClosedGroupCiphertextMessageWrapper - + + let result = SSKProtoClosedGroupCiphertextMessageWrapper(proto: proto, + ciphertext: ciphertext, + ephemeralPublicKey: ephemeralPublicKey) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoClosedGroupCiphertextMessageWrapper { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoClosedGroupCiphertextMessageWrapper.SSKProtoClosedGroupCiphertextMessageWrapperBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoClosedGroupCiphertextMessageWrapper? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageQuoteQuotedAttachment + +@objc public class SSKProtoDataMessageQuoteQuotedAttachment: NSObject { + + // MARK: - SSKProtoDataMessageQuoteQuotedAttachmentFlags + + @objc public enum SSKProtoDataMessageQuoteQuotedAttachmentFlags: Int32 { + case voiceMessage = 1 + } + + private class func SSKProtoDataMessageQuoteQuotedAttachmentFlagsWrap(_ value: SignalServiceProtos_DataMessage.Quote.QuotedAttachment.Flags) -> SSKProtoDataMessageQuoteQuotedAttachmentFlags { + switch value { + case .voiceMessage: return .voiceMessage + } + } + + private class func SSKProtoDataMessageQuoteQuotedAttachmentFlagsUnwrap(_ value: SSKProtoDataMessageQuoteQuotedAttachmentFlags) -> SignalServiceProtos_DataMessage.Quote.QuotedAttachment.Flags { + switch value { + case .voiceMessage: return .voiceMessage + } + } + + // MARK: - SSKProtoDataMessageQuoteQuotedAttachmentBuilder + + @objc public class func builder() -> SSKProtoDataMessageQuoteQuotedAttachmentBuilder { + return SSKProtoDataMessageQuoteQuotedAttachmentBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageQuoteQuotedAttachmentBuilder { + let builder = SSKProtoDataMessageQuoteQuotedAttachmentBuilder() + if let _value = contentType { + builder.setContentType(_value) + } + if let _value = fileName { + builder.setFileName(_value) + } + if let _value = thumbnail { + builder.setThumbnail(_value) + } + if hasFlags { + builder.setFlags(flags) + } + return builder + } + + @objc public class SSKProtoDataMessageQuoteQuotedAttachmentBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Quote.QuotedAttachment() + + @objc fileprivate override init() {} + + @objc public func setContentType(_ valueParam: String) { + proto.contentType = valueParam + } + + @objc public func setFileName(_ valueParam: String) { + proto.fileName = valueParam + } + + @objc public func setThumbnail(_ valueParam: SSKProtoAttachmentPointer) { + proto.thumbnail = valueParam.proto + } + + @objc public func setFlags(_ valueParam: UInt32) { + proto.flags = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageQuoteQuotedAttachment { + return try SSKProtoDataMessageQuoteQuotedAttachment.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageQuoteQuotedAttachment.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Quote.QuotedAttachment + + @objc public let thumbnail: SSKProtoAttachmentPointer? + + @objc public var contentType: String? { + guard proto.hasContentType else { + return nil + } + return proto.contentType + } + @objc public var hasContentType: Bool { + return proto.hasContentType + } + + @objc public var fileName: String? { + guard proto.hasFileName else { + return nil + } + return proto.fileName + } + @objc public var hasFileName: Bool { + return proto.hasFileName + } + + @objc public var flags: UInt32 { + return proto.flags + } + @objc public var hasFlags: Bool { + return proto.hasFlags + } + + private init(proto: SignalServiceProtos_DataMessage.Quote.QuotedAttachment, + thumbnail: SSKProtoAttachmentPointer?) { + self.proto = proto + self.thumbnail = thumbnail + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageQuoteQuotedAttachment { + let proto = try SignalServiceProtos_DataMessage.Quote.QuotedAttachment(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Quote.QuotedAttachment) throws -> SSKProtoDataMessageQuoteQuotedAttachment { + var thumbnail: SSKProtoAttachmentPointer? = nil + if proto.hasThumbnail { + thumbnail = try SSKProtoAttachmentPointer.parseProto(proto.thumbnail) + } + + // MARK: - Begin Validation Logic for SSKProtoDataMessageQuoteQuotedAttachment - + + // MARK: - End Validation Logic for SSKProtoDataMessageQuoteQuotedAttachment - + + let result = SSKProtoDataMessageQuoteQuotedAttachment(proto: proto, + thumbnail: thumbnail) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageQuoteQuotedAttachment { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageQuoteQuotedAttachment.SSKProtoDataMessageQuoteQuotedAttachmentBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageQuoteQuotedAttachment? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageQuote + +@objc public class SSKProtoDataMessageQuote: NSObject { + + // MARK: - SSKProtoDataMessageQuoteBuilder + + @objc public class func builder(id: UInt64, author: String) -> SSKProtoDataMessageQuoteBuilder { + return SSKProtoDataMessageQuoteBuilder(id: id, author: author) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageQuoteBuilder { + let builder = SSKProtoDataMessageQuoteBuilder(id: id, author: author) + if let _value = text { + builder.setText(_value) + } + builder.setAttachments(attachments) + return builder + } + + @objc public class SSKProtoDataMessageQuoteBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Quote() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64, author: String) { + super.init() + + setId(id) + setAuthor(author) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func setAuthor(_ valueParam: String) { + proto.author = valueParam + } + + @objc public func setText(_ valueParam: String) { + proto.text = valueParam + } + + @objc public func addAttachments(_ valueParam: SSKProtoDataMessageQuoteQuotedAttachment) { + var items = proto.attachments + items.append(valueParam.proto) + proto.attachments = items + } + + @objc public func setAttachments(_ wrappedItems: [SSKProtoDataMessageQuoteQuotedAttachment]) { + proto.attachments = wrappedItems.map { $0.proto } + } + + @objc public func build() throws -> SSKProtoDataMessageQuote { + return try SSKProtoDataMessageQuote.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageQuote.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Quote + + @objc public let id: UInt64 + + @objc public let author: String + + @objc public let attachments: [SSKProtoDataMessageQuoteQuotedAttachment] + + @objc public var text: String? { + guard proto.hasText else { + return nil + } + return proto.text + } + @objc public var hasText: Bool { + return proto.hasText + } + + private init(proto: SignalServiceProtos_DataMessage.Quote, + id: UInt64, + author: String, + attachments: [SSKProtoDataMessageQuoteQuotedAttachment]) { + self.proto = proto + self.id = id + self.author = author + self.attachments = attachments + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageQuote { + let proto = try SignalServiceProtos_DataMessage.Quote(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Quote) throws -> SSKProtoDataMessageQuote { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + guard proto.hasAuthor else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: author") + } + let author = proto.author + + var attachments: [SSKProtoDataMessageQuoteQuotedAttachment] = [] + attachments = try proto.attachments.map { try SSKProtoDataMessageQuoteQuotedAttachment.parseProto($0) } + + // MARK: - Begin Validation Logic for SSKProtoDataMessageQuote - + + // MARK: - End Validation Logic for SSKProtoDataMessageQuote - + + let result = SSKProtoDataMessageQuote(proto: proto, + id: id, + author: author, + attachments: attachments) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageQuote { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageQuote.SSKProtoDataMessageQuoteBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageQuote? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageContactName + +@objc public class SSKProtoDataMessageContactName: NSObject { + + // MARK: - SSKProtoDataMessageContactNameBuilder + + @objc public class func builder() -> SSKProtoDataMessageContactNameBuilder { + return SSKProtoDataMessageContactNameBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageContactNameBuilder { + let builder = SSKProtoDataMessageContactNameBuilder() + if let _value = givenName { + builder.setGivenName(_value) + } + if let _value = familyName { + builder.setFamilyName(_value) + } + if let _value = prefix { + builder.setPrefix(_value) + } + if let _value = suffix { + builder.setSuffix(_value) + } + if let _value = middleName { + builder.setMiddleName(_value) + } + if let _value = displayName { + builder.setDisplayName(_value) + } + return builder + } + + @objc public class SSKProtoDataMessageContactNameBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Contact.Name() + + @objc fileprivate override init() {} + + @objc public func setGivenName(_ valueParam: String) { + proto.givenName = valueParam + } + + @objc public func setFamilyName(_ valueParam: String) { + proto.familyName = valueParam + } + + @objc public func setPrefix(_ valueParam: String) { + proto.prefix = valueParam + } + + @objc public func setSuffix(_ valueParam: String) { + proto.suffix = valueParam + } + + @objc public func setMiddleName(_ valueParam: String) { + proto.middleName = valueParam + } + + @objc public func setDisplayName(_ valueParam: String) { + proto.displayName = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageContactName { + return try SSKProtoDataMessageContactName.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageContactName.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Contact.Name + + @objc public var givenName: String? { + guard proto.hasGivenName else { + return nil + } + return proto.givenName + } + @objc public var hasGivenName: Bool { + return proto.hasGivenName + } + + @objc public var familyName: String? { + guard proto.hasFamilyName else { + return nil + } + return proto.familyName + } + @objc public var hasFamilyName: Bool { + return proto.hasFamilyName + } + + @objc public var prefix: String? { + guard proto.hasPrefix else { + return nil + } + return proto.prefix + } + @objc public var hasPrefix: Bool { + return proto.hasPrefix + } + + @objc public var suffix: String? { + guard proto.hasSuffix else { + return nil + } + return proto.suffix + } + @objc public var hasSuffix: Bool { + return proto.hasSuffix + } + + @objc public var middleName: String? { + guard proto.hasMiddleName else { + return nil + } + return proto.middleName + } + @objc public var hasMiddleName: Bool { + return proto.hasMiddleName + } + + @objc public var displayName: String? { + guard proto.hasDisplayName else { + return nil + } + return proto.displayName + } + @objc public var hasDisplayName: Bool { + return proto.hasDisplayName + } + + private init(proto: SignalServiceProtos_DataMessage.Contact.Name) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageContactName { + let proto = try SignalServiceProtos_DataMessage.Contact.Name(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Contact.Name) throws -> SSKProtoDataMessageContactName { + // MARK: - Begin Validation Logic for SSKProtoDataMessageContactName - + + // MARK: - End Validation Logic for SSKProtoDataMessageContactName - + + let result = SSKProtoDataMessageContactName(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageContactName { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageContactName.SSKProtoDataMessageContactNameBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageContactName? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageContactPhone + +@objc public class SSKProtoDataMessageContactPhone: NSObject { + + // MARK: - SSKProtoDataMessageContactPhoneType + + @objc public enum SSKProtoDataMessageContactPhoneType: Int32 { + case home = 1 + case mobile = 2 + case work = 3 + case custom = 4 + } + + private class func SSKProtoDataMessageContactPhoneTypeWrap(_ value: SignalServiceProtos_DataMessage.Contact.Phone.TypeEnum) -> SSKProtoDataMessageContactPhoneType { + switch value { + case .home: return .home + case .mobile: return .mobile + case .work: return .work + case .custom: return .custom + } + } + + private class func SSKProtoDataMessageContactPhoneTypeUnwrap(_ value: SSKProtoDataMessageContactPhoneType) -> SignalServiceProtos_DataMessage.Contact.Phone.TypeEnum { + switch value { + case .home: return .home + case .mobile: return .mobile + case .work: return .work + case .custom: return .custom + } + } + + // MARK: - SSKProtoDataMessageContactPhoneBuilder + + @objc public class func builder() -> SSKProtoDataMessageContactPhoneBuilder { + return SSKProtoDataMessageContactPhoneBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageContactPhoneBuilder { + let builder = SSKProtoDataMessageContactPhoneBuilder() + if let _value = value { + builder.setValue(_value) + } + if hasType { + builder.setType(type) + } + if let _value = label { + builder.setLabel(_value) + } + return builder + } + + @objc public class SSKProtoDataMessageContactPhoneBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Contact.Phone() + + @objc fileprivate override init() {} + + @objc public func setValue(_ valueParam: String) { + proto.value = valueParam + } + + @objc public func setType(_ valueParam: SSKProtoDataMessageContactPhoneType) { + proto.type = SSKProtoDataMessageContactPhoneTypeUnwrap(valueParam) + } + + @objc public func setLabel(_ valueParam: String) { + proto.label = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageContactPhone { + return try SSKProtoDataMessageContactPhone.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageContactPhone.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Contact.Phone + + @objc public var value: String? { + guard proto.hasValue else { + return nil + } + return proto.value + } + @objc public var hasValue: Bool { + return proto.hasValue + } + + @objc public var type: SSKProtoDataMessageContactPhoneType { + return SSKProtoDataMessageContactPhone.SSKProtoDataMessageContactPhoneTypeWrap(proto.type) + } + @objc public var hasType: Bool { + return proto.hasType + } + + @objc public var label: String? { + guard proto.hasLabel else { + return nil + } + return proto.label + } + @objc public var hasLabel: Bool { + return proto.hasLabel + } + + private init(proto: SignalServiceProtos_DataMessage.Contact.Phone) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageContactPhone { + let proto = try SignalServiceProtos_DataMessage.Contact.Phone(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Contact.Phone) throws -> SSKProtoDataMessageContactPhone { + // MARK: - Begin Validation Logic for SSKProtoDataMessageContactPhone - + + // MARK: - End Validation Logic for SSKProtoDataMessageContactPhone - + + let result = SSKProtoDataMessageContactPhone(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageContactPhone { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageContactPhone.SSKProtoDataMessageContactPhoneBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageContactPhone? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageContactEmail + +@objc public class SSKProtoDataMessageContactEmail: NSObject { + + // MARK: - SSKProtoDataMessageContactEmailType + + @objc public enum SSKProtoDataMessageContactEmailType: Int32 { + case home = 1 + case mobile = 2 + case work = 3 + case custom = 4 + } + + private class func SSKProtoDataMessageContactEmailTypeWrap(_ value: SignalServiceProtos_DataMessage.Contact.Email.TypeEnum) -> SSKProtoDataMessageContactEmailType { + switch value { + case .home: return .home + case .mobile: return .mobile + case .work: return .work + case .custom: return .custom + } + } + + private class func SSKProtoDataMessageContactEmailTypeUnwrap(_ value: SSKProtoDataMessageContactEmailType) -> SignalServiceProtos_DataMessage.Contact.Email.TypeEnum { + switch value { + case .home: return .home + case .mobile: return .mobile + case .work: return .work + case .custom: return .custom + } + } + + // MARK: - SSKProtoDataMessageContactEmailBuilder + + @objc public class func builder() -> SSKProtoDataMessageContactEmailBuilder { + return SSKProtoDataMessageContactEmailBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageContactEmailBuilder { + let builder = SSKProtoDataMessageContactEmailBuilder() + if let _value = value { + builder.setValue(_value) + } + if hasType { + builder.setType(type) + } + if let _value = label { + builder.setLabel(_value) + } + return builder + } + + @objc public class SSKProtoDataMessageContactEmailBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Contact.Email() + + @objc fileprivate override init() {} + + @objc public func setValue(_ valueParam: String) { + proto.value = valueParam + } + + @objc public func setType(_ valueParam: SSKProtoDataMessageContactEmailType) { + proto.type = SSKProtoDataMessageContactEmailTypeUnwrap(valueParam) + } + + @objc public func setLabel(_ valueParam: String) { + proto.label = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageContactEmail { + return try SSKProtoDataMessageContactEmail.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageContactEmail.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Contact.Email + + @objc public var value: String? { + guard proto.hasValue else { + return nil + } + return proto.value + } + @objc public var hasValue: Bool { + return proto.hasValue + } + + @objc public var type: SSKProtoDataMessageContactEmailType { + return SSKProtoDataMessageContactEmail.SSKProtoDataMessageContactEmailTypeWrap(proto.type) + } + @objc public var hasType: Bool { + return proto.hasType + } + + @objc public var label: String? { + guard proto.hasLabel else { + return nil + } + return proto.label + } + @objc public var hasLabel: Bool { + return proto.hasLabel + } + + private init(proto: SignalServiceProtos_DataMessage.Contact.Email) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageContactEmail { + let proto = try SignalServiceProtos_DataMessage.Contact.Email(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Contact.Email) throws -> SSKProtoDataMessageContactEmail { + // MARK: - Begin Validation Logic for SSKProtoDataMessageContactEmail - + + // MARK: - End Validation Logic for SSKProtoDataMessageContactEmail - + + let result = SSKProtoDataMessageContactEmail(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageContactEmail { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageContactEmail.SSKProtoDataMessageContactEmailBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageContactEmail? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageContactPostalAddress + +@objc public class SSKProtoDataMessageContactPostalAddress: NSObject { + + // MARK: - SSKProtoDataMessageContactPostalAddressType + + @objc public enum SSKProtoDataMessageContactPostalAddressType: Int32 { + case home = 1 + case work = 2 + case custom = 3 + } + + private class func SSKProtoDataMessageContactPostalAddressTypeWrap(_ value: SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum) -> SSKProtoDataMessageContactPostalAddressType { + switch value { + case .home: return .home + case .work: return .work + case .custom: return .custom + } + } + + private class func SSKProtoDataMessageContactPostalAddressTypeUnwrap(_ value: SSKProtoDataMessageContactPostalAddressType) -> SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum { + switch value { + case .home: return .home + case .work: return .work + case .custom: return .custom + } + } + + // MARK: - SSKProtoDataMessageContactPostalAddressBuilder + + @objc public class func builder() -> SSKProtoDataMessageContactPostalAddressBuilder { + return SSKProtoDataMessageContactPostalAddressBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageContactPostalAddressBuilder { + let builder = SSKProtoDataMessageContactPostalAddressBuilder() + if hasType { + builder.setType(type) + } + if let _value = label { + builder.setLabel(_value) + } + if let _value = street { + builder.setStreet(_value) + } + if let _value = pobox { + builder.setPobox(_value) + } + if let _value = neighborhood { + builder.setNeighborhood(_value) + } + if let _value = city { + builder.setCity(_value) + } + if let _value = region { + builder.setRegion(_value) + } + if let _value = postcode { + builder.setPostcode(_value) + } + if let _value = country { + builder.setCountry(_value) + } + return builder + } + + @objc public class SSKProtoDataMessageContactPostalAddressBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Contact.PostalAddress() + + @objc fileprivate override init() {} + + @objc public func setType(_ valueParam: SSKProtoDataMessageContactPostalAddressType) { + proto.type = SSKProtoDataMessageContactPostalAddressTypeUnwrap(valueParam) + } + + @objc public func setLabel(_ valueParam: String) { + proto.label = valueParam + } + + @objc public func setStreet(_ valueParam: String) { + proto.street = valueParam + } + + @objc public func setPobox(_ valueParam: String) { + proto.pobox = valueParam + } + + @objc public func setNeighborhood(_ valueParam: String) { + proto.neighborhood = valueParam + } + + @objc public func setCity(_ valueParam: String) { + proto.city = valueParam + } + + @objc public func setRegion(_ valueParam: String) { + proto.region = valueParam + } + + @objc public func setPostcode(_ valueParam: String) { + proto.postcode = valueParam + } + + @objc public func setCountry(_ valueParam: String) { + proto.country = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageContactPostalAddress { + return try SSKProtoDataMessageContactPostalAddress.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageContactPostalAddress.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Contact.PostalAddress + + @objc public var type: SSKProtoDataMessageContactPostalAddressType { + return SSKProtoDataMessageContactPostalAddress.SSKProtoDataMessageContactPostalAddressTypeWrap(proto.type) + } + @objc public var hasType: Bool { + return proto.hasType + } + + @objc public var label: String? { + guard proto.hasLabel else { + return nil + } + return proto.label + } + @objc public var hasLabel: Bool { + return proto.hasLabel + } + + @objc public var street: String? { + guard proto.hasStreet else { + return nil + } + return proto.street + } + @objc public var hasStreet: Bool { + return proto.hasStreet + } + + @objc public var pobox: String? { + guard proto.hasPobox else { + return nil + } + return proto.pobox + } + @objc public var hasPobox: Bool { + return proto.hasPobox + } + + @objc public var neighborhood: String? { + guard proto.hasNeighborhood else { + return nil + } + return proto.neighborhood + } + @objc public var hasNeighborhood: Bool { + return proto.hasNeighborhood + } + + @objc public var city: String? { + guard proto.hasCity else { + return nil + } + return proto.city + } + @objc public var hasCity: Bool { + return proto.hasCity + } + + @objc public var region: String? { + guard proto.hasRegion else { + return nil + } + return proto.region + } + @objc public var hasRegion: Bool { + return proto.hasRegion + } + + @objc public var postcode: String? { + guard proto.hasPostcode else { + return nil + } + return proto.postcode + } + @objc public var hasPostcode: Bool { + return proto.hasPostcode + } + + @objc public var country: String? { + guard proto.hasCountry else { + return nil + } + return proto.country + } + @objc public var hasCountry: Bool { + return proto.hasCountry + } + + private init(proto: SignalServiceProtos_DataMessage.Contact.PostalAddress) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageContactPostalAddress { + let proto = try SignalServiceProtos_DataMessage.Contact.PostalAddress(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Contact.PostalAddress) throws -> SSKProtoDataMessageContactPostalAddress { + // MARK: - Begin Validation Logic for SSKProtoDataMessageContactPostalAddress - + + // MARK: - End Validation Logic for SSKProtoDataMessageContactPostalAddress - + + let result = SSKProtoDataMessageContactPostalAddress(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageContactPostalAddress { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageContactPostalAddress.SSKProtoDataMessageContactPostalAddressBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageContactPostalAddress? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageContactAvatar + +@objc public class SSKProtoDataMessageContactAvatar: NSObject { + + // MARK: - SSKProtoDataMessageContactAvatarBuilder + + @objc public class func builder() -> SSKProtoDataMessageContactAvatarBuilder { + return SSKProtoDataMessageContactAvatarBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageContactAvatarBuilder { + let builder = SSKProtoDataMessageContactAvatarBuilder() + if let _value = avatar { + builder.setAvatar(_value) + } + if hasIsProfile { + builder.setIsProfile(isProfile) + } + return builder + } + + @objc public class SSKProtoDataMessageContactAvatarBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Contact.Avatar() + + @objc fileprivate override init() {} + + @objc public func setAvatar(_ valueParam: SSKProtoAttachmentPointer) { + proto.avatar = valueParam.proto + } + + @objc public func setIsProfile(_ valueParam: Bool) { + proto.isProfile = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageContactAvatar { + return try SSKProtoDataMessageContactAvatar.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageContactAvatar.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Contact.Avatar + + @objc public let avatar: SSKProtoAttachmentPointer? + + @objc public var isProfile: Bool { + return proto.isProfile + } + @objc public var hasIsProfile: Bool { + return proto.hasIsProfile + } + + private init(proto: SignalServiceProtos_DataMessage.Contact.Avatar, + avatar: SSKProtoAttachmentPointer?) { + self.proto = proto + self.avatar = avatar + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageContactAvatar { + let proto = try SignalServiceProtos_DataMessage.Contact.Avatar(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Contact.Avatar) throws -> SSKProtoDataMessageContactAvatar { + var avatar: SSKProtoAttachmentPointer? = nil + if proto.hasAvatar { + avatar = try SSKProtoAttachmentPointer.parseProto(proto.avatar) + } + + // MARK: - Begin Validation Logic for SSKProtoDataMessageContactAvatar - + + // MARK: - End Validation Logic for SSKProtoDataMessageContactAvatar - + + let result = SSKProtoDataMessageContactAvatar(proto: proto, + avatar: avatar) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageContactAvatar { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageContactAvatar.SSKProtoDataMessageContactAvatarBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageContactAvatar? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageContact + +@objc public class SSKProtoDataMessageContact: NSObject { + + // MARK: - SSKProtoDataMessageContactBuilder + + @objc public class func builder() -> SSKProtoDataMessageContactBuilder { + return SSKProtoDataMessageContactBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageContactBuilder { + let builder = SSKProtoDataMessageContactBuilder() + if let _value = name { + builder.setName(_value) + } + builder.setNumber(number) + builder.setEmail(email) + builder.setAddress(address) + if let _value = avatar { + builder.setAvatar(_value) + } + if let _value = organization { + builder.setOrganization(_value) + } + return builder + } + + @objc public class SSKProtoDataMessageContactBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Contact() + + @objc fileprivate override init() {} + + @objc public func setName(_ valueParam: SSKProtoDataMessageContactName) { + proto.name = valueParam.proto + } + + @objc public func addNumber(_ valueParam: SSKProtoDataMessageContactPhone) { + var items = proto.number + items.append(valueParam.proto) + proto.number = items + } + + @objc public func setNumber(_ wrappedItems: [SSKProtoDataMessageContactPhone]) { + proto.number = wrappedItems.map { $0.proto } + } + + @objc public func addEmail(_ valueParam: SSKProtoDataMessageContactEmail) { + var items = proto.email + items.append(valueParam.proto) + proto.email = items + } + + @objc public func setEmail(_ wrappedItems: [SSKProtoDataMessageContactEmail]) { + proto.email = wrappedItems.map { $0.proto } + } + + @objc public func addAddress(_ valueParam: SSKProtoDataMessageContactPostalAddress) { + var items = proto.address + items.append(valueParam.proto) + proto.address = items + } + + @objc public func setAddress(_ wrappedItems: [SSKProtoDataMessageContactPostalAddress]) { + proto.address = wrappedItems.map { $0.proto } + } + + @objc public func setAvatar(_ valueParam: SSKProtoDataMessageContactAvatar) { + proto.avatar = valueParam.proto + } + + @objc public func setOrganization(_ valueParam: String) { + proto.organization = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageContact { + return try SSKProtoDataMessageContact.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageContact.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Contact + + @objc public let name: SSKProtoDataMessageContactName? + + @objc public let number: [SSKProtoDataMessageContactPhone] + + @objc public let email: [SSKProtoDataMessageContactEmail] + + @objc public let address: [SSKProtoDataMessageContactPostalAddress] + + @objc public let avatar: SSKProtoDataMessageContactAvatar? + + @objc public var organization: String? { + guard proto.hasOrganization else { + return nil + } + return proto.organization + } + @objc public var hasOrganization: Bool { + return proto.hasOrganization + } + + private init(proto: SignalServiceProtos_DataMessage.Contact, + name: SSKProtoDataMessageContactName?, + number: [SSKProtoDataMessageContactPhone], + email: [SSKProtoDataMessageContactEmail], + address: [SSKProtoDataMessageContactPostalAddress], + avatar: SSKProtoDataMessageContactAvatar?) { + self.proto = proto + self.name = name + self.number = number + self.email = email + self.address = address + self.avatar = avatar + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageContact { + let proto = try SignalServiceProtos_DataMessage.Contact(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Contact) throws -> SSKProtoDataMessageContact { + var name: SSKProtoDataMessageContactName? = nil + if proto.hasName { + name = try SSKProtoDataMessageContactName.parseProto(proto.name) + } + + var number: [SSKProtoDataMessageContactPhone] = [] + number = try proto.number.map { try SSKProtoDataMessageContactPhone.parseProto($0) } + + var email: [SSKProtoDataMessageContactEmail] = [] + email = try proto.email.map { try SSKProtoDataMessageContactEmail.parseProto($0) } + + var address: [SSKProtoDataMessageContactPostalAddress] = [] + address = try proto.address.map { try SSKProtoDataMessageContactPostalAddress.parseProto($0) } + + var avatar: SSKProtoDataMessageContactAvatar? = nil + if proto.hasAvatar { + avatar = try SSKProtoDataMessageContactAvatar.parseProto(proto.avatar) + } + + // MARK: - Begin Validation Logic for SSKProtoDataMessageContact - + + // MARK: - End Validation Logic for SSKProtoDataMessageContact - + + let result = SSKProtoDataMessageContact(proto: proto, + name: name, + number: number, + email: email, + address: address, + avatar: avatar) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageContact { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageContact.SSKProtoDataMessageContactBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageContact? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessagePreview + +@objc public class SSKProtoDataMessagePreview: NSObject { + + // MARK: - SSKProtoDataMessagePreviewBuilder + + @objc public class func builder(url: String) -> SSKProtoDataMessagePreviewBuilder { + return SSKProtoDataMessagePreviewBuilder(url: url) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessagePreviewBuilder { + let builder = SSKProtoDataMessagePreviewBuilder(url: url) + if let _value = title { + builder.setTitle(_value) + } + if let _value = image { + builder.setImage(_value) + } + return builder + } + + @objc public class SSKProtoDataMessagePreviewBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.Preview() + + @objc fileprivate override init() {} + + @objc fileprivate init(url: String) { + super.init() + + setUrl(url) + } + + @objc public func setUrl(_ valueParam: String) { + proto.url = valueParam + } + + @objc public func setTitle(_ valueParam: String) { + proto.title = valueParam + } + + @objc public func setImage(_ valueParam: SSKProtoAttachmentPointer) { + proto.image = valueParam.proto + } + + @objc public func build() throws -> SSKProtoDataMessagePreview { + return try SSKProtoDataMessagePreview.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessagePreview.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.Preview + + @objc public let url: String + + @objc public let image: SSKProtoAttachmentPointer? + + @objc public var title: String? { + guard proto.hasTitle else { + return nil + } + return proto.title + } + @objc public var hasTitle: Bool { + return proto.hasTitle + } + + private init(proto: SignalServiceProtos_DataMessage.Preview, + url: String, + image: SSKProtoAttachmentPointer?) { + self.proto = proto + self.url = url + self.image = image + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessagePreview { + let proto = try SignalServiceProtos_DataMessage.Preview(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.Preview) throws -> SSKProtoDataMessagePreview { + guard proto.hasURL else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: url") + } + let url = proto.url + + var image: SSKProtoAttachmentPointer? = nil + if proto.hasImage { + image = try SSKProtoAttachmentPointer.parseProto(proto.image) + } + + // MARK: - Begin Validation Logic for SSKProtoDataMessagePreview - + + // MARK: - End Validation Logic for SSKProtoDataMessagePreview - + + let result = SSKProtoDataMessagePreview(proto: proto, + url: url, + image: image) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessagePreview { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessagePreview? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageLokiProfile + +@objc public class SSKProtoDataMessageLokiProfile: NSObject { + + // MARK: - SSKProtoDataMessageLokiProfileBuilder + + @objc public class func builder() -> SSKProtoDataMessageLokiProfileBuilder { + return SSKProtoDataMessageLokiProfileBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageLokiProfileBuilder { + let builder = SSKProtoDataMessageLokiProfileBuilder() + if let _value = displayName { + builder.setDisplayName(_value) + } + if let _value = profilePicture { + builder.setProfilePicture(_value) + } + return builder + } + + @objc public class SSKProtoDataMessageLokiProfileBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.LokiProfile() + + @objc fileprivate override init() {} + + @objc public func setDisplayName(_ valueParam: String) { + proto.displayName = valueParam + } + + @objc public func setProfilePicture(_ valueParam: String) { + proto.profilePicture = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageLokiProfile { + return try SSKProtoDataMessageLokiProfile.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageLokiProfile.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.LokiProfile + + @objc public var displayName: String? { + guard proto.hasDisplayName else { + return nil + } + return proto.displayName + } + @objc public var hasDisplayName: Bool { + return proto.hasDisplayName + } + + @objc public var profilePicture: String? { + guard proto.hasProfilePicture else { + return nil + } + return proto.profilePicture + } + @objc public var hasProfilePicture: Bool { + return proto.hasProfilePicture + } + + private init(proto: SignalServiceProtos_DataMessage.LokiProfile) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageLokiProfile { + let proto = try SignalServiceProtos_DataMessage.LokiProfile(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.LokiProfile) throws -> SSKProtoDataMessageLokiProfile { + // MARK: - Begin Validation Logic for SSKProtoDataMessageLokiProfile - + + // MARK: - End Validation Logic for SSKProtoDataMessageLokiProfile - + + let result = SSKProtoDataMessageLokiProfile(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageLokiProfile { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageLokiProfile? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageClosedGroupUpdateSenderKey + +@objc public class SSKProtoDataMessageClosedGroupUpdateSenderKey: NSObject { + + // MARK: - SSKProtoDataMessageClosedGroupUpdateSenderKeyBuilder + + @objc public class func builder(chainKey: Data, keyIndex: UInt32, publicKey: Data) -> SSKProtoDataMessageClosedGroupUpdateSenderKeyBuilder { + return SSKProtoDataMessageClosedGroupUpdateSenderKeyBuilder(chainKey: chainKey, keyIndex: keyIndex, publicKey: publicKey) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageClosedGroupUpdateSenderKeyBuilder { + let builder = SSKProtoDataMessageClosedGroupUpdateSenderKeyBuilder(chainKey: chainKey, keyIndex: keyIndex, publicKey: publicKey) + return builder + } + + @objc public class SSKProtoDataMessageClosedGroupUpdateSenderKeyBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey() + + @objc fileprivate override init() {} + + @objc fileprivate init(chainKey: Data, keyIndex: UInt32, publicKey: Data) { + super.init() + + setChainKey(chainKey) + setKeyIndex(keyIndex) + setPublicKey(publicKey) + } + + @objc public func setChainKey(_ valueParam: Data) { + proto.chainKey = valueParam + } + + @objc public func setKeyIndex(_ valueParam: UInt32) { + proto.keyIndex = valueParam + } + + @objc public func setPublicKey(_ valueParam: Data) { + proto.publicKey = valueParam + } + + @objc public func build() throws -> SSKProtoDataMessageClosedGroupUpdateSenderKey { + return try SSKProtoDataMessageClosedGroupUpdateSenderKey.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageClosedGroupUpdateSenderKey.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey + + @objc public let chainKey: Data + + @objc public let keyIndex: UInt32 + + @objc public let publicKey: Data + + private init(proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey, + chainKey: Data, + keyIndex: UInt32, + publicKey: Data) { + self.proto = proto + self.chainKey = chainKey + self.keyIndex = keyIndex + self.publicKey = publicKey + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageClosedGroupUpdateSenderKey { + let proto = try SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey) throws -> SSKProtoDataMessageClosedGroupUpdateSenderKey { + guard proto.hasChainKey else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: chainKey") + } + let chainKey = proto.chainKey + + guard proto.hasKeyIndex else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: keyIndex") + } + let keyIndex = proto.keyIndex + + guard proto.hasPublicKey else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: publicKey") + } + let publicKey = proto.publicKey + + // MARK: - Begin Validation Logic for SSKProtoDataMessageClosedGroupUpdateSenderKey - + + // MARK: - End Validation Logic for SSKProtoDataMessageClosedGroupUpdateSenderKey - + + let result = SSKProtoDataMessageClosedGroupUpdateSenderKey(proto: proto, + chainKey: chainKey, + keyIndex: keyIndex, + publicKey: publicKey) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageClosedGroupUpdateSenderKey { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageClosedGroupUpdateSenderKey.SSKProtoDataMessageClosedGroupUpdateSenderKeyBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageClosedGroupUpdateSenderKey? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessageClosedGroupUpdate + +@objc public class SSKProtoDataMessageClosedGroupUpdate: NSObject { + + // MARK: - SSKProtoDataMessageClosedGroupUpdateType + + @objc public enum SSKProtoDataMessageClosedGroupUpdateType: Int32 { + case new = 0 + case info = 1 + case senderKeyRequest = 2 + case senderKey = 3 + } + + private class func SSKProtoDataMessageClosedGroupUpdateTypeWrap(_ value: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum) -> SSKProtoDataMessageClosedGroupUpdateType { + switch value { + case .new: return .new + case .info: return .info + case .senderKeyRequest: return .senderKeyRequest + case .senderKey: return .senderKey + } + } + + private class func SSKProtoDataMessageClosedGroupUpdateTypeUnwrap(_ value: SSKProtoDataMessageClosedGroupUpdateType) -> SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum { + switch value { + case .new: return .new + case .info: return .info + case .senderKeyRequest: return .senderKeyRequest + case .senderKey: return .senderKey + } + } + + // MARK: - SSKProtoDataMessageClosedGroupUpdateBuilder + + @objc public class func builder(groupPublicKey: Data, type: SSKProtoDataMessageClosedGroupUpdateType) -> SSKProtoDataMessageClosedGroupUpdateBuilder { + return SSKProtoDataMessageClosedGroupUpdateBuilder(groupPublicKey: groupPublicKey, type: type) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageClosedGroupUpdateBuilder { + let builder = SSKProtoDataMessageClosedGroupUpdateBuilder(groupPublicKey: groupPublicKey, type: type) + if let _value = name { + builder.setName(_value) + } + if let _value = groupPrivateKey { + builder.setGroupPrivateKey(_value) + } + builder.setSenderKeys(senderKeys) + builder.setMembers(members) + builder.setAdmins(admins) + return builder + } + + @objc public class SSKProtoDataMessageClosedGroupUpdateBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage.ClosedGroupUpdate() + + @objc fileprivate override init() {} + + @objc fileprivate init(groupPublicKey: Data, type: SSKProtoDataMessageClosedGroupUpdateType) { + super.init() + + setGroupPublicKey(groupPublicKey) + setType(type) + } + + @objc public func setName(_ valueParam: String) { + proto.name = valueParam + } + + @objc public func setGroupPublicKey(_ valueParam: Data) { + proto.groupPublicKey = valueParam + } + + @objc public func setGroupPrivateKey(_ valueParam: Data) { + proto.groupPrivateKey = valueParam + } + + @objc public func addSenderKeys(_ valueParam: SSKProtoDataMessageClosedGroupUpdateSenderKey) { + var items = proto.senderKeys + items.append(valueParam.proto) + proto.senderKeys = items + } + + @objc public func setSenderKeys(_ wrappedItems: [SSKProtoDataMessageClosedGroupUpdateSenderKey]) { + proto.senderKeys = wrappedItems.map { $0.proto } + } + + @objc public func addMembers(_ valueParam: Data) { + var items = proto.members + items.append(valueParam) + proto.members = items + } + + @objc public func setMembers(_ wrappedItems: [Data]) { + proto.members = wrappedItems + } + + @objc public func addAdmins(_ valueParam: Data) { + var items = proto.admins + items.append(valueParam) + proto.admins = items + } + + @objc public func setAdmins(_ wrappedItems: [Data]) { + proto.admins = wrappedItems + } + + @objc public func setType(_ valueParam: SSKProtoDataMessageClosedGroupUpdateType) { + proto.type = SSKProtoDataMessageClosedGroupUpdateTypeUnwrap(valueParam) + } + + @objc public func build() throws -> SSKProtoDataMessageClosedGroupUpdate { + return try SSKProtoDataMessageClosedGroupUpdate.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessageClosedGroupUpdate.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate + + @objc public let groupPublicKey: Data + + @objc public let senderKeys: [SSKProtoDataMessageClosedGroupUpdateSenderKey] + + @objc public let type: SSKProtoDataMessageClosedGroupUpdateType + + @objc public var name: String? { + guard proto.hasName else { + return nil + } + return proto.name + } + @objc public var hasName: Bool { + return proto.hasName + } + + @objc public var groupPrivateKey: Data? { + guard proto.hasGroupPrivateKey else { + return nil + } + return proto.groupPrivateKey + } + @objc public var hasGroupPrivateKey: Bool { + return proto.hasGroupPrivateKey + } + + @objc public var members: [Data] { + return proto.members + } + + @objc public var admins: [Data] { + return proto.admins + } + + private init(proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate, + groupPublicKey: Data, + senderKeys: [SSKProtoDataMessageClosedGroupUpdateSenderKey], + type: SSKProtoDataMessageClosedGroupUpdateType) { + self.proto = proto + self.groupPublicKey = groupPublicKey + self.senderKeys = senderKeys + self.type = type + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessageClosedGroupUpdate { + let proto = try SignalServiceProtos_DataMessage.ClosedGroupUpdate(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate) throws -> SSKProtoDataMessageClosedGroupUpdate { + guard proto.hasGroupPublicKey else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: groupPublicKey") + } + let groupPublicKey = proto.groupPublicKey + + var senderKeys: [SSKProtoDataMessageClosedGroupUpdateSenderKey] = [] + senderKeys = try proto.senderKeys.map { try SSKProtoDataMessageClosedGroupUpdateSenderKey.parseProto($0) } + + guard proto.hasType else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: type") + } + let type = SSKProtoDataMessageClosedGroupUpdateTypeWrap(proto.type) + + // MARK: - Begin Validation Logic for SSKProtoDataMessageClosedGroupUpdate - + + // MARK: - End Validation Logic for SSKProtoDataMessageClosedGroupUpdate - + + let result = SSKProtoDataMessageClosedGroupUpdate(proto: proto, + groupPublicKey: groupPublicKey, + senderKeys: senderKeys, + type: type) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessageClosedGroupUpdate { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessageClosedGroupUpdate.SSKProtoDataMessageClosedGroupUpdateBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessageClosedGroupUpdate? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoDataMessage + +@objc public class SSKProtoDataMessage: NSObject { + + // MARK: - SSKProtoDataMessageFlags + + @objc public enum SSKProtoDataMessageFlags: Int32 { + case endSession = 1 + case expirationTimerUpdate = 2 + case profileKeyUpdate = 4 + case unlinkDevice = 128 + } + + private class func SSKProtoDataMessageFlagsWrap(_ value: SignalServiceProtos_DataMessage.Flags) -> SSKProtoDataMessageFlags { + switch value { + case .endSession: return .endSession + case .expirationTimerUpdate: return .expirationTimerUpdate + case .profileKeyUpdate: return .profileKeyUpdate + case .unlinkDevice: return .unlinkDevice + } + } + + private class func SSKProtoDataMessageFlagsUnwrap(_ value: SSKProtoDataMessageFlags) -> SignalServiceProtos_DataMessage.Flags { + switch value { + case .endSession: return .endSession + case .expirationTimerUpdate: return .expirationTimerUpdate + case .profileKeyUpdate: return .profileKeyUpdate + case .unlinkDevice: return .unlinkDevice + } + } + + // MARK: - SSKProtoDataMessageBuilder + + @objc public class func builder() -> SSKProtoDataMessageBuilder { + return SSKProtoDataMessageBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoDataMessageBuilder { + let builder = SSKProtoDataMessageBuilder() + if let _value = body { + builder.setBody(_value) + } + builder.setAttachments(attachments) + if let _value = group { + builder.setGroup(_value) + } + if hasFlags { + builder.setFlags(flags) + } + if hasExpireTimer { + builder.setExpireTimer(expireTimer) + } + if let _value = profileKey { + builder.setProfileKey(_value) + } + if hasTimestamp { + builder.setTimestamp(timestamp) + } + if let _value = quote { + builder.setQuote(_value) + } + builder.setContact(contact) + builder.setPreview(preview) + if let _value = profile { + builder.setProfile(_value) + } + if let _value = closedGroupUpdate { + builder.setClosedGroupUpdate(_value) + } + if let _value = publicChatInfo { + builder.setPublicChatInfo(_value) + } + return builder + } + + @objc public class SSKProtoDataMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_DataMessage() + + @objc fileprivate override init() {} + + @objc public func setBody(_ valueParam: String) { + proto.body = valueParam + } + + @objc public func addAttachments(_ valueParam: SSKProtoAttachmentPointer) { + var items = proto.attachments + items.append(valueParam.proto) + proto.attachments = items + } + + @objc public func setAttachments(_ wrappedItems: [SSKProtoAttachmentPointer]) { + proto.attachments = wrappedItems.map { $0.proto } + } + + @objc public func setGroup(_ valueParam: SSKProtoGroupContext) { + proto.group = valueParam.proto + } + + @objc public func setFlags(_ valueParam: UInt32) { + proto.flags = valueParam + } + + @objc public func setExpireTimer(_ valueParam: UInt32) { + proto.expireTimer = valueParam + } + + @objc public func setProfileKey(_ valueParam: Data) { + proto.profileKey = valueParam + } + + @objc public func setTimestamp(_ valueParam: UInt64) { + proto.timestamp = valueParam + } + + @objc public func setQuote(_ valueParam: SSKProtoDataMessageQuote) { + proto.quote = valueParam.proto + } + + @objc public func addContact(_ valueParam: SSKProtoDataMessageContact) { + var items = proto.contact + items.append(valueParam.proto) + proto.contact = items + } + + @objc public func setContact(_ wrappedItems: [SSKProtoDataMessageContact]) { + proto.contact = wrappedItems.map { $0.proto } + } + + @objc public func addPreview(_ valueParam: SSKProtoDataMessagePreview) { + var items = proto.preview + items.append(valueParam.proto) + proto.preview = items + } + + @objc public func setPreview(_ wrappedItems: [SSKProtoDataMessagePreview]) { + proto.preview = wrappedItems.map { $0.proto } + } + + @objc public func setProfile(_ valueParam: SSKProtoDataMessageLokiProfile) { + proto.profile = valueParam.proto + } + + @objc public func setClosedGroupUpdate(_ valueParam: SSKProtoDataMessageClosedGroupUpdate) { + proto.closedGroupUpdate = valueParam.proto + } + + @objc public func setPublicChatInfo(_ valueParam: SSKProtoPublicChatInfo) { + proto.publicChatInfo = valueParam.proto + } + + @objc public func build() throws -> SSKProtoDataMessage { + return try SSKProtoDataMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoDataMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_DataMessage + + @objc public let attachments: [SSKProtoAttachmentPointer] + + @objc public let group: SSKProtoGroupContext? + + @objc public let quote: SSKProtoDataMessageQuote? + + @objc public let contact: [SSKProtoDataMessageContact] + + @objc public let preview: [SSKProtoDataMessagePreview] + + @objc public let profile: SSKProtoDataMessageLokiProfile? + + @objc public let closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate? + + @objc public let publicChatInfo: SSKProtoPublicChatInfo? + + @objc public var body: String? { + guard proto.hasBody else { + return nil + } + return proto.body + } + @objc public var hasBody: Bool { + return proto.hasBody + } + + @objc public var flags: UInt32 { + return proto.flags + } + @objc public var hasFlags: Bool { + return proto.hasFlags + } + + @objc public var expireTimer: UInt32 { + return proto.expireTimer + } + @objc public var hasExpireTimer: Bool { + return proto.hasExpireTimer + } + + @objc public var profileKey: Data? { + guard proto.hasProfileKey else { + return nil + } + return proto.profileKey + } + @objc public var hasProfileKey: Bool { + return proto.hasProfileKey + } + + @objc public var timestamp: UInt64 { + return proto.timestamp + } + @objc public var hasTimestamp: Bool { + return proto.hasTimestamp + } + + private init(proto: SignalServiceProtos_DataMessage, + attachments: [SSKProtoAttachmentPointer], + group: SSKProtoGroupContext?, + quote: SSKProtoDataMessageQuote?, + contact: [SSKProtoDataMessageContact], + preview: [SSKProtoDataMessagePreview], + profile: SSKProtoDataMessageLokiProfile?, + closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate?, + publicChatInfo: SSKProtoPublicChatInfo?) { + self.proto = proto + self.attachments = attachments + self.group = group + self.quote = quote + self.contact = contact + self.preview = preview + self.profile = profile + self.closedGroupUpdate = closedGroupUpdate + self.publicChatInfo = publicChatInfo + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoDataMessage { + let proto = try SignalServiceProtos_DataMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage) throws -> SSKProtoDataMessage { + var attachments: [SSKProtoAttachmentPointer] = [] + attachments = try proto.attachments.map { try SSKProtoAttachmentPointer.parseProto($0) } + + var group: SSKProtoGroupContext? = nil + if proto.hasGroup { + group = try SSKProtoGroupContext.parseProto(proto.group) + } + + var quote: SSKProtoDataMessageQuote? = nil + if proto.hasQuote { + quote = try SSKProtoDataMessageQuote.parseProto(proto.quote) + } + + var contact: [SSKProtoDataMessageContact] = [] + contact = try proto.contact.map { try SSKProtoDataMessageContact.parseProto($0) } + + var preview: [SSKProtoDataMessagePreview] = [] + preview = try proto.preview.map { try SSKProtoDataMessagePreview.parseProto($0) } + + var profile: SSKProtoDataMessageLokiProfile? = nil + if proto.hasProfile { + profile = try SSKProtoDataMessageLokiProfile.parseProto(proto.profile) + } + + var closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate? = nil + if proto.hasClosedGroupUpdate { + closedGroupUpdate = try SSKProtoDataMessageClosedGroupUpdate.parseProto(proto.closedGroupUpdate) + } + + var publicChatInfo: SSKProtoPublicChatInfo? = nil + if proto.hasPublicChatInfo { + publicChatInfo = try SSKProtoPublicChatInfo.parseProto(proto.publicChatInfo) + } + + // MARK: - Begin Validation Logic for SSKProtoDataMessage - + + // MARK: - End Validation Logic for SSKProtoDataMessage - + + let result = SSKProtoDataMessage(proto: proto, + attachments: attachments, + group: group, + quote: quote, + contact: contact, + preview: preview, + profile: profile, + closedGroupUpdate: closedGroupUpdate, + publicChatInfo: publicChatInfo) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoDataMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoDataMessage.SSKProtoDataMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoDataMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoNullMessage + +@objc public class SSKProtoNullMessage: NSObject { + + // MARK: - SSKProtoNullMessageBuilder + + @objc public class func builder() -> SSKProtoNullMessageBuilder { + return SSKProtoNullMessageBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoNullMessageBuilder { + let builder = SSKProtoNullMessageBuilder() + if let _value = padding { + builder.setPadding(_value) + } + return builder + } + + @objc public class SSKProtoNullMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_NullMessage() + + @objc fileprivate override init() {} + + @objc public func setPadding(_ valueParam: Data) { + proto.padding = valueParam + } + + @objc public func build() throws -> SSKProtoNullMessage { + return try SSKProtoNullMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoNullMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_NullMessage + + @objc public var padding: Data? { + guard proto.hasPadding else { + return nil + } + return proto.padding + } + @objc public var hasPadding: Bool { + return proto.hasPadding + } + + private init(proto: SignalServiceProtos_NullMessage) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoNullMessage { + let proto = try SignalServiceProtos_NullMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_NullMessage) throws -> SSKProtoNullMessage { + // MARK: - Begin Validation Logic for SSKProtoNullMessage - + + // MARK: - End Validation Logic for SSKProtoNullMessage - + + let result = SSKProtoNullMessage(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoNullMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoNullMessage.SSKProtoNullMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoNullMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoReceiptMessage + +@objc public class SSKProtoReceiptMessage: NSObject { + + // MARK: - SSKProtoReceiptMessageType + + @objc public enum SSKProtoReceiptMessageType: Int32 { + case delivery = 0 + case read = 1 + } + + private class func SSKProtoReceiptMessageTypeWrap(_ value: SignalServiceProtos_ReceiptMessage.TypeEnum) -> SSKProtoReceiptMessageType { + switch value { + case .delivery: return .delivery + case .read: return .read + } + } + + private class func SSKProtoReceiptMessageTypeUnwrap(_ value: SSKProtoReceiptMessageType) -> SignalServiceProtos_ReceiptMessage.TypeEnum { + switch value { + case .delivery: return .delivery + case .read: return .read + } + } + + // MARK: - SSKProtoReceiptMessageBuilder + + @objc public class func builder(type: SSKProtoReceiptMessageType) -> SSKProtoReceiptMessageBuilder { + return SSKProtoReceiptMessageBuilder(type: type) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoReceiptMessageBuilder { + let builder = SSKProtoReceiptMessageBuilder(type: type) + builder.setTimestamp(timestamp) + return builder + } + + @objc public class SSKProtoReceiptMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_ReceiptMessage() + + @objc fileprivate override init() {} + + @objc fileprivate init(type: SSKProtoReceiptMessageType) { + super.init() + + setType(type) + } + + @objc public func setType(_ valueParam: SSKProtoReceiptMessageType) { + proto.type = SSKProtoReceiptMessageTypeUnwrap(valueParam) + } + + @objc public func addTimestamp(_ valueParam: UInt64) { + var items = proto.timestamp + items.append(valueParam) + proto.timestamp = items + } + + @objc public func setTimestamp(_ wrappedItems: [UInt64]) { + proto.timestamp = wrappedItems + } + + @objc public func build() throws -> SSKProtoReceiptMessage { + return try SSKProtoReceiptMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoReceiptMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_ReceiptMessage + + @objc public let type: SSKProtoReceiptMessageType + + @objc public var timestamp: [UInt64] { + return proto.timestamp + } + + private init(proto: SignalServiceProtos_ReceiptMessage, + type: SSKProtoReceiptMessageType) { + self.proto = proto + self.type = type + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoReceiptMessage { + let proto = try SignalServiceProtos_ReceiptMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_ReceiptMessage) throws -> SSKProtoReceiptMessage { + guard proto.hasType else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: type") + } + let type = SSKProtoReceiptMessageTypeWrap(proto.type) + + // MARK: - Begin Validation Logic for SSKProtoReceiptMessage - + + // MARK: - End Validation Logic for SSKProtoReceiptMessage - + + let result = SSKProtoReceiptMessage(proto: proto, + type: type) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoReceiptMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoReceiptMessage.SSKProtoReceiptMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoReceiptMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoVerified + +@objc public class SSKProtoVerified: NSObject { + + // MARK: - SSKProtoVerifiedState + + @objc public enum SSKProtoVerifiedState: Int32 { + case `default` = 0 + case verified = 1 + case unverified = 2 + } + + private class func SSKProtoVerifiedStateWrap(_ value: SignalServiceProtos_Verified.State) -> SSKProtoVerifiedState { + switch value { + case .default: return .default + case .verified: return .verified + case .unverified: return .unverified + } + } + + private class func SSKProtoVerifiedStateUnwrap(_ value: SSKProtoVerifiedState) -> SignalServiceProtos_Verified.State { + switch value { + case .default: return .default + case .verified: return .verified + case .unverified: return .unverified + } + } + + // MARK: - SSKProtoVerifiedBuilder + + @objc public class func builder(destination: String) -> SSKProtoVerifiedBuilder { + return SSKProtoVerifiedBuilder(destination: destination) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoVerifiedBuilder { + let builder = SSKProtoVerifiedBuilder(destination: destination) + if let _value = identityKey { + builder.setIdentityKey(_value) + } + if hasState { + builder.setState(state) + } + if let _value = nullMessage { + builder.setNullMessage(_value) + } + return builder + } + + @objc public class SSKProtoVerifiedBuilder: NSObject { + + private var proto = SignalServiceProtos_Verified() + + @objc fileprivate override init() {} + + @objc fileprivate init(destination: String) { + super.init() + + setDestination(destination) + } + + @objc public func setDestination(_ valueParam: String) { + proto.destination = valueParam + } + + @objc public func setIdentityKey(_ valueParam: Data) { + proto.identityKey = valueParam + } + + @objc public func setState(_ valueParam: SSKProtoVerifiedState) { + proto.state = SSKProtoVerifiedStateUnwrap(valueParam) + } + + @objc public func setNullMessage(_ valueParam: Data) { + proto.nullMessage = valueParam + } + + @objc public func build() throws -> SSKProtoVerified { + return try SSKProtoVerified.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoVerified.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_Verified + + @objc public let destination: String + + @objc public var identityKey: Data? { + guard proto.hasIdentityKey else { + return nil + } + return proto.identityKey + } + @objc public var hasIdentityKey: Bool { + return proto.hasIdentityKey + } + + @objc public var state: SSKProtoVerifiedState { + return SSKProtoVerified.SSKProtoVerifiedStateWrap(proto.state) + } + @objc public var hasState: Bool { + return proto.hasState + } + + @objc public var nullMessage: Data? { + guard proto.hasNullMessage else { + return nil + } + return proto.nullMessage + } + @objc public var hasNullMessage: Bool { + return proto.hasNullMessage + } + + private init(proto: SignalServiceProtos_Verified, + destination: String) { + self.proto = proto + self.destination = destination + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoVerified { + let proto = try SignalServiceProtos_Verified(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_Verified) throws -> SSKProtoVerified { + guard proto.hasDestination else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: destination") + } + let destination = proto.destination + + // MARK: - Begin Validation Logic for SSKProtoVerified - + + // MARK: - End Validation Logic for SSKProtoVerified - + + let result = SSKProtoVerified(proto: proto, + destination: destination) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoVerified { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoVerified.SSKProtoVerifiedBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoVerified? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageSentUnidentifiedDeliveryStatus + +@objc public class SSKProtoSyncMessageSentUnidentifiedDeliveryStatus: NSObject { + + // MARK: - SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder + + @objc public class func builder() -> SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder { + return SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder { + let builder = SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder() + if let _value = destination { + builder.setDestination(_value) + } + if hasUnidentified { + builder.setUnidentified(unidentified) + } + return builder + } + + @objc public class SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus() + + @objc fileprivate override init() {} + + @objc public func setDestination(_ valueParam: String) { + proto.destination = valueParam + } + + @objc public func setUnidentified(_ valueParam: Bool) { + proto.unidentified = valueParam + } + + @objc public func build() throws -> SSKProtoSyncMessageSentUnidentifiedDeliveryStatus { + return try SSKProtoSyncMessageSentUnidentifiedDeliveryStatus.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageSentUnidentifiedDeliveryStatus.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus + + @objc public var destination: String? { + guard proto.hasDestination else { + return nil + } + return proto.destination + } + @objc public var hasDestination: Bool { + return proto.hasDestination + } + + @objc public var unidentified: Bool { + return proto.unidentified + } + @objc public var hasUnidentified: Bool { + return proto.hasUnidentified + } + + private init(proto: SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageSentUnidentifiedDeliveryStatus { + let proto = try SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus) throws -> SSKProtoSyncMessageSentUnidentifiedDeliveryStatus { + // MARK: - Begin Validation Logic for SSKProtoSyncMessageSentUnidentifiedDeliveryStatus - + + // MARK: - End Validation Logic for SSKProtoSyncMessageSentUnidentifiedDeliveryStatus - + + let result = SSKProtoSyncMessageSentUnidentifiedDeliveryStatus(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageSentUnidentifiedDeliveryStatus { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageSentUnidentifiedDeliveryStatus.SSKProtoSyncMessageSentUnidentifiedDeliveryStatusBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageSentUnidentifiedDeliveryStatus? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageSent + +@objc public class SSKProtoSyncMessageSent: NSObject { + + // MARK: - SSKProtoSyncMessageSentBuilder + + @objc public class func builder() -> SSKProtoSyncMessageSentBuilder { + return SSKProtoSyncMessageSentBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageSentBuilder { + let builder = SSKProtoSyncMessageSentBuilder() + if let _value = destination { + builder.setDestination(_value) + } + if hasTimestamp { + builder.setTimestamp(timestamp) + } + if let _value = message { + builder.setMessage(_value) + } + if hasExpirationStartTimestamp { + builder.setExpirationStartTimestamp(expirationStartTimestamp) + } + builder.setUnidentifiedStatus(unidentifiedStatus) + if hasIsRecipientUpdate { + builder.setIsRecipientUpdate(isRecipientUpdate) + } + return builder + } + + @objc public class SSKProtoSyncMessageSentBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Sent() + + @objc fileprivate override init() {} + + @objc public func setDestination(_ valueParam: String) { + proto.destination = valueParam + } + + @objc public func setTimestamp(_ valueParam: UInt64) { + proto.timestamp = valueParam + } + + @objc public func setMessage(_ valueParam: SSKProtoDataMessage) { + proto.message = valueParam.proto + } + + @objc public func setExpirationStartTimestamp(_ valueParam: UInt64) { + proto.expirationStartTimestamp = valueParam + } + + @objc public func addUnidentifiedStatus(_ valueParam: SSKProtoSyncMessageSentUnidentifiedDeliveryStatus) { + var items = proto.unidentifiedStatus + items.append(valueParam.proto) + proto.unidentifiedStatus = items + } + + @objc public func setUnidentifiedStatus(_ wrappedItems: [SSKProtoSyncMessageSentUnidentifiedDeliveryStatus]) { + proto.unidentifiedStatus = wrappedItems.map { $0.proto } + } + + @objc public func setIsRecipientUpdate(_ valueParam: Bool) { + proto.isRecipientUpdate = valueParam + } + + @objc public func build() throws -> SSKProtoSyncMessageSent { + return try SSKProtoSyncMessageSent.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageSent.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Sent + + @objc public let message: SSKProtoDataMessage? + + @objc public let unidentifiedStatus: [SSKProtoSyncMessageSentUnidentifiedDeliveryStatus] + + @objc public var destination: String? { + guard proto.hasDestination else { + return nil + } + return proto.destination + } + @objc public var hasDestination: Bool { + return proto.hasDestination + } + + @objc public var timestamp: UInt64 { + return proto.timestamp + } + @objc public var hasTimestamp: Bool { + return proto.hasTimestamp + } + + @objc public var expirationStartTimestamp: UInt64 { + return proto.expirationStartTimestamp + } + @objc public var hasExpirationStartTimestamp: Bool { + return proto.hasExpirationStartTimestamp + } + + @objc public var isRecipientUpdate: Bool { + return proto.isRecipientUpdate + } + @objc public var hasIsRecipientUpdate: Bool { + return proto.hasIsRecipientUpdate + } + + private init(proto: SignalServiceProtos_SyncMessage.Sent, + message: SSKProtoDataMessage?, + unidentifiedStatus: [SSKProtoSyncMessageSentUnidentifiedDeliveryStatus]) { + self.proto = proto + self.message = message + self.unidentifiedStatus = unidentifiedStatus + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageSent { + let proto = try SignalServiceProtos_SyncMessage.Sent(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Sent) throws -> SSKProtoSyncMessageSent { + var message: SSKProtoDataMessage? = nil + if proto.hasMessage { + message = try SSKProtoDataMessage.parseProto(proto.message) + } + + var unidentifiedStatus: [SSKProtoSyncMessageSentUnidentifiedDeliveryStatus] = [] + unidentifiedStatus = try proto.unidentifiedStatus.map { try SSKProtoSyncMessageSentUnidentifiedDeliveryStatus.parseProto($0) } + + // MARK: - Begin Validation Logic for SSKProtoSyncMessageSent - + + // MARK: - End Validation Logic for SSKProtoSyncMessageSent - + + let result = SSKProtoSyncMessageSent(proto: proto, + message: message, + unidentifiedStatus: unidentifiedStatus) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageSent { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageSent.SSKProtoSyncMessageSentBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageSent? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageContacts + +@objc public class SSKProtoSyncMessageContacts: NSObject { + + // MARK: - SSKProtoSyncMessageContactsBuilder + + @objc public class func builder() -> SSKProtoSyncMessageContactsBuilder { + return SSKProtoSyncMessageContactsBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageContactsBuilder { + let builder = SSKProtoSyncMessageContactsBuilder() + if let _value = blob { + builder.setBlob(_value) + } + if hasIsComplete { + builder.setIsComplete(isComplete) + } + if let _value = data { + builder.setData(_value) + } + return builder + } + + @objc public class SSKProtoSyncMessageContactsBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Contacts() + + @objc fileprivate override init() {} + + @objc public func setBlob(_ valueParam: SSKProtoAttachmentPointer) { + proto.blob = valueParam.proto + } + + @objc public func setIsComplete(_ valueParam: Bool) { + proto.isComplete = valueParam + } + + @objc public func setData(_ valueParam: Data) { + proto.data = valueParam + } + + @objc public func build() throws -> SSKProtoSyncMessageContacts { + return try SSKProtoSyncMessageContacts.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageContacts.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Contacts + + @objc public let blob: SSKProtoAttachmentPointer? + + @objc public var isComplete: Bool { + return proto.isComplete + } + @objc public var hasIsComplete: Bool { + return proto.hasIsComplete + } + + @objc public var data: Data? { + guard proto.hasData else { + return nil + } + return proto.data + } + @objc public var hasData: Bool { + return proto.hasData + } + + private init(proto: SignalServiceProtos_SyncMessage.Contacts, + blob: SSKProtoAttachmentPointer?) { + self.proto = proto + self.blob = blob + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageContacts { + let proto = try SignalServiceProtos_SyncMessage.Contacts(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Contacts) throws -> SSKProtoSyncMessageContacts { + var blob: SSKProtoAttachmentPointer? = nil + if proto.hasBlob { + blob = try SSKProtoAttachmentPointer.parseProto(proto.blob) + } + + // MARK: - Begin Validation Logic for SSKProtoSyncMessageContacts - + + // MARK: - End Validation Logic for SSKProtoSyncMessageContacts - + + let result = SSKProtoSyncMessageContacts(proto: proto, + blob: blob) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageContacts { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageContacts.SSKProtoSyncMessageContactsBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageContacts? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageGroups + +@objc public class SSKProtoSyncMessageGroups: NSObject { + + // MARK: - SSKProtoSyncMessageGroupsBuilder + + @objc public class func builder() -> SSKProtoSyncMessageGroupsBuilder { + return SSKProtoSyncMessageGroupsBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageGroupsBuilder { + let builder = SSKProtoSyncMessageGroupsBuilder() + if let _value = blob { + builder.setBlob(_value) + } + if let _value = data { + builder.setData(_value) + } + return builder + } + + @objc public class SSKProtoSyncMessageGroupsBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Groups() + + @objc fileprivate override init() {} + + @objc public func setBlob(_ valueParam: SSKProtoAttachmentPointer) { + proto.blob = valueParam.proto + } + + @objc public func setData(_ valueParam: Data) { + proto.data = valueParam + } + + @objc public func build() throws -> SSKProtoSyncMessageGroups { + return try SSKProtoSyncMessageGroups.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageGroups.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Groups + + @objc public let blob: SSKProtoAttachmentPointer? + + @objc public var data: Data? { + guard proto.hasData else { + return nil + } + return proto.data + } + @objc public var hasData: Bool { + return proto.hasData + } + + private init(proto: SignalServiceProtos_SyncMessage.Groups, + blob: SSKProtoAttachmentPointer?) { + self.proto = proto + self.blob = blob + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageGroups { + let proto = try SignalServiceProtos_SyncMessage.Groups(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Groups) throws -> SSKProtoSyncMessageGroups { + var blob: SSKProtoAttachmentPointer? = nil + if proto.hasBlob { + blob = try SSKProtoAttachmentPointer.parseProto(proto.blob) + } + + // MARK: - Begin Validation Logic for SSKProtoSyncMessageGroups - + + // MARK: - End Validation Logic for SSKProtoSyncMessageGroups - + + let result = SSKProtoSyncMessageGroups(proto: proto, + blob: blob) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageGroups { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageGroups.SSKProtoSyncMessageGroupsBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageGroups? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageOpenGroupDetails + +@objc public class SSKProtoSyncMessageOpenGroupDetails: NSObject { + + // MARK: - SSKProtoSyncMessageOpenGroupDetailsBuilder + + @objc public class func builder(url: String, channelID: UInt64) -> SSKProtoSyncMessageOpenGroupDetailsBuilder { + return SSKProtoSyncMessageOpenGroupDetailsBuilder(url: url, channelID: channelID) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageOpenGroupDetailsBuilder { + let builder = SSKProtoSyncMessageOpenGroupDetailsBuilder(url: url, channelID: channelID) + return builder + } + + @objc public class SSKProtoSyncMessageOpenGroupDetailsBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.OpenGroupDetails() + + @objc fileprivate override init() {} + + @objc fileprivate init(url: String, channelID: UInt64) { + super.init() + + setUrl(url) + setChannelID(channelID) + } + + @objc public func setUrl(_ valueParam: String) { + proto.url = valueParam + } + + @objc public func setChannelID(_ valueParam: UInt64) { + proto.channelID = valueParam + } + + @objc public func build() throws -> SSKProtoSyncMessageOpenGroupDetails { + return try SSKProtoSyncMessageOpenGroupDetails.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageOpenGroupDetails.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.OpenGroupDetails + + @objc public let url: String + + @objc public let channelID: UInt64 + + private init(proto: SignalServiceProtos_SyncMessage.OpenGroupDetails, + url: String, + channelID: UInt64) { + self.proto = proto + self.url = url + self.channelID = channelID + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageOpenGroupDetails { + let proto = try SignalServiceProtos_SyncMessage.OpenGroupDetails(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.OpenGroupDetails) throws -> SSKProtoSyncMessageOpenGroupDetails { + guard proto.hasURL else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: url") + } + let url = proto.url + + guard proto.hasChannelID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: channelID") + } + let channelID = proto.channelID + + // MARK: - Begin Validation Logic for SSKProtoSyncMessageOpenGroupDetails - + + // MARK: - End Validation Logic for SSKProtoSyncMessageOpenGroupDetails - + + let result = SSKProtoSyncMessageOpenGroupDetails(proto: proto, + url: url, + channelID: channelID) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageOpenGroupDetails { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageOpenGroupDetails.SSKProtoSyncMessageOpenGroupDetailsBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageOpenGroupDetails? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageBlocked + +@objc public class SSKProtoSyncMessageBlocked: NSObject { + + // MARK: - SSKProtoSyncMessageBlockedBuilder + + @objc public class func builder() -> SSKProtoSyncMessageBlockedBuilder { + return SSKProtoSyncMessageBlockedBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageBlockedBuilder { + let builder = SSKProtoSyncMessageBlockedBuilder() + builder.setNumbers(numbers) + builder.setGroupIds(groupIds) + return builder + } + + @objc public class SSKProtoSyncMessageBlockedBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Blocked() + + @objc fileprivate override init() {} + + @objc public func addNumbers(_ valueParam: String) { + var items = proto.numbers + items.append(valueParam) + proto.numbers = items + } + + @objc public func setNumbers(_ wrappedItems: [String]) { + proto.numbers = wrappedItems + } + + @objc public func addGroupIds(_ valueParam: Data) { + var items = proto.groupIds + items.append(valueParam) + proto.groupIds = items + } + + @objc public func setGroupIds(_ wrappedItems: [Data]) { + proto.groupIds = wrappedItems + } + + @objc public func build() throws -> SSKProtoSyncMessageBlocked { + return try SSKProtoSyncMessageBlocked.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageBlocked.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Blocked + + @objc public var numbers: [String] { + return proto.numbers + } + + @objc public var groupIds: [Data] { + return proto.groupIds + } + + private init(proto: SignalServiceProtos_SyncMessage.Blocked) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageBlocked { + let proto = try SignalServiceProtos_SyncMessage.Blocked(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Blocked) throws -> SSKProtoSyncMessageBlocked { + // MARK: - Begin Validation Logic for SSKProtoSyncMessageBlocked - + + // MARK: - End Validation Logic for SSKProtoSyncMessageBlocked - + + let result = SSKProtoSyncMessageBlocked(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageBlocked { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageBlocked.SSKProtoSyncMessageBlockedBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageBlocked? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageRequest + +@objc public class SSKProtoSyncMessageRequest: NSObject { + + // MARK: - SSKProtoSyncMessageRequestType + + @objc public enum SSKProtoSyncMessageRequestType: Int32 { + case unknown = 0 + case contacts = 1 + case groups = 2 + case blocked = 3 + case configuration = 4 + } + + private class func SSKProtoSyncMessageRequestTypeWrap(_ value: SignalServiceProtos_SyncMessage.Request.TypeEnum) -> SSKProtoSyncMessageRequestType { + switch value { + case .unknown: return .unknown + case .contacts: return .contacts + case .groups: return .groups + case .blocked: return .blocked + case .configuration: return .configuration + } + } + + private class func SSKProtoSyncMessageRequestTypeUnwrap(_ value: SSKProtoSyncMessageRequestType) -> SignalServiceProtos_SyncMessage.Request.TypeEnum { + switch value { + case .unknown: return .unknown + case .contacts: return .contacts + case .groups: return .groups + case .blocked: return .blocked + case .configuration: return .configuration + } + } + + // MARK: - SSKProtoSyncMessageRequestBuilder + + @objc public class func builder(type: SSKProtoSyncMessageRequestType) -> SSKProtoSyncMessageRequestBuilder { + return SSKProtoSyncMessageRequestBuilder(type: type) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageRequestBuilder { + let builder = SSKProtoSyncMessageRequestBuilder(type: type) + return builder + } + + @objc public class SSKProtoSyncMessageRequestBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Request() + + @objc fileprivate override init() {} + + @objc fileprivate init(type: SSKProtoSyncMessageRequestType) { + super.init() + + setType(type) + } + + @objc public func setType(_ valueParam: SSKProtoSyncMessageRequestType) { + proto.type = SSKProtoSyncMessageRequestTypeUnwrap(valueParam) + } + + @objc public func build() throws -> SSKProtoSyncMessageRequest { + return try SSKProtoSyncMessageRequest.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageRequest.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Request + + @objc public let type: SSKProtoSyncMessageRequestType + + private init(proto: SignalServiceProtos_SyncMessage.Request, + type: SSKProtoSyncMessageRequestType) { + self.proto = proto + self.type = type + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageRequest { + let proto = try SignalServiceProtos_SyncMessage.Request(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Request) throws -> SSKProtoSyncMessageRequest { + guard proto.hasType else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: type") + } + let type = SSKProtoSyncMessageRequestTypeWrap(proto.type) + + // MARK: - Begin Validation Logic for SSKProtoSyncMessageRequest - + + // MARK: - End Validation Logic for SSKProtoSyncMessageRequest - + + let result = SSKProtoSyncMessageRequest(proto: proto, + type: type) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageRequest { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageRequest.SSKProtoSyncMessageRequestBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageRequest? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageRead + +@objc public class SSKProtoSyncMessageRead: NSObject { + + // MARK: - SSKProtoSyncMessageReadBuilder + + @objc public class func builder(sender: String, timestamp: UInt64) -> SSKProtoSyncMessageReadBuilder { + return SSKProtoSyncMessageReadBuilder(sender: sender, timestamp: timestamp) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageReadBuilder { + let builder = SSKProtoSyncMessageReadBuilder(sender: sender, timestamp: timestamp) + return builder + } + + @objc public class SSKProtoSyncMessageReadBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Read() + + @objc fileprivate override init() {} + + @objc fileprivate init(sender: String, timestamp: UInt64) { + super.init() + + setSender(sender) + setTimestamp(timestamp) + } + + @objc public func setSender(_ valueParam: String) { + proto.sender = valueParam + } + + @objc public func setTimestamp(_ valueParam: UInt64) { + proto.timestamp = valueParam + } + + @objc public func build() throws -> SSKProtoSyncMessageRead { + return try SSKProtoSyncMessageRead.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageRead.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Read + + @objc public let sender: String + + @objc public let timestamp: UInt64 + + private init(proto: SignalServiceProtos_SyncMessage.Read, + sender: String, + timestamp: UInt64) { + self.proto = proto + self.sender = sender + self.timestamp = timestamp + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageRead { + let proto = try SignalServiceProtos_SyncMessage.Read(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Read) throws -> SSKProtoSyncMessageRead { + guard proto.hasSender else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sender") + } + let sender = proto.sender + + guard proto.hasTimestamp else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: timestamp") + } + let timestamp = proto.timestamp + + // MARK: - Begin Validation Logic for SSKProtoSyncMessageRead - + + // MARK: - End Validation Logic for SSKProtoSyncMessageRead - + + let result = SSKProtoSyncMessageRead(proto: proto, + sender: sender, + timestamp: timestamp) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageRead { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageRead.SSKProtoSyncMessageReadBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageRead? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessageConfiguration + +@objc public class SSKProtoSyncMessageConfiguration: NSObject { + + // MARK: - SSKProtoSyncMessageConfigurationBuilder + + @objc public class func builder() -> SSKProtoSyncMessageConfigurationBuilder { + return SSKProtoSyncMessageConfigurationBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageConfigurationBuilder { + let builder = SSKProtoSyncMessageConfigurationBuilder() + if hasReadReceipts { + builder.setReadReceipts(readReceipts) + } + if hasUnidentifiedDeliveryIndicators { + builder.setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators) + } + if hasTypingIndicators { + builder.setTypingIndicators(typingIndicators) + } + if hasLinkPreviews { + builder.setLinkPreviews(linkPreviews) + } + return builder + } + + @objc public class SSKProtoSyncMessageConfigurationBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage.Configuration() + + @objc fileprivate override init() {} + + @objc public func setReadReceipts(_ valueParam: Bool) { + proto.readReceipts = valueParam + } + + @objc public func setUnidentifiedDeliveryIndicators(_ valueParam: Bool) { + proto.unidentifiedDeliveryIndicators = valueParam + } + + @objc public func setTypingIndicators(_ valueParam: Bool) { + proto.typingIndicators = valueParam + } + + @objc public func setLinkPreviews(_ valueParam: Bool) { + proto.linkPreviews = valueParam + } + + @objc public func build() throws -> SSKProtoSyncMessageConfiguration { + return try SSKProtoSyncMessageConfiguration.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessageConfiguration.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage.Configuration + + @objc public var readReceipts: Bool { + return proto.readReceipts + } + @objc public var hasReadReceipts: Bool { + return proto.hasReadReceipts + } + + @objc public var unidentifiedDeliveryIndicators: Bool { + return proto.unidentifiedDeliveryIndicators + } + @objc public var hasUnidentifiedDeliveryIndicators: Bool { + return proto.hasUnidentifiedDeliveryIndicators + } + + @objc public var typingIndicators: Bool { + return proto.typingIndicators + } + @objc public var hasTypingIndicators: Bool { + return proto.hasTypingIndicators + } + + @objc public var linkPreviews: Bool { + return proto.linkPreviews + } + @objc public var hasLinkPreviews: Bool { + return proto.hasLinkPreviews + } + + private init(proto: SignalServiceProtos_SyncMessage.Configuration) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessageConfiguration { + let proto = try SignalServiceProtos_SyncMessage.Configuration(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage.Configuration) throws -> SSKProtoSyncMessageConfiguration { + // MARK: - Begin Validation Logic for SSKProtoSyncMessageConfiguration - + + // MARK: - End Validation Logic for SSKProtoSyncMessageConfiguration - + + let result = SSKProtoSyncMessageConfiguration(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessageConfiguration { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessageConfiguration.SSKProtoSyncMessageConfigurationBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessageConfiguration? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoSyncMessage + +@objc public class SSKProtoSyncMessage: NSObject { + + // MARK: - SSKProtoSyncMessageBuilder + + @objc public class func builder() -> SSKProtoSyncMessageBuilder { + return SSKProtoSyncMessageBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoSyncMessageBuilder { + let builder = SSKProtoSyncMessageBuilder() + if let _value = sent { + builder.setSent(_value) + } + if let _value = contacts { + builder.setContacts(_value) + } + if let _value = groups { + builder.setGroups(_value) + } + if let _value = request { + builder.setRequest(_value) + } + builder.setRead(read) + if let _value = blocked { + builder.setBlocked(_value) + } + if let _value = verified { + builder.setVerified(_value) + } + if let _value = configuration { + builder.setConfiguration(_value) + } + if let _value = padding { + builder.setPadding(_value) + } + builder.setOpenGroups(openGroups) + return builder + } + + @objc public class SSKProtoSyncMessageBuilder: NSObject { + + private var proto = SignalServiceProtos_SyncMessage() + + @objc fileprivate override init() {} + + @objc public func setSent(_ valueParam: SSKProtoSyncMessageSent) { + proto.sent = valueParam.proto + } + + @objc public func setContacts(_ valueParam: SSKProtoSyncMessageContacts) { + proto.contacts = valueParam.proto + } + + @objc public func setGroups(_ valueParam: SSKProtoSyncMessageGroups) { + proto.groups = valueParam.proto + } + + @objc public func setRequest(_ valueParam: SSKProtoSyncMessageRequest) { + proto.request = valueParam.proto + } + + @objc public func addRead(_ valueParam: SSKProtoSyncMessageRead) { + var items = proto.read + items.append(valueParam.proto) + proto.read = items + } + + @objc public func setRead(_ wrappedItems: [SSKProtoSyncMessageRead]) { + proto.read = wrappedItems.map { $0.proto } + } + + @objc public func setBlocked(_ valueParam: SSKProtoSyncMessageBlocked) { + proto.blocked = valueParam.proto + } + + @objc public func setVerified(_ valueParam: SSKProtoVerified) { + proto.verified = valueParam.proto + } + + @objc public func setConfiguration(_ valueParam: SSKProtoSyncMessageConfiguration) { + proto.configuration = valueParam.proto + } + + @objc public func setPadding(_ valueParam: Data) { + proto.padding = valueParam + } + + @objc public func addOpenGroups(_ valueParam: SSKProtoSyncMessageOpenGroupDetails) { + var items = proto.openGroups + items.append(valueParam.proto) + proto.openGroups = items + } + + @objc public func setOpenGroups(_ wrappedItems: [SSKProtoSyncMessageOpenGroupDetails]) { + proto.openGroups = wrappedItems.map { $0.proto } + } + + @objc public func build() throws -> SSKProtoSyncMessage { + return try SSKProtoSyncMessage.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoSyncMessage.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_SyncMessage + + @objc public let sent: SSKProtoSyncMessageSent? + + @objc public let contacts: SSKProtoSyncMessageContacts? + + @objc public let groups: SSKProtoSyncMessageGroups? + + @objc public let request: SSKProtoSyncMessageRequest? + + @objc public let read: [SSKProtoSyncMessageRead] + + @objc public let blocked: SSKProtoSyncMessageBlocked? + + @objc public let verified: SSKProtoVerified? + + @objc public let configuration: SSKProtoSyncMessageConfiguration? + + @objc public let openGroups: [SSKProtoSyncMessageOpenGroupDetails] + + @objc public var padding: Data? { + guard proto.hasPadding else { + return nil + } + return proto.padding + } + @objc public var hasPadding: Bool { + return proto.hasPadding + } + + private init(proto: SignalServiceProtos_SyncMessage, + sent: SSKProtoSyncMessageSent?, + contacts: SSKProtoSyncMessageContacts?, + groups: SSKProtoSyncMessageGroups?, + request: SSKProtoSyncMessageRequest?, + read: [SSKProtoSyncMessageRead], + blocked: SSKProtoSyncMessageBlocked?, + verified: SSKProtoVerified?, + configuration: SSKProtoSyncMessageConfiguration?, + openGroups: [SSKProtoSyncMessageOpenGroupDetails]) { + self.proto = proto + self.sent = sent + self.contacts = contacts + self.groups = groups + self.request = request + self.read = read + self.blocked = blocked + self.verified = verified + self.configuration = configuration + self.openGroups = openGroups + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoSyncMessage { + let proto = try SignalServiceProtos_SyncMessage(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_SyncMessage) throws -> SSKProtoSyncMessage { + var sent: SSKProtoSyncMessageSent? = nil + if proto.hasSent { + sent = try SSKProtoSyncMessageSent.parseProto(proto.sent) + } + + var contacts: SSKProtoSyncMessageContacts? = nil + if proto.hasContacts { + contacts = try SSKProtoSyncMessageContacts.parseProto(proto.contacts) + } + + var groups: SSKProtoSyncMessageGroups? = nil + if proto.hasGroups { + groups = try SSKProtoSyncMessageGroups.parseProto(proto.groups) + } + + var request: SSKProtoSyncMessageRequest? = nil + if proto.hasRequest { + request = try SSKProtoSyncMessageRequest.parseProto(proto.request) + } + + var read: [SSKProtoSyncMessageRead] = [] + read = try proto.read.map { try SSKProtoSyncMessageRead.parseProto($0) } + + var blocked: SSKProtoSyncMessageBlocked? = nil + if proto.hasBlocked { + blocked = try SSKProtoSyncMessageBlocked.parseProto(proto.blocked) + } + + var verified: SSKProtoVerified? = nil + if proto.hasVerified { + verified = try SSKProtoVerified.parseProto(proto.verified) + } + + var configuration: SSKProtoSyncMessageConfiguration? = nil + if proto.hasConfiguration { + configuration = try SSKProtoSyncMessageConfiguration.parseProto(proto.configuration) + } + + var openGroups: [SSKProtoSyncMessageOpenGroupDetails] = [] + openGroups = try proto.openGroups.map { try SSKProtoSyncMessageOpenGroupDetails.parseProto($0) } + + // MARK: - Begin Validation Logic for SSKProtoSyncMessage - + + // MARK: - End Validation Logic for SSKProtoSyncMessage - + + let result = SSKProtoSyncMessage(proto: proto, + sent: sent, + contacts: contacts, + groups: groups, + request: request, + read: read, + blocked: blocked, + verified: verified, + configuration: configuration, + openGroups: openGroups) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoSyncMessage { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoSyncMessage.SSKProtoSyncMessageBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoSyncMessage? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoAttachmentPointer + +@objc public class SSKProtoAttachmentPointer: NSObject { + + // MARK: - SSKProtoAttachmentPointerFlags + + @objc public enum SSKProtoAttachmentPointerFlags: Int32 { + case voiceMessage = 1 + } + + private class func SSKProtoAttachmentPointerFlagsWrap(_ value: SignalServiceProtos_AttachmentPointer.Flags) -> SSKProtoAttachmentPointerFlags { + switch value { + case .voiceMessage: return .voiceMessage + } + } + + private class func SSKProtoAttachmentPointerFlagsUnwrap(_ value: SSKProtoAttachmentPointerFlags) -> SignalServiceProtos_AttachmentPointer.Flags { + switch value { + case .voiceMessage: return .voiceMessage + } + } + + // MARK: - SSKProtoAttachmentPointerBuilder + + @objc public class func builder(id: UInt64) -> SSKProtoAttachmentPointerBuilder { + return SSKProtoAttachmentPointerBuilder(id: id) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoAttachmentPointerBuilder { + let builder = SSKProtoAttachmentPointerBuilder(id: id) + if let _value = contentType { + builder.setContentType(_value) + } + if let _value = key { + builder.setKey(_value) + } + if hasSize { + builder.setSize(size) + } + if let _value = thumbnail { + builder.setThumbnail(_value) + } + if let _value = digest { + builder.setDigest(_value) + } + if let _value = fileName { + builder.setFileName(_value) + } + if hasFlags { + builder.setFlags(flags) + } + if hasWidth { + builder.setWidth(width) + } + if hasHeight { + builder.setHeight(height) + } + if let _value = caption { + builder.setCaption(_value) + } + if let _value = url { + builder.setUrl(_value) + } + return builder + } + + @objc public class SSKProtoAttachmentPointerBuilder: NSObject { + + private var proto = SignalServiceProtos_AttachmentPointer() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: UInt64) { + super.init() + + setId(id) + } + + @objc public func setId(_ valueParam: UInt64) { + proto.id = valueParam + } + + @objc public func setContentType(_ valueParam: String) { + proto.contentType = valueParam + } + + @objc public func setKey(_ valueParam: Data) { + proto.key = valueParam + } + + @objc public func setSize(_ valueParam: UInt32) { + proto.size = valueParam + } + + @objc public func setThumbnail(_ valueParam: Data) { + proto.thumbnail = valueParam + } + + @objc public func setDigest(_ valueParam: Data) { + proto.digest = valueParam + } + + @objc public func setFileName(_ valueParam: String) { + proto.fileName = valueParam + } + + @objc public func setFlags(_ valueParam: UInt32) { + proto.flags = valueParam + } + + @objc public func setWidth(_ valueParam: UInt32) { + proto.width = valueParam + } + + @objc public func setHeight(_ valueParam: UInt32) { + proto.height = valueParam + } + + @objc public func setCaption(_ valueParam: String) { + proto.caption = valueParam + } + + @objc public func setUrl(_ valueParam: String) { + proto.url = valueParam + } + + @objc public func build() throws -> SSKProtoAttachmentPointer { + return try SSKProtoAttachmentPointer.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoAttachmentPointer.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_AttachmentPointer + + @objc public let id: UInt64 + + @objc public var contentType: String? { + guard proto.hasContentType else { + return nil + } + return proto.contentType + } + @objc public var hasContentType: Bool { + return proto.hasContentType + } + + @objc public var key: Data? { + guard proto.hasKey else { + return nil + } + return proto.key + } + @objc public var hasKey: Bool { + return proto.hasKey + } + + @objc public var size: UInt32 { + return proto.size + } + @objc public var hasSize: Bool { + return proto.hasSize + } + + @objc public var thumbnail: Data? { + guard proto.hasThumbnail else { + return nil + } + return proto.thumbnail + } + @objc public var hasThumbnail: Bool { + return proto.hasThumbnail + } + + @objc public var digest: Data? { + guard proto.hasDigest else { + return nil + } + return proto.digest + } + @objc public var hasDigest: Bool { + return proto.hasDigest + } + + @objc public var fileName: String? { + guard proto.hasFileName else { + return nil + } + return proto.fileName + } + @objc public var hasFileName: Bool { + return proto.hasFileName + } + + @objc public var flags: UInt32 { + return proto.flags + } + @objc public var hasFlags: Bool { + return proto.hasFlags + } + + @objc public var width: UInt32 { + return proto.width + } + @objc public var hasWidth: Bool { + return proto.hasWidth + } + + @objc public var height: UInt32 { + return proto.height + } + @objc public var hasHeight: Bool { + return proto.hasHeight + } + + @objc public var caption: String? { + guard proto.hasCaption else { + return nil + } + return proto.caption + } + @objc public var hasCaption: Bool { + return proto.hasCaption + } + + @objc public var url: String? { + guard proto.hasURL else { + return nil + } + return proto.url + } + @objc public var hasURL: Bool { + return proto.hasURL + } + + private init(proto: SignalServiceProtos_AttachmentPointer, + id: UInt64) { + self.proto = proto + self.id = id + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoAttachmentPointer { + let proto = try SignalServiceProtos_AttachmentPointer(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_AttachmentPointer) throws -> SSKProtoAttachmentPointer { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + // MARK: - Begin Validation Logic for SSKProtoAttachmentPointer - + + // MARK: - End Validation Logic for SSKProtoAttachmentPointer - + + let result = SSKProtoAttachmentPointer(proto: proto, + id: id) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoAttachmentPointer { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoAttachmentPointer.SSKProtoAttachmentPointerBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoAttachmentPointer? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoGroupContext + +@objc public class SSKProtoGroupContext: NSObject { + + // MARK: - SSKProtoGroupContextType + + @objc public enum SSKProtoGroupContextType: Int32 { + case unknown = 0 + case update = 1 + case deliver = 2 + case quit = 3 + case requestInfo = 4 + } + + private class func SSKProtoGroupContextTypeWrap(_ value: SignalServiceProtos_GroupContext.TypeEnum) -> SSKProtoGroupContextType { + switch value { + case .unknown: return .unknown + case .update: return .update + case .deliver: return .deliver + case .quit: return .quit + case .requestInfo: return .requestInfo + } + } + + private class func SSKProtoGroupContextTypeUnwrap(_ value: SSKProtoGroupContextType) -> SignalServiceProtos_GroupContext.TypeEnum { + switch value { + case .unknown: return .unknown + case .update: return .update + case .deliver: return .deliver + case .quit: return .quit + case .requestInfo: return .requestInfo + } + } + + // MARK: - SSKProtoGroupContextBuilder + + @objc public class func builder(id: Data, type: SSKProtoGroupContextType) -> SSKProtoGroupContextBuilder { + return SSKProtoGroupContextBuilder(id: id, type: type) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoGroupContextBuilder { + let builder = SSKProtoGroupContextBuilder(id: id, type: type) + if let _value = name { + builder.setName(_value) + } + builder.setMembers(members) + if let _value = avatar { + builder.setAvatar(_value) + } + builder.setAdmins(admins) + return builder + } + + @objc public class SSKProtoGroupContextBuilder: NSObject { + + private var proto = SignalServiceProtos_GroupContext() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: Data, type: SSKProtoGroupContextType) { + super.init() + + setId(id) + setType(type) + } + + @objc public func setId(_ valueParam: Data) { + proto.id = valueParam + } + + @objc public func setType(_ valueParam: SSKProtoGroupContextType) { + proto.type = SSKProtoGroupContextTypeUnwrap(valueParam) + } + + @objc public func setName(_ valueParam: String) { + proto.name = valueParam + } + + @objc public func addMembers(_ valueParam: String) { + var items = proto.members + items.append(valueParam) + proto.members = items + } + + @objc public func setMembers(_ wrappedItems: [String]) { + proto.members = wrappedItems + } + + @objc public func setAvatar(_ valueParam: SSKProtoAttachmentPointer) { + proto.avatar = valueParam.proto + } + + @objc public func addAdmins(_ valueParam: String) { + var items = proto.admins + items.append(valueParam) + proto.admins = items + } + + @objc public func setAdmins(_ wrappedItems: [String]) { + proto.admins = wrappedItems + } + + @objc public func build() throws -> SSKProtoGroupContext { + return try SSKProtoGroupContext.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoGroupContext.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_GroupContext + + @objc public let id: Data + + @objc public let type: SSKProtoGroupContextType + + @objc public let avatar: SSKProtoAttachmentPointer? + + @objc public var name: String? { + guard proto.hasName else { + return nil + } + return proto.name + } + @objc public var hasName: Bool { + return proto.hasName + } + + @objc public var members: [String] { + return proto.members + } + + @objc public var admins: [String] { + return proto.admins + } + + private init(proto: SignalServiceProtos_GroupContext, + id: Data, + type: SSKProtoGroupContextType, + avatar: SSKProtoAttachmentPointer?) { + self.proto = proto + self.id = id + self.type = type + self.avatar = avatar + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoGroupContext { + let proto = try SignalServiceProtos_GroupContext(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_GroupContext) throws -> SSKProtoGroupContext { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + guard proto.hasType else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: type") + } + let type = SSKProtoGroupContextTypeWrap(proto.type) + + var avatar: SSKProtoAttachmentPointer? = nil + if proto.hasAvatar { + avatar = try SSKProtoAttachmentPointer.parseProto(proto.avatar) + } + + // MARK: - Begin Validation Logic for SSKProtoGroupContext - + + // MARK: - End Validation Logic for SSKProtoGroupContext - + + let result = SSKProtoGroupContext(proto: proto, + id: id, + type: type, + avatar: avatar) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoGroupContext { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoGroupContext.SSKProtoGroupContextBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoGroupContext? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoContactDetailsAvatar + +@objc public class SSKProtoContactDetailsAvatar: NSObject { + + // MARK: - SSKProtoContactDetailsAvatarBuilder + + @objc public class func builder() -> SSKProtoContactDetailsAvatarBuilder { + return SSKProtoContactDetailsAvatarBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoContactDetailsAvatarBuilder { + let builder = SSKProtoContactDetailsAvatarBuilder() + if let _value = contentType { + builder.setContentType(_value) + } + if hasLength { + builder.setLength(length) + } + return builder + } + + @objc public class SSKProtoContactDetailsAvatarBuilder: NSObject { + + private var proto = SignalServiceProtos_ContactDetails.Avatar() + + @objc fileprivate override init() {} + + @objc public func setContentType(_ valueParam: String) { + proto.contentType = valueParam + } + + @objc public func setLength(_ valueParam: UInt32) { + proto.length = valueParam + } + + @objc public func build() throws -> SSKProtoContactDetailsAvatar { + return try SSKProtoContactDetailsAvatar.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoContactDetailsAvatar.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_ContactDetails.Avatar + + @objc public var contentType: String? { + guard proto.hasContentType else { + return nil + } + return proto.contentType + } + @objc public var hasContentType: Bool { + return proto.hasContentType + } + + @objc public var length: UInt32 { + return proto.length + } + @objc public var hasLength: Bool { + return proto.hasLength + } + + private init(proto: SignalServiceProtos_ContactDetails.Avatar) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoContactDetailsAvatar { + let proto = try SignalServiceProtos_ContactDetails.Avatar(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_ContactDetails.Avatar) throws -> SSKProtoContactDetailsAvatar { + // MARK: - Begin Validation Logic for SSKProtoContactDetailsAvatar - + + // MARK: - End Validation Logic for SSKProtoContactDetailsAvatar - + + let result = SSKProtoContactDetailsAvatar(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoContactDetailsAvatar { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoContactDetailsAvatar.SSKProtoContactDetailsAvatarBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoContactDetailsAvatar? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoContactDetails + +@objc public class SSKProtoContactDetails: NSObject { + + // MARK: - SSKProtoContactDetailsBuilder + + @objc public class func builder(number: String) -> SSKProtoContactDetailsBuilder { + return SSKProtoContactDetailsBuilder(number: number) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoContactDetailsBuilder { + let builder = SSKProtoContactDetailsBuilder(number: number) + if let _value = name { + builder.setName(_value) + } + if let _value = avatar { + builder.setAvatar(_value) + } + if let _value = color { + builder.setColor(_value) + } + if let _value = verified { + builder.setVerified(_value) + } + if let _value = profileKey { + builder.setProfileKey(_value) + } + if hasBlocked { + builder.setBlocked(blocked) + } + if hasExpireTimer { + builder.setExpireTimer(expireTimer) + } + if let _value = nickname { + builder.setNickname(_value) + } + return builder + } + + @objc public class SSKProtoContactDetailsBuilder: NSObject { + + private var proto = SignalServiceProtos_ContactDetails() + + @objc fileprivate override init() {} + + @objc fileprivate init(number: String) { + super.init() + + setNumber(number) + } + + @objc public func setNumber(_ valueParam: String) { + proto.number = valueParam + } + + @objc public func setName(_ valueParam: String) { + proto.name = valueParam + } + + @objc public func setAvatar(_ valueParam: SSKProtoContactDetailsAvatar) { + proto.avatar = valueParam.proto + } + + @objc public func setColor(_ valueParam: String) { + proto.color = valueParam + } + + @objc public func setVerified(_ valueParam: SSKProtoVerified) { + proto.verified = valueParam.proto + } + + @objc public func setProfileKey(_ valueParam: Data) { + proto.profileKey = valueParam + } + + @objc public func setBlocked(_ valueParam: Bool) { + proto.blocked = valueParam + } + + @objc public func setExpireTimer(_ valueParam: UInt32) { + proto.expireTimer = valueParam + } + + @objc public func setNickname(_ valueParam: String) { + proto.nickname = valueParam + } + + @objc public func build() throws -> SSKProtoContactDetails { + return try SSKProtoContactDetails.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoContactDetails.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_ContactDetails + + @objc public let number: String + + @objc public let avatar: SSKProtoContactDetailsAvatar? + + @objc public let verified: SSKProtoVerified? + + @objc public var name: String? { + guard proto.hasName else { + return nil + } + return proto.name + } + @objc public var hasName: Bool { + return proto.hasName + } + + @objc public var color: String? { + guard proto.hasColor else { + return nil + } + return proto.color + } + @objc public var hasColor: Bool { + return proto.hasColor + } + + @objc public var profileKey: Data? { + guard proto.hasProfileKey else { + return nil + } + return proto.profileKey + } + @objc public var hasProfileKey: Bool { + return proto.hasProfileKey + } + + @objc public var blocked: Bool { + return proto.blocked + } + @objc public var hasBlocked: Bool { + return proto.hasBlocked + } + + @objc public var expireTimer: UInt32 { + return proto.expireTimer + } + @objc public var hasExpireTimer: Bool { + return proto.hasExpireTimer + } + + @objc public var nickname: String? { + guard proto.hasNickname else { + return nil + } + return proto.nickname + } + @objc public var hasNickname: Bool { + return proto.hasNickname + } + + private init(proto: SignalServiceProtos_ContactDetails, + number: String, + avatar: SSKProtoContactDetailsAvatar?, + verified: SSKProtoVerified?) { + self.proto = proto + self.number = number + self.avatar = avatar + self.verified = verified + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoContactDetails { + let proto = try SignalServiceProtos_ContactDetails(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_ContactDetails) throws -> SSKProtoContactDetails { + guard proto.hasNumber else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: number") + } + let number = proto.number + + var avatar: SSKProtoContactDetailsAvatar? = nil + if proto.hasAvatar { + avatar = try SSKProtoContactDetailsAvatar.parseProto(proto.avatar) + } + + var verified: SSKProtoVerified? = nil + if proto.hasVerified { + verified = try SSKProtoVerified.parseProto(proto.verified) + } + + // MARK: - Begin Validation Logic for SSKProtoContactDetails - + + // MARK: - End Validation Logic for SSKProtoContactDetails - + + let result = SSKProtoContactDetails(proto: proto, + number: number, + avatar: avatar, + verified: verified) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoContactDetails { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoContactDetails.SSKProtoContactDetailsBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoContactDetails? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoGroupDetailsAvatar + +@objc public class SSKProtoGroupDetailsAvatar: NSObject { + + // MARK: - SSKProtoGroupDetailsAvatarBuilder + + @objc public class func builder() -> SSKProtoGroupDetailsAvatarBuilder { + return SSKProtoGroupDetailsAvatarBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoGroupDetailsAvatarBuilder { + let builder = SSKProtoGroupDetailsAvatarBuilder() + if let _value = contentType { + builder.setContentType(_value) + } + if hasLength { + builder.setLength(length) + } + return builder + } + + @objc public class SSKProtoGroupDetailsAvatarBuilder: NSObject { + + private var proto = SignalServiceProtos_GroupDetails.Avatar() + + @objc fileprivate override init() {} + + @objc public func setContentType(_ valueParam: String) { + proto.contentType = valueParam + } + + @objc public func setLength(_ valueParam: UInt32) { + proto.length = valueParam + } + + @objc public func build() throws -> SSKProtoGroupDetailsAvatar { + return try SSKProtoGroupDetailsAvatar.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoGroupDetailsAvatar.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_GroupDetails.Avatar + + @objc public var contentType: String? { + guard proto.hasContentType else { + return nil + } + return proto.contentType + } + @objc public var hasContentType: Bool { + return proto.hasContentType + } + + @objc public var length: UInt32 { + return proto.length + } + @objc public var hasLength: Bool { + return proto.hasLength + } + + private init(proto: SignalServiceProtos_GroupDetails.Avatar) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoGroupDetailsAvatar { + let proto = try SignalServiceProtos_GroupDetails.Avatar(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_GroupDetails.Avatar) throws -> SSKProtoGroupDetailsAvatar { + // MARK: - Begin Validation Logic for SSKProtoGroupDetailsAvatar - + + // MARK: - End Validation Logic for SSKProtoGroupDetailsAvatar - + + let result = SSKProtoGroupDetailsAvatar(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoGroupDetailsAvatar { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoGroupDetailsAvatar.SSKProtoGroupDetailsAvatarBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoGroupDetailsAvatar? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoGroupDetails + +@objc public class SSKProtoGroupDetails: NSObject { + + // MARK: - SSKProtoGroupDetailsBuilder + + @objc public class func builder(id: Data) -> SSKProtoGroupDetailsBuilder { + return SSKProtoGroupDetailsBuilder(id: id) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoGroupDetailsBuilder { + let builder = SSKProtoGroupDetailsBuilder(id: id) + if let _value = name { + builder.setName(_value) + } + builder.setMembers(members) + if let _value = avatar { + builder.setAvatar(_value) + } + if hasActive { + builder.setActive(active) + } + if hasExpireTimer { + builder.setExpireTimer(expireTimer) + } + if let _value = color { + builder.setColor(_value) + } + if hasBlocked { + builder.setBlocked(blocked) + } + builder.setAdmins(admins) + return builder + } + + @objc public class SSKProtoGroupDetailsBuilder: NSObject { + + private var proto = SignalServiceProtos_GroupDetails() + + @objc fileprivate override init() {} + + @objc fileprivate init(id: Data) { + super.init() + + setId(id) + } + + @objc public func setId(_ valueParam: Data) { + proto.id = valueParam + } + + @objc public func setName(_ valueParam: String) { + proto.name = valueParam + } + + @objc public func addMembers(_ valueParam: String) { + var items = proto.members + items.append(valueParam) + proto.members = items + } + + @objc public func setMembers(_ wrappedItems: [String]) { + proto.members = wrappedItems + } + + @objc public func setAvatar(_ valueParam: SSKProtoGroupDetailsAvatar) { + proto.avatar = valueParam.proto + } + + @objc public func setActive(_ valueParam: Bool) { + proto.active = valueParam + } + + @objc public func setExpireTimer(_ valueParam: UInt32) { + proto.expireTimer = valueParam + } + + @objc public func setColor(_ valueParam: String) { + proto.color = valueParam + } + + @objc public func setBlocked(_ valueParam: Bool) { + proto.blocked = valueParam + } + + @objc public func addAdmins(_ valueParam: String) { + var items = proto.admins + items.append(valueParam) + proto.admins = items + } + + @objc public func setAdmins(_ wrappedItems: [String]) { + proto.admins = wrappedItems + } + + @objc public func build() throws -> SSKProtoGroupDetails { + return try SSKProtoGroupDetails.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoGroupDetails.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_GroupDetails + + @objc public let id: Data + + @objc public let avatar: SSKProtoGroupDetailsAvatar? + + @objc public var name: String? { + guard proto.hasName else { + return nil + } + return proto.name + } + @objc public var hasName: Bool { + return proto.hasName + } + + @objc public var members: [String] { + return proto.members + } + + @objc public var active: Bool { + return proto.active + } + @objc public var hasActive: Bool { + return proto.hasActive + } + + @objc public var expireTimer: UInt32 { + return proto.expireTimer + } + @objc public var hasExpireTimer: Bool { + return proto.hasExpireTimer + } + + @objc public var color: String? { + guard proto.hasColor else { + return nil + } + return proto.color + } + @objc public var hasColor: Bool { + return proto.hasColor + } + + @objc public var blocked: Bool { + return proto.blocked + } + @objc public var hasBlocked: Bool { + return proto.hasBlocked + } + + @objc public var admins: [String] { + return proto.admins + } + + private init(proto: SignalServiceProtos_GroupDetails, + id: Data, + avatar: SSKProtoGroupDetailsAvatar?) { + self.proto = proto + self.id = id + self.avatar = avatar + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoGroupDetails { + let proto = try SignalServiceProtos_GroupDetails(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_GroupDetails) throws -> SSKProtoGroupDetails { + guard proto.hasID else { + throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: id") + } + let id = proto.id + + var avatar: SSKProtoGroupDetailsAvatar? = nil + if proto.hasAvatar { + avatar = try SSKProtoGroupDetailsAvatar.parseProto(proto.avatar) + } + + // MARK: - Begin Validation Logic for SSKProtoGroupDetails - + + // MARK: - End Validation Logic for SSKProtoGroupDetails - + + let result = SSKProtoGroupDetails(proto: proto, + id: id, + avatar: avatar) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoGroupDetails { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoGroupDetails.SSKProtoGroupDetailsBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoGroupDetails? { + return try! self.build() + } +} + +#endif + +// MARK: - SSKProtoPublicChatInfo + +@objc public class SSKProtoPublicChatInfo: NSObject { + + // MARK: - SSKProtoPublicChatInfoBuilder + + @objc public class func builder() -> SSKProtoPublicChatInfoBuilder { + return SSKProtoPublicChatInfoBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SSKProtoPublicChatInfoBuilder { + let builder = SSKProtoPublicChatInfoBuilder() + if hasServerID { + builder.setServerID(serverID) + } + return builder + } + + @objc public class SSKProtoPublicChatInfoBuilder: NSObject { + + private var proto = SignalServiceProtos_PublicChatInfo() + + @objc fileprivate override init() {} + + @objc public func setServerID(_ valueParam: UInt64) { + proto.serverID = valueParam + } + + @objc public func build() throws -> SSKProtoPublicChatInfo { + return try SSKProtoPublicChatInfo.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SSKProtoPublicChatInfo.parseProto(proto).serializedData() + } + } + + fileprivate let proto: SignalServiceProtos_PublicChatInfo + + @objc public var serverID: UInt64 { + return proto.serverID + } + @objc public var hasServerID: Bool { + return proto.hasServerID + } + + private init(proto: SignalServiceProtos_PublicChatInfo) { + self.proto = proto + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SSKProtoPublicChatInfo { + let proto = try SignalServiceProtos_PublicChatInfo(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: SignalServiceProtos_PublicChatInfo) throws -> SSKProtoPublicChatInfo { + // MARK: - Begin Validation Logic for SSKProtoPublicChatInfo - + + // MARK: - End Validation Logic for SSKProtoPublicChatInfo - + + let result = SSKProtoPublicChatInfo(proto: proto) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SSKProtoPublicChatInfo { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SSKProtoPublicChatInfo.SSKProtoPublicChatInfoBuilder { + @objc public func buildIgnoringErrors() -> SSKProtoPublicChatInfo? { + return try! self.build() + } +} + +#endif diff --git a/SignalUtilitiesKit/SSKProtoEnvelope+Conversion.swift b/SignalUtilitiesKit/SSKProtoEnvelope+Conversion.swift new file mode 100644 index 000000000..614dff7a7 --- /dev/null +++ b/SignalUtilitiesKit/SSKProtoEnvelope+Conversion.swift @@ -0,0 +1,15 @@ + +extension SSKProtoEnvelope { + + static func from(_ json: JSON) -> SSKProtoEnvelope? { + guard let base64EncodedData = json["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { + print("[Loki] Failed to decode data for message: \(json).") + return nil + } + guard let result = try? MessageWrapper.unwrap(data: data) else { + print("[Loki] Failed to unwrap data for message: \(json).") + return nil + } + return result + } +} diff --git a/SignalUtilitiesKit/SSKProtoPrekeyBundleMessage+Loki.swift b/SignalUtilitiesKit/SSKProtoPrekeyBundleMessage+Loki.swift new file mode 100644 index 000000000..387bf4415 --- /dev/null +++ b/SignalUtilitiesKit/SSKProtoPrekeyBundleMessage+Loki.swift @@ -0,0 +1,23 @@ + +@objc public extension SSKProtoPrekeyBundleMessage { + + @objc(builderFromPreKeyBundle:) + public static func builder(from preKeyBundle: PreKeyBundle) -> SSKProtoPrekeyBundleMessageBuilder { + let builder = self.builder() + builder.setIdentityKey(preKeyBundle.identityKey) + builder.setDeviceID(UInt32(preKeyBundle.deviceId)) + builder.setPrekeyID(UInt32(preKeyBundle.preKeyId)) + builder.setPrekey(preKeyBundle.preKeyPublic) + builder.setSignedKeyID(UInt32(preKeyBundle.signedPreKeyId)) + builder.setSignedKey(preKeyBundle.signedPreKeyPublic) + builder.setSignature(preKeyBundle.signedPreKeySignature) + return builder + } + + @objc(getPreKeyBundleWithTransaction:) + public func getPreKeyBundle(with transaction: YapDatabaseReadWriteTransaction) -> PreKeyBundle? { + let registrationId = TSAccountManager.sharedInstance().getOrGenerateRegistrationId(transaction) + return PreKeyBundle(registrationId: Int32(registrationId), deviceId: Int32(deviceID), preKeyId: Int32(prekeyID), preKeyPublic: prekey, + signedPreKeyPublic: signedKey, signedPreKeyId: Int32(signedKeyID), signedPreKeySignature: signature, identityKey: identityKey) + } +} diff --git a/SignalUtilitiesKit/SSKWebSocket.swift b/SignalUtilitiesKit/SSKWebSocket.swift new file mode 100644 index 000000000..b72a680ad --- /dev/null +++ b/SignalUtilitiesKit/SSKWebSocket.swift @@ -0,0 +1,185 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import Starscream + +@objc +public enum SSKWebSocketState: UInt { + case open, connecting, disconnected +} + +@objc +public class SSKWebSocketError: NSObject, CustomNSError { + + init(underlyingError: Starscream.WSError) { + self.underlyingError = underlyingError + } + + // MARK: - CustomNSError + + @objc + public static let errorDomain = "SignalServiceKit.SSKWebSocketError" + + public var errorUserInfo: [String: Any] { + return [ + type(of: self).kStatusCodeKey: underlyingError.code, + NSUnderlyingErrorKey: (underlyingError as NSError) + ] + } + + // MARK: - + + @objc + public static let kStatusCodeKey = "SSKWebSocketErrorStatusCode" + + let underlyingError: Starscream.WSError +} + +@objc +public protocol SSKWebSocket { + + @objc + var delegate: SSKWebSocketDelegate? { get set } + + @objc + var state: SSKWebSocketState { get } + + @objc + func connect() + + @objc + func disconnect() + + @objc(writeData:error:) + func write(data: Data) throws + + @objc + func writePing() throws +} + +@objc +public protocol SSKWebSocketDelegate: class { + func websocketDidConnect(socket: SSKWebSocket) + + func websocketDidDisconnect(socket: SSKWebSocket, error: Error?) + + func websocketDidReceiveData(socket: SSKWebSocket, data: Data) + + @objc optional func websocketDidReceiveMessage(socket: SSKWebSocket, text: String) +} + +@objc +public class SSKWebSocketManager: NSObject { + + @objc + public class func buildSocket(request: URLRequest) -> SSKWebSocket { + return SSKWebSocketImpl(request: request) + } +} + +class SSKWebSocketImpl: SSKWebSocket { + + private let socket: Starscream.WebSocket + + init(request: URLRequest) { + let socket = WebSocket(request: request) + + socket.disableSSLCertValidation = true + socket.socketSecurityLevel = StreamSocketSecurityLevel.tlSv1_2 + let security = SSLSecurity(certs: [TextSecureCertificate()], usePublicKeys: false) + security.validateEntireChain = false + socket.security = security + + // TODO cipher suite selection + // socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256] + + self.socket = socket + + socket.delegate = self + } + + // MARK: - SSKWebSocket + + weak var delegate: SSKWebSocketDelegate? + + var hasEverConnected = false + var state: SSKWebSocketState { + if socket.isConnected { + return .open + } + + if hasEverConnected { + return .disconnected + } + + return .connecting + } + + func connect() { + socket.connect() + } + + func disconnect() { + socket.disconnect() + } + + func write(data: Data) throws { + socket.write(data: data) + } + + func writePing() throws { + socket.write(ping: Data()) + } +} + +extension SSKWebSocketImpl: WebSocketDelegate { + func websocketDidConnect(socket: WebSocketClient) { + hasEverConnected = true + delegate?.websocketDidConnect(socket: self) + } + + func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { + let websocketError: Error? + switch error { + case let wsError as WSError: + websocketError = SSKWebSocketError(underlyingError: wsError) + case let nsError as NSError: + // Assert that error is either a Starscream.WSError or an OS level networking error + if #available(iOS 10, *) { + let networkDownCode = 50 + assert(nsError.domain == "NSPOSIXErrorDomain" && nsError.code == networkDownCode) + } else { + assert(nsError.domain == kCFErrorDomainCFNetwork as String) + } + websocketError = error + default: + assert(error == nil, "unexpected error type: \(String(describing: error))") + websocketError = error + } + + delegate?.websocketDidDisconnect(socket: self, error: websocketError) + } + + func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + if let websocketDidReceiveMessage = self.delegate?.websocketDidReceiveMessage { + websocketDidReceiveMessage(self, text) + } + } + + func websocketDidReceiveData(socket: WebSocketClient, data: Data) { + delegate?.websocketDidReceiveData(socket: self, data: data) + } +} + +private func TextSecureCertificate() -> SSLCert { + let data = SSKTextSecureServiceCertificateData() + return SSLCert(data: data) +} + +private extension StreamSocketSecurityLevel { + static var tlSv1_2: StreamSocketSecurityLevel { + return StreamSocketSecurityLevel(rawValue: "kCFStreamSocketSecurityLevelTLSv1_2") + } +} diff --git a/SignalUtilitiesKit/SessionManagementProtocol.swift b/SignalUtilitiesKit/SessionManagementProtocol.swift new file mode 100644 index 000000000..17fdabd77 --- /dev/null +++ b/SignalUtilitiesKit/SessionManagementProtocol.swift @@ -0,0 +1,223 @@ +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(LKSessionManagementProtocol) +public final class SessionManagementProtocol : NSObject { + + internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + + // MARK: - General + + @objc(createPreKeys) + public static func createPreKeys() { + // We don't generate new pre keys here like Signal does. + // This is because we need the records to be linked to a contact since we don't have a central server. + // It's done automatically when we generate a pre key bundle to send to a contact (generatePreKeyBundleForContact:). + // You can use getOrCreatePreKeyForContact: to generate one if needed. + let signedPreKeyRecord = storage.generateRandomSignedRecord() + signedPreKeyRecord.markAsAcceptedByService() + storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id) + print("[Loki] Pre keys created successfully.") + } + + @objc(refreshSignedPreKey) + public static func refreshSignedPreKey() { + // We don't generate new pre keys here like Signal does. + // This is because we need the records to be linked to a contact since we don't have a central server. + // It's done automatically when we generate a pre key bundle to send to a contact (generatePreKeyBundleForContact:). + // You can use getOrCreatePreKeyForContact: to generate one if needed. + guard storage.currentSignedPrekeyId() == nil else { return } + let signedPreKeyRecord = storage.generateRandomSignedRecord() + signedPreKeyRecord.markAsAcceptedByService() + storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id) + TSPreKeyManager.clearPreKeyUpdateFailureCount() + TSPreKeyManager.clearSignedPreKeyRecords() + print("[Loki] Signed pre key refreshed successfully.") + } + + @objc(rotateSignedPreKey) + public static func rotateSignedPreKey() { + // This is identical to what Signal does, except that it doesn't upload the signed pre key + // to a server. + let signedPreKeyRecord = storage.generateRandomSignedRecord() + signedPreKeyRecord.markAsAcceptedByService() + storage.storeSignedPreKey(signedPreKeyRecord.id, signedPreKeyRecord: signedPreKeyRecord) + storage.setCurrentSignedPrekeyId(signedPreKeyRecord.id) + TSPreKeyManager.clearPreKeyUpdateFailureCount() + TSPreKeyManager.clearSignedPreKeyRecords() + print("[Loki] Signed pre key rotated successfully.") + } + + // MARK: - Sending + + @objc(isSessionRequiredForMessage:recipientID:transaction:) + public static func isSessionRequired(for message: TSOutgoingMessage, recipientID: String, transaction: YapDatabaseReadWriteTransaction) -> Bool { + if SharedSenderKeysImplementation.shared.isClosedGroup(recipientID) { + return false + } else { + return !shouldUseFallbackEncryption(for: message, recipientID: recipientID, transaction: transaction) + } + } + + @objc(shouldUseFallbackEncryptionForMessage:recipientID:transaction:) + public static func shouldUseFallbackEncryption(for message: TSOutgoingMessage, recipientID: String, transaction: YapDatabaseReadWriteTransaction) -> Bool { + if SharedSenderKeysImplementation.shared.isClosedGroup(recipientID) { return false } + else if message is SessionRequestMessage { return true } + else if message is EndSessionMessage { return true } + else if let message = message as? DeviceLinkMessage, message.kind == .request { return true } + else if message is OWSOutgoingNullMessage { return false } + return !storage.containsSession(recipientID, deviceId: Int32(OWSDevicePrimaryDeviceId), protocolContext: transaction) + } + + private static func hasSentSessionRequestExpired(for publicKey: String) -> Bool { + let timestamp = Storage.getSessionRequestSentTimestamp(for: publicKey) + let expiration = timestamp + TTLUtilities.getTTL(for: .sessionRequest) + return NSDate.ows_millisecondTimeStamp() > expiration + } + + @objc(sendSessionRequestIfNeededToPublicKey:transaction:) + public static func sendSessionRequestIfNeeded(to publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + // It's never necessary to establish a session with self + guard publicKey != getUserHexEncodedPublicKey() else { return } + // Check that we don't already have a session + let hasSession = storage.containsSession(publicKey, deviceId: Int32(OWSDevicePrimaryDeviceId), protocolContext: transaction) + guard !hasSession else { return } + // Check that we didn't already send a session request + let hasSentSessionRequest = (Storage.getSessionRequestSentTimestamp(for: publicKey) > 0) + let hasSentSessionRequestExpired = SessionManagementProtocol.hasSentSessionRequestExpired(for: publicKey) + if hasSentSessionRequestExpired { + Storage.setSessionRequestSentTimestamp(for: publicKey, to: 0, using: transaction) + } + guard !hasSentSessionRequest || hasSentSessionRequestExpired else { return } + // Create the thread if needed + let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction) + thread.save(with: transaction) + // Send the session request + print("[Loki] Sending session request to: \(publicKey).") + Storage.setSessionRequestSentTimestamp(for: publicKey, to: NSDate.ows_millisecondTimeStamp(), using: transaction) + let sessionRequestMessage = SessionRequestMessage(thread: thread) + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + messageSenderJobQueue.add(message: sessionRequestMessage, transaction: transaction) + } + + @objc(sendNullMessageToPublicKey:transaction:) + public static func sendNullMessage(to publicKey: String, in transaction: YapDatabaseReadWriteTransaction) { + let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction) + thread.save(with: transaction) + let nullMessage = OWSOutgoingNullMessage(outgoingMessageWithTimestamp: NSDate.millisecondTimestamp(), in: thread, messageBody: nil, + attachmentIds: [], expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false, groupMetaMessage: .unspecified, quotedMessage: nil, + contactShare: nil, linkPreview: nil) + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + messageSenderJobQueue.add(message: nullMessage, transaction: transaction) + } + + /// - Note: Deprecated. + /// + /// Only relevant for closed groups that don't use shared sender keys. + @objc(shouldIgnoreMissingPreKeyBundleExceptionForMessage:to:) + public static func shouldIgnoreMissingPreKeyBundleException(for message: TSOutgoingMessage, to hexEncodedPublicKey: String) -> Bool { + // When a closed group is created, members try to establish sessions with eachother in the background through + // session requests. Until ALL users those session requests were sent to have come online, stored the pre key + // bundles contained in the session requests and replied with background messages to finalize the session + // creation, a given user won't be able to successfully send a message to all members of a group. This check + // is so that until we can do better on this front the user at least won't see this as an error in the UI. + guard let groupThread = message.thread as? TSGroupThread else { return false } + return groupThread.groupModel.groupType == .closedGroup && !groupThread.usesSharedSenderKeys + } + + @objc(startSessionResetInThread:transaction:) + public static func startSessionReset(in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { + // Check preconditions + guard let thread = thread as? TSContactThread else { + return print("[Loki] Can't restore session for non contact thread.") + } + // Send end session messages to the devices requiring session restoration + let devices = thread.sessionRestoreDevices // TODO: Rename this to something that reads better + for device in devices { + guard ECKeyPair.isValidHexEncodedPublicKey(candidate: device) else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: device, transaction: transaction) + thread.save(with: transaction) + let endSessionMessage = EndSessionMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread) + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + messageSenderJobQueue.add(message: endSessionMessage, transaction: transaction) + } + thread.removeAllSessionRestoreDevices(with: transaction) + // Notify the user + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetInProgress) + infoMessage.save(with: transaction) + // Update the session reset status + thread.sessionResetStatus = .initiated + thread.save(with: transaction) + } + + // MARK: - Receiving + + @objc(handleDecryptionError:forPublicKey:transaction:) + public static func handleDecryptionError(_ errorMessage: TSErrorMessage, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + let type = errorMessage.errorType + let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey + let thread = TSContactThread.getOrCreateThread(withContactId: masterPublicKey, transaction: transaction) + let restorationTimeInMs = UInt64(storage.getRestorationTime() * 1000) + // Show the session reset prompt upon certain errors + switch type { + case .noSession, .invalidMessage, .invalidKeyException: + if restorationTimeInMs > errorMessage.timestamp { + // Automatically rebuild session after restoration + sendSessionRequestIfNeeded(to: publicKey, using: transaction) + } else { + // Store the source device's public key in case it was a secondary device + thread.addSessionRestoreDevice(publicKey, transaction: transaction) + } + default: break + } + } + + private static func shouldProcessSessionRequest(from publicKey: String, at timestamp: UInt64) -> Bool { + let sentTimestamp = Storage.getSessionRequestSentTimestamp(for: publicKey) + let processedTimestamp = Storage.getSessionRequestProcessedTimestamp(for: publicKey) + let restorationTimestamp = UInt64(storage.getRestorationTime() * 1000) + return timestamp > sentTimestamp && timestamp > processedTimestamp && timestamp > restorationTimestamp + } + + @objc(handlePreKeyBundleMessageIfNeeded:wrappedIn:transaction:) + public static func handlePreKeyBundleMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) { + let publicKey = envelope.source! // Set during UD decryption + guard let preKeyBundleMessage = protoContent.prekeyBundleMessage else { return } + print("[Loki] Received a pre key bundle message from: \(publicKey).") + guard let preKeyBundle = preKeyBundleMessage.getPreKeyBundle(with: transaction) else { + return print("[Loki] Couldn't parse pre key bundle received from: \(publicKey).") + } + if !shouldProcessSessionRequest(from: publicKey, at: envelope.timestamp) { + return print("[Loki] Ignoring session request from: \(publicKey).") + } + storage.setPreKeyBundle(preKeyBundle, forContact: publicKey, transaction: transaction) + Storage.setSessionRequestProcessedTimestamp(for: publicKey, to: NSDate.ows_millisecondTimeStamp(), using: transaction) + sendNullMessage(to: publicKey, in: transaction) + } + + @objc(handleEndSessionMessageReceivedInThread:using:) + public static func handleEndSessionMessageReceived(in thread: TSContactThread, using transaction: YapDatabaseReadWriteTransaction) { + let publicKey = thread.contactIdentifier() + print("[Loki] End session message received from: \(publicKey).") + // Notify the user + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetInProgress) + infoMessage.save(with: transaction) + // Archive all sessions + storage.archiveAllSessions(forContact: publicKey, protocolContext: transaction) + // Update the session reset status + thread.sessionResetStatus = .requestReceived + thread.save(with: transaction) + // Send a null message + sendNullMessage(to: publicKey, in: transaction) + } +} diff --git a/SignalUtilitiesKit/SessionMetaProtocol.swift b/SignalUtilitiesKit/SessionMetaProtocol.swift new file mode 100644 index 000000000..dde938992 --- /dev/null +++ b/SignalUtilitiesKit/SessionMetaProtocol.swift @@ -0,0 +1,134 @@ +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. + +/// See [Receipts, Transcripts & Typing Indicators](https://github.com/loki-project/session-protocol-docs/wiki/Receipts,-Transcripts-&-Typing-Indicators) for more information. +@objc(LKSessionMetaProtocol) +public final class SessionMetaProtocol : NSObject { + + internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + + // MARK: - Sending + + // MARK: Message Destination(s) + @objc public static func getDestinationsForOutgoingSyncMessage() -> NSMutableSet { + return NSMutableSet(set: [ getUserHexEncodedPublicKey() ]) // return NSMutableSet(set: MultiDeviceProtocol.getUserLinkedDevices()) + } + + @objc(getDestinationsForOutgoingGroupMessage:inThread:) + public static func getDestinations(for outgoingGroupMessage: TSOutgoingMessage, in thread: TSThread) -> NSMutableSet { + guard let thread = thread as? TSGroupThread else { preconditionFailure("Can't get destinations for group message in non-group thread.") } + var result: Set = [] + if thread.isPublicChat { + storage.dbReadConnection.read { transaction in + if let openGroup = LokiDatabaseUtilities.getPublicChat(for: thread.uniqueId!, in: transaction) { + result = [ openGroup.server ] // Aim the message at the open group server + } else { + // Should never occur + } + } + } else { + if let groupThread = thread as? TSGroupThread, groupThread.usesSharedSenderKeys { + let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupThread.groupModel.groupId) + result = [ groupPublicKey ] + } else { + result = Set(outgoingGroupMessage.sendingRecipientIds()) + .intersection(thread.groupModel.groupMemberIds) + .subtracting([ getUserHexEncodedPublicKey() ]) // .subtracting(MultiDeviceProtocol.getUserLinkedDevices()) + } + } + return NSMutableSet(set: result) + } + + // MARK: Note to Self + @objc(isThreadNoteToSelf:) + public static func isThreadNoteToSelf(_ thread: TSThread) -> Bool { + guard let thread = thread as? TSContactThread else { return false } + return thread.contactIdentifier() == getUserHexEncodedPublicKey() + /* + var isNoteToSelf = false + storage.dbReadConnection.read { transaction in + isNoteToSelf = LokiDatabaseUtilities.isUserLinkedDevice(thread.contactIdentifier(), transaction: transaction) + } + return isNoteToSelf + */ + } + + // MARK: Transcripts + @objc(shouldSendTranscriptForMessage:inThread:) + public static func shouldSendTranscript(for message: TSOutgoingMessage, in thread: TSThread) -> Bool { + guard message.shouldSyncTranscript() else { return false } + let isOpenGroupMessage = (thread as? TSGroupThread)?.isPublicChat == true + let wouldSignalRequireTranscript = (AreRecipientUpdatesEnabled() || !message.hasSyncedTranscript) + guard wouldSignalRequireTranscript && !isOpenGroupMessage else { return false } + return false + /* + var usesMultiDevice = false + storage.dbReadConnection.read { transaction in + usesMultiDevice = !storage.getDeviceLinks(for: getUserHexEncodedPublicKey(), in: transaction).isEmpty + || UserDefaults.standard[.masterHexEncodedPublicKey] != nil + } + return usesMultiDevice + */ + } + + // MARK: Typing Indicators + /// Invoked only if typing indicators are enabled in the settings. Provides an opportunity + /// to avoid sending them if certain conditions are met. + @objc(shouldSendTypingIndicatorInThread:) + public static func shouldSendTypingIndicator(in thread: TSThread) -> Bool { + return !thread.isGroupThread() && thread.numberOfInteractions() > 0 + } + + // MARK: Receipts + @objc(shouldSendReceiptInThread:) + public static func shouldSendReceipt(in thread: TSThread) -> Bool { + return !thread.isGroupThread() + } + + // MARK: - Receiving + + @objc(isErrorMessageFromBeforeRestoration:) + public static func isErrorMessageFromBeforeRestoration(_ errorMessage: TSErrorMessage) -> Bool { + let restorationTimeInMs = UInt64(storage.getRestorationTime() * 1000) + return errorMessage.timestamp < restorationTimeInMs + } + + @objc(shouldSkipMessageDecryptResult:wrappedIn:) + public static func shouldSkipMessageDecryptResult(_ result: OWSMessageDecryptResult, wrappedIn envelope: SSKProtoEnvelope) -> Bool { + return result.source == getUserHexEncodedPublicKey() + /* + if result.source == getUserHexEncodedPublicKey() { return true } + var isLinkedDevice = false + Storage.read { transaction in + isLinkedDevice = LokiDatabaseUtilities.isUserLinkedDevice(result.source, transaction: transaction) + } + return isLinkedDevice && envelope.type == .closedGroupCiphertext + */ + } + + @objc(updateDisplayNameIfNeededForPublicKey:using:transaction:) + public static func updateDisplayNameIfNeeded(for publicKey: String, using dataMessage: SSKProtoDataMessage, in transaction: YapDatabaseReadWriteTransaction) { + guard let profile = dataMessage.profile, let displayName = profile.displayName, !displayName.isEmpty else { return } + let profileManager = SSKEnvironment.shared.profileManager + profileManager.updateProfileForContact(withID: publicKey, displayName: displayName, with: transaction) + } + + @objc(updateProfileKeyIfNeededForPublicKey:using:) + public static func updateProfileKeyIfNeeded(for publicKey: String, using dataMessage: SSKProtoDataMessage) { + guard dataMessage.hasProfileKey, let profileKey = dataMessage.profileKey else { return } + guard profileKey.count == kAES256_KeyByteLength else { + return print("[Loki] Unexpected profile key size: \(profileKey.count).") + } + let profilePictureURL = dataMessage.profile?.profilePicture + let profileManager = SSKEnvironment.shared.profileManager + profileManager.setProfileKeyData(profileKey, forRecipientId: publicKey, avatarURL: profilePictureURL) + } +} diff --git a/SignalUtilitiesKit/SessionRequestMessage.swift b/SignalUtilitiesKit/SessionRequestMessage.swift new file mode 100644 index 000000000..b612b1608 --- /dev/null +++ b/SignalUtilitiesKit/SessionRequestMessage.swift @@ -0,0 +1,51 @@ + +@objc(LKSessionRequestMessage) +internal final class SessionRequestMessage : TSOutgoingMessage { + + @objc internal override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .sessionRequest)) } + + @objc internal override func shouldBeSaved() -> Bool { return false } + @objc internal override func shouldSyncTranscript() -> Bool { return false } + + @objc internal init(thread: TSThread) { + super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "", + attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false, + groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + required init(dictionary: [String:Any]) throws { + try super.init(dictionary: dictionary) + } + + override func prepareCustomContentBuilder(_ recipient: SignalRecipient) -> Any? { + guard let contentBuilder = super.prepareCustomContentBuilder(recipient) as? SSKProtoContent.SSKProtoContentBuilder else { return nil } + // Attach a null message + let nullMessageBuilder = SSKProtoNullMessage.builder() + let paddingSize = UInt.random(in: 0..<512) // random(in:) uses the system's default random generator, which is cryptographically secure + let padding = Cryptography.generateRandomBytes(paddingSize) + nullMessageBuilder.setPadding(padding) + do { + let nullMessage = try nullMessageBuilder.build() + contentBuilder.setNullMessage(nullMessage) + } catch { + owsFailDebug("Failed to build session request message for: \(recipient.recipientId()) due to error: \(error).") + return nil + } + // Generate a pre key bundle for the recipient and attach it + let preKeyBundle = OWSPrimaryStorage.shared().generatePreKeyBundle(forContact: recipient.recipientId()) + let preKeyBundleMessageBuilder = SSKProtoPrekeyBundleMessage.builder(from: preKeyBundle) + do { + let preKeyBundleMessage = try preKeyBundleMessageBuilder.build() + contentBuilder.setPrekeyBundleMessage(preKeyBundleMessage) + } catch { + owsFailDebug("Failed to build session request message for: \(recipient.recipientId()) due to error: \(error).") + return nil + } + // Return + return contentBuilder + } +} diff --git a/SignalUtilitiesKit/SharedSenderKeysImplementation.swift b/SignalUtilitiesKit/SharedSenderKeysImplementation.swift new file mode 100644 index 000000000..4a3371bf4 --- /dev/null +++ b/SignalUtilitiesKit/SharedSenderKeysImplementation.swift @@ -0,0 +1,220 @@ +import CryptoSwift +import PromiseKit + + +@objc(LKSharedSenderKeysImplementation) +public final class SharedSenderKeysImplementation : NSObject { + private static let gcmTagSize: UInt = 16 + private static let ivSize: UInt = 12 + + // MARK: Documentation + // A quick overview of how shared sender key based closed groups work: + // + // • When a user creates a group, they generate a key pair for the group along with a ratchet for + // every member of the group. They bundle this together with some other group info such as the group + // name in a `ClosedGroupUpdateMessage` and send that using established channels to every member of + // the group. Note that because a user can only pick from their existing contacts when selecting + // the group members they shouldn't need to establish sessions before being able to send the + // `ClosedGroupUpdateMessage`. Another way to optimize the performance of the group creation process + // is to batch fetch the device links of all members involved ahead of time, rather than letting + // the sending pipeline do it separately for every user the `ClosedGroupUpdateMessage` is sent to. + // • After the group is created, every user polls for the public key associated with the group. + // • Upon receiving a `ClosedGroupUpdateMessage` of type `.new`, a user sends session requests to all + // other members of the group they don't yet have a session with for reasons outlined below. + // • When a user sends a message they step their ratchet and use the resulting message key to encrypt + // the message. + // • When another user receives that message, they step the ratchet associated with the sender and + // use the resulting message key to decrypt the message. + // • When a user leaves or is kicked from a group, all members must generate new ratchets to ensure that + // removed users can't decrypt messages going forward. To this end every user deletes all ratchets + // associated with the group in question upon receiving a group update message that indicates that + // a user left. They then generate a new ratchet for themselves and send it out to all members of + // the group (again fetching device links ahead of time). The user should already have established + // sessions with all other members at this point because of the behavior outlined a few points above. + // • When a user adds a new member to the group, they generate a ratchet for that new member and + // send that bundled in a `ClosedGroupUpdateMessage` to the group. They send a + // `ClosedGroupUpdateMessage` with the newly generated ratchet but also the existing ratchets of + // every other member of the group to the user that joined. + + // MARK: Ratcheting Error + public enum RatchetingError : LocalizedError { + case loadingFailed(groupPublicKey: String, senderPublicKey: String) + case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String) + case generic + + public var errorDescription: String? { + switch self { + case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)." + case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)." + case .generic: return "An error occurred" + } + } + } + + // MARK: Initialization + @objc public static let shared = SharedSenderKeysImplementation() + + private override init() { } + + // MARK: Private/Internal API + internal func generateRatchet(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> ClosedGroupRatchet { + let rootChainKey = Data.getSecureRandomData(ofSize: 32)!.toHexString() + let ratchet = ClosedGroupRatchet(chainKey: rootChainKey, keyIndex: 0, messageKeys: []) + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction) + return ratchet + } + + private func step(_ ratchet: ClosedGroupRatchet) throws -> ClosedGroupRatchet { + let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ]) + let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ]) + let nextKeyIndex = ratchet.keyIndex + 1 + let messageKeys = ratchet.messageKeys + [ nextMessageKey.toHexString() ] + return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: messageKeys) + } + + /// - Note: Sync. Don't call from the main thread. + private func stepRatchetOnce(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet { + #if DEBUG + assert(!Thread.isMainThread) + #endif + guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey) else { + let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) + print("[Loki] \(error.errorDescription!)") + throw error + } + do { + let result = try step(ratchet) + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction) + return result + } catch { + print("[Loki] Couldn't step ratchet due to error: \(error).") + throw error + } + } + + /// - Note: Sync. Don't call from the main thread. + private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> ClosedGroupRatchet { + #if DEBUG + assert(!Thread.isMainThread) + #endif + let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current + guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else { + let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) + print("[Loki] \(error.errorDescription!)") + throw error + } + if targetKeyIndex < ratchet.keyIndex { + // There's no need to advance the ratchet if this is invoked for an old key index + guard ratchet.messageKeys.count > targetKeyIndex else { + let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) + print("[Loki] \(error.errorDescription!)") + throw error + } + return ratchet + } else { + var currentKeyIndex = ratchet.keyIndex + var result = ratchet + while currentKeyIndex < targetKeyIndex { + do { + result = try step(result) + currentKeyIndex = result.keyIndex + } catch { + print("[Loki] Couldn't step ratchet due to error: \(error).") + throw error + } + } + let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: collection, using: transaction) + return result + } + } + + // MARK: Public API + @objc(encrypt:forGroupWithPublicKey:senderPublicKey:protocolContext:error:) + public func encrypt(_ plaintext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, protocolContext: Any) throws -> [Any] { + let transaction = protocolContext as! YapDatabaseReadWriteTransaction + let (ivAndCiphertext, keyIndex) = try encrypt(plaintext, for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + return [ ivAndCiphertext, NSNumber(value: keyIndex) ] + } + + public func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> (ivAndCiphertext: Data, keyIndex: UInt) { + let ratchet: ClosedGroupRatchet + do { + ratchet = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + } catch { + // FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more + // convenient because there's an easy way to get the sender public key from here. + if case RatchetingError.loadingFailed(_, _) = error { + ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + } + throw error + } + let iv = Data.getSecureRandomData(ofSize: SharedSenderKeysImplementation.ivSize)! + let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined) + let messageKey = ratchet.messageKeys.last! + let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding) + let ciphertext = try aes.encrypt(plaintext.bytes) + return (ivAndCiphertext: iv + Data(bytes: ciphertext), ratchet.keyIndex) + } + + @objc(decrypt:forGroupWithPublicKey:senderPublicKey:keyIndex:protocolContext:error:) + public func decrypt(_ ivAndCiphertext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, protocolContext: Any) throws -> Data { + let transaction = protocolContext as! YapDatabaseReadWriteTransaction + return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction) + } + + public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> Data { + let ratchet: ClosedGroupRatchet + do { + ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction, isRetry: isRetry) + } catch { + if !isRetry { + return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true) + } else { + // FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more + // convenient because there's an easy way to get the sender public key from here. + if case RatchetingError.loadingFailed(_, _) = error { + ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + } + throw error + } + } + let iv = ivAndCiphertext[0.. 16 { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order + lastNMessageKeys = [String](messageKeys[messageKeys.index(messageKeys.endIndex, offsetBy: -16).. Bool { + return Storage.getUserClosedGroupPublicKeys().contains(publicKey) + } + + public func getKeyPair(forGroupWithPublicKey groupPublicKey: String) -> ECKeyPair { + let privateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey)! + return ECKeyPair(publicKey: Data(hex: groupPublicKey.removing05PrefixIfNeeded()), privateKey: Data(hex: privateKey)) + } +} diff --git a/SignalUtilitiesKit/SignalAccount.h b/SignalUtilitiesKit/SignalAccount.h new file mode 100644 index 000000000..5b416f9a4 --- /dev/null +++ b/SignalUtilitiesKit/SignalAccount.h @@ -0,0 +1,46 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class Contact; +@class SignalRecipient; +@class YapDatabaseReadTransaction; + +// This class represents a single valid Signal account. +// +// * Contacts with multiple signal accounts will correspond to +// multiple instances of SignalAccount. +// * For non-contacts, the contact property will be nil. +@interface SignalAccount : TSYapDatabaseObject + +// An E164 value identifying the signal account. +// +// This is the key property of this class and it +// will always be non-null. +@property (nonatomic, readonly) NSString *recipientId; + +// This property is optional and will not be set for +// non-contact account. +@property (nonatomic, nullable) Contact *contact; + +@property (nonatomic) BOOL hasMultipleAccountContact; + +// For contacts with more than one signal account, +// this is a label for the account. +@property (nonatomic) NSString *multipleAccountLabelText; + +- (nullable NSString *)contactFullName; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithSignalRecipient:(SignalRecipient *)signalRecipient; + +- (instancetype)initWithRecipientId:(NSString *)recipientId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SignalAccount.m b/SignalUtilitiesKit/SignalAccount.m new file mode 100644 index 000000000..3e837f2c3 --- /dev/null +++ b/SignalUtilitiesKit/SignalAccount.m @@ -0,0 +1,57 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "SignalAccount.h" +#import "Contact.h" +#import "NSString+SSK.h" +#import "OWSPrimaryStorage.h" +#import "SignalRecipient.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SignalAccount () + +@property (nonatomic) NSString *recipientId; + +@end + +#pragma mark - + +@implementation SignalAccount + +- (instancetype)initWithSignalRecipient:(SignalRecipient *)signalRecipient +{ + OWSAssertDebug(signalRecipient); + return [self initWithRecipientId:signalRecipient.recipientId]; +} + +- (instancetype)initWithRecipientId:(NSString *)recipientId +{ + if (self = [super init]) { + OWSAssertDebug(recipientId.length > 0); + + _recipientId = recipientId; + } + return self; +} + +- (nullable NSString *)uniqueId +{ + return _recipientId; +} + +- (nullable NSString *)contactFullName +{ + return self.contact.fullName.filterStringForDisplay; +} + +- (NSString *)multipleAccountLabelText +{ + return _multipleAccountLabelText.filterStringForDisplay; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SignalIOS.pb.swift b/SignalUtilitiesKit/SignalIOS.pb.swift new file mode 100644 index 000000000..588c729ed --- /dev/null +++ b/SignalUtilitiesKit/SignalIOS.pb.swift @@ -0,0 +1,318 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: SignalIOS.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +//* +// Copyright (C) 2014-2016 Open Whisper Systems +// +// Licensed according to the LICENSE file in this repository. + +/// iOS - since we use a modern proto-compiler, we must specify +/// the legacy proto format. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct IOSProtos_BackupSnapshot { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var entity: [IOSProtos_BackupSnapshot.BackupEntity] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct BackupEntity { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var type: IOSProtos_BackupSnapshot.BackupEntity.TypeEnum { + get {return _type ?? .unknown} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + /// @required + var entityData: Data { + get {return _entityData ?? SwiftProtobuf.Internal.emptyData} + set {_entityData = newValue} + } + /// Returns true if `entityData` has been explicitly set. + var hasEntityData: Bool {return self._entityData != nil} + /// Clears the value of `entityData`. Subsequent reads from it will return its default value. + mutating func clearEntityData() {self._entityData = nil} + + /// @required + var collection: String { + get {return _collection ?? String()} + set {_collection = newValue} + } + /// Returns true if `collection` has been explicitly set. + var hasCollection: Bool {return self._collection != nil} + /// Clears the value of `collection`. Subsequent reads from it will return its default value. + mutating func clearCollection() {self._collection = nil} + + /// @required + var key: String { + get {return _key ?? String()} + set {_key = newValue} + } + /// Returns true if `key` has been explicitly set. + var hasKey: Bool {return self._key != nil} + /// Clears the value of `key`. Subsequent reads from it will return its default value. + mutating func clearKey() {self._key = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case migration // = 1 + case thread // = 2 + case interaction // = 3 + case attachment // = 4 + case misc // = 5 + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .migration + case 2: self = .thread + case 3: self = .interaction + case 4: self = .attachment + case 5: self = .misc + default: return nil + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .migration: return 1 + case .thread: return 2 + case .interaction: return 3 + case .attachment: return 4 + case .misc: return 5 + } + } + + } + + init() {} + + fileprivate var _type: IOSProtos_BackupSnapshot.BackupEntity.TypeEnum? = nil + fileprivate var _entityData: Data? = nil + fileprivate var _collection: String? = nil + fileprivate var _key: String? = nil + } + + init() {} +} + +#if swift(>=4.2) + +extension IOSProtos_BackupSnapshot.BackupEntity.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct IOSProtos_DeviceName { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var ephemeralPublic: Data { + get {return _ephemeralPublic ?? SwiftProtobuf.Internal.emptyData} + set {_ephemeralPublic = newValue} + } + /// Returns true if `ephemeralPublic` has been explicitly set. + var hasEphemeralPublic: Bool {return self._ephemeralPublic != nil} + /// Clears the value of `ephemeralPublic`. Subsequent reads from it will return its default value. + mutating func clearEphemeralPublic() {self._ephemeralPublic = nil} + + /// @required + var syntheticIv: Data { + get {return _syntheticIv ?? SwiftProtobuf.Internal.emptyData} + set {_syntheticIv = newValue} + } + /// Returns true if `syntheticIv` has been explicitly set. + var hasSyntheticIv: Bool {return self._syntheticIv != nil} + /// Clears the value of `syntheticIv`. Subsequent reads from it will return its default value. + mutating func clearSyntheticIv() {self._syntheticIv = nil} + + /// @required + var ciphertext: Data { + get {return _ciphertext ?? SwiftProtobuf.Internal.emptyData} + set {_ciphertext = newValue} + } + /// Returns true if `ciphertext` has been explicitly set. + var hasCiphertext: Bool {return self._ciphertext != nil} + /// Clears the value of `ciphertext`. Subsequent reads from it will return its default value. + mutating func clearCiphertext() {self._ciphertext = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _ephemeralPublic: Data? = nil + fileprivate var _syntheticIv: Data? = nil + fileprivate var _ciphertext: Data? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "IOSProtos" + +extension IOSProtos_BackupSnapshot: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".BackupSnapshot" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "entity"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeRepeatedMessageField(value: &self.entity) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.entity.isEmpty { + try visitor.visitRepeatedMessageField(value: self.entity, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: IOSProtos_BackupSnapshot, rhs: IOSProtos_BackupSnapshot) -> Bool { + if lhs.entity != rhs.entity {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension IOSProtos_BackupSnapshot.BackupEntity: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = IOSProtos_BackupSnapshot.protoMessageName + ".BackupEntity" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "entityData"), + 3: .same(proto: "collection"), + 4: .same(proto: "key"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularEnumField(value: &self._type) + case 2: try decoder.decodeSingularBytesField(value: &self._entityData) + case 3: try decoder.decodeSingularStringField(value: &self._collection) + case 4: try decoder.decodeSingularStringField(value: &self._key) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 1) + } + if let v = self._entityData { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + if let v = self._collection { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if let v = self._key { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: IOSProtos_BackupSnapshot.BackupEntity, rhs: IOSProtos_BackupSnapshot.BackupEntity) -> Bool { + if lhs._type != rhs._type {return false} + if lhs._entityData != rhs._entityData {return false} + if lhs._collection != rhs._collection {return false} + if lhs._key != rhs._key {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension IOSProtos_BackupSnapshot.BackupEntity.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "MIGRATION"), + 2: .same(proto: "THREAD"), + 3: .same(proto: "INTERACTION"), + 4: .same(proto: "ATTACHMENT"), + 5: .same(proto: "MISC"), + ] +} + +extension IOSProtos_DeviceName: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DeviceName" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "ephemeralPublic"), + 2: .same(proto: "syntheticIv"), + 3: .same(proto: "ciphertext"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._ephemeralPublic) + case 2: try decoder.decodeSingularBytesField(value: &self._syntheticIv) + case 3: try decoder.decodeSingularBytesField(value: &self._ciphertext) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._ephemeralPublic { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._syntheticIv { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + if let v = self._ciphertext { + try visitor.visitSingularBytesField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: IOSProtos_DeviceName, rhs: IOSProtos_DeviceName) -> Bool { + if lhs._ephemeralPublic != rhs._ephemeralPublic {return false} + if lhs._syntheticIv != rhs._syntheticIv {return false} + if lhs._ciphertext != rhs._ciphertext {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/SignalUtilitiesKit/SignalIOSProto.swift b/SignalUtilitiesKit/SignalIOSProto.swift new file mode 100644 index 000000000..5761fbda7 --- /dev/null +++ b/SignalUtilitiesKit/SignalIOSProto.swift @@ -0,0 +1,409 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +// WARNING: This code is generated. Only edit within the markers. + +public enum SignalIOSProtoError: Error { + case invalidProtobuf(description: String) +} + +// MARK: - SignalIOSProtoBackupSnapshotBackupEntity + +@objc public class SignalIOSProtoBackupSnapshotBackupEntity: NSObject { + + // MARK: - SignalIOSProtoBackupSnapshotBackupEntityType + + @objc public enum SignalIOSProtoBackupSnapshotBackupEntityType: Int32 { + case unknown = 0 + case migration = 1 + case thread = 2 + case interaction = 3 + case attachment = 4 + case misc = 5 + } + + private class func SignalIOSProtoBackupSnapshotBackupEntityTypeWrap(_ value: IOSProtos_BackupSnapshot.BackupEntity.TypeEnum) -> SignalIOSProtoBackupSnapshotBackupEntityType { + switch value { + case .unknown: return .unknown + case .migration: return .migration + case .thread: return .thread + case .interaction: return .interaction + case .attachment: return .attachment + case .misc: return .misc + } + } + + private class func SignalIOSProtoBackupSnapshotBackupEntityTypeUnwrap(_ value: SignalIOSProtoBackupSnapshotBackupEntityType) -> IOSProtos_BackupSnapshot.BackupEntity.TypeEnum { + switch value { + case .unknown: return .unknown + case .migration: return .migration + case .thread: return .thread + case .interaction: return .interaction + case .attachment: return .attachment + case .misc: return .misc + } + } + + // MARK: - SignalIOSProtoBackupSnapshotBackupEntityBuilder + + @objc public class func builder(type: SignalIOSProtoBackupSnapshotBackupEntityType, entityData: Data, collection: String, key: String) -> SignalIOSProtoBackupSnapshotBackupEntityBuilder { + return SignalIOSProtoBackupSnapshotBackupEntityBuilder(type: type, entityData: entityData, collection: collection, key: key) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SignalIOSProtoBackupSnapshotBackupEntityBuilder { + let builder = SignalIOSProtoBackupSnapshotBackupEntityBuilder(type: type, entityData: entityData, collection: collection, key: key) + return builder + } + + @objc public class SignalIOSProtoBackupSnapshotBackupEntityBuilder: NSObject { + + private var proto = IOSProtos_BackupSnapshot.BackupEntity() + + @objc fileprivate override init() {} + + @objc fileprivate init(type: SignalIOSProtoBackupSnapshotBackupEntityType, entityData: Data, collection: String, key: String) { + super.init() + + setType(type) + setEntityData(entityData) + setCollection(collection) + setKey(key) + } + + @objc public func setType(_ valueParam: SignalIOSProtoBackupSnapshotBackupEntityType) { + proto.type = SignalIOSProtoBackupSnapshotBackupEntityTypeUnwrap(valueParam) + } + + @objc public func setEntityData(_ valueParam: Data) { + proto.entityData = valueParam + } + + @objc public func setCollection(_ valueParam: String) { + proto.collection = valueParam + } + + @objc public func setKey(_ valueParam: String) { + proto.key = valueParam + } + + @objc public func build() throws -> SignalIOSProtoBackupSnapshotBackupEntity { + return try SignalIOSProtoBackupSnapshotBackupEntity.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SignalIOSProtoBackupSnapshotBackupEntity.parseProto(proto).serializedData() + } + } + + fileprivate let proto: IOSProtos_BackupSnapshot.BackupEntity + + @objc public let type: SignalIOSProtoBackupSnapshotBackupEntityType + + @objc public let entityData: Data + + @objc public let collection: String + + @objc public let key: String + + private init(proto: IOSProtos_BackupSnapshot.BackupEntity, + type: SignalIOSProtoBackupSnapshotBackupEntityType, + entityData: Data, + collection: String, + key: String) { + self.proto = proto + self.type = type + self.entityData = entityData + self.collection = collection + self.key = key + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SignalIOSProtoBackupSnapshotBackupEntity { + let proto = try IOSProtos_BackupSnapshot.BackupEntity(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: IOSProtos_BackupSnapshot.BackupEntity) throws -> SignalIOSProtoBackupSnapshotBackupEntity { + guard proto.hasType else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: type") + } + let type = SignalIOSProtoBackupSnapshotBackupEntityTypeWrap(proto.type) + + guard proto.hasEntityData else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: entityData") + } + let entityData = proto.entityData + + guard proto.hasCollection else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: collection") + } + let collection = proto.collection + + guard proto.hasKey else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: key") + } + let key = proto.key + + // MARK: - Begin Validation Logic for SignalIOSProtoBackupSnapshotBackupEntity - + + // MARK: - End Validation Logic for SignalIOSProtoBackupSnapshotBackupEntity - + + let result = SignalIOSProtoBackupSnapshotBackupEntity(proto: proto, + type: type, + entityData: entityData, + collection: collection, + key: key) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SignalIOSProtoBackupSnapshotBackupEntity { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SignalIOSProtoBackupSnapshotBackupEntity.SignalIOSProtoBackupSnapshotBackupEntityBuilder { + @objc public func buildIgnoringErrors() -> SignalIOSProtoBackupSnapshotBackupEntity? { + return try! self.build() + } +} + +#endif + +// MARK: - SignalIOSProtoBackupSnapshot + +@objc public class SignalIOSProtoBackupSnapshot: NSObject { + + // MARK: - SignalIOSProtoBackupSnapshotBuilder + + @objc public class func builder() -> SignalIOSProtoBackupSnapshotBuilder { + return SignalIOSProtoBackupSnapshotBuilder() + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SignalIOSProtoBackupSnapshotBuilder { + let builder = SignalIOSProtoBackupSnapshotBuilder() + builder.setEntity(entity) + return builder + } + + @objc public class SignalIOSProtoBackupSnapshotBuilder: NSObject { + + private var proto = IOSProtos_BackupSnapshot() + + @objc fileprivate override init() {} + + @objc public func addEntity(_ valueParam: SignalIOSProtoBackupSnapshotBackupEntity) { + var items = proto.entity + items.append(valueParam.proto) + proto.entity = items + } + + @objc public func setEntity(_ wrappedItems: [SignalIOSProtoBackupSnapshotBackupEntity]) { + proto.entity = wrappedItems.map { $0.proto } + } + + @objc public func build() throws -> SignalIOSProtoBackupSnapshot { + return try SignalIOSProtoBackupSnapshot.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SignalIOSProtoBackupSnapshot.parseProto(proto).serializedData() + } + } + + fileprivate let proto: IOSProtos_BackupSnapshot + + @objc public let entity: [SignalIOSProtoBackupSnapshotBackupEntity] + + private init(proto: IOSProtos_BackupSnapshot, + entity: [SignalIOSProtoBackupSnapshotBackupEntity]) { + self.proto = proto + self.entity = entity + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SignalIOSProtoBackupSnapshot { + let proto = try IOSProtos_BackupSnapshot(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: IOSProtos_BackupSnapshot) throws -> SignalIOSProtoBackupSnapshot { + var entity: [SignalIOSProtoBackupSnapshotBackupEntity] = [] + entity = try proto.entity.map { try SignalIOSProtoBackupSnapshotBackupEntity.parseProto($0) } + + // MARK: - Begin Validation Logic for SignalIOSProtoBackupSnapshot - + + // MARK: - End Validation Logic for SignalIOSProtoBackupSnapshot - + + let result = SignalIOSProtoBackupSnapshot(proto: proto, + entity: entity) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SignalIOSProtoBackupSnapshot { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SignalIOSProtoBackupSnapshot.SignalIOSProtoBackupSnapshotBuilder { + @objc public func buildIgnoringErrors() -> SignalIOSProtoBackupSnapshot? { + return try! self.build() + } +} + +#endif + +// MARK: - SignalIOSProtoDeviceName + +@objc public class SignalIOSProtoDeviceName: NSObject { + + // MARK: - SignalIOSProtoDeviceNameBuilder + + @objc public class func builder(ephemeralPublic: Data, syntheticIv: Data, ciphertext: Data) -> SignalIOSProtoDeviceNameBuilder { + return SignalIOSProtoDeviceNameBuilder(ephemeralPublic: ephemeralPublic, syntheticIv: syntheticIv, ciphertext: ciphertext) + } + + // asBuilder() constructs a builder that reflects the proto's contents. + @objc public func asBuilder() -> SignalIOSProtoDeviceNameBuilder { + let builder = SignalIOSProtoDeviceNameBuilder(ephemeralPublic: ephemeralPublic, syntheticIv: syntheticIv, ciphertext: ciphertext) + return builder + } + + @objc public class SignalIOSProtoDeviceNameBuilder: NSObject { + + private var proto = IOSProtos_DeviceName() + + @objc fileprivate override init() {} + + @objc fileprivate init(ephemeralPublic: Data, syntheticIv: Data, ciphertext: Data) { + super.init() + + setEphemeralPublic(ephemeralPublic) + setSyntheticIv(syntheticIv) + setCiphertext(ciphertext) + } + + @objc public func setEphemeralPublic(_ valueParam: Data) { + proto.ephemeralPublic = valueParam + } + + @objc public func setSyntheticIv(_ valueParam: Data) { + proto.syntheticIv = valueParam + } + + @objc public func setCiphertext(_ valueParam: Data) { + proto.ciphertext = valueParam + } + + @objc public func build() throws -> SignalIOSProtoDeviceName { + return try SignalIOSProtoDeviceName.parseProto(proto) + } + + @objc public func buildSerializedData() throws -> Data { + return try SignalIOSProtoDeviceName.parseProto(proto).serializedData() + } + } + + fileprivate let proto: IOSProtos_DeviceName + + @objc public let ephemeralPublic: Data + + @objc public let syntheticIv: Data + + @objc public let ciphertext: Data + + private init(proto: IOSProtos_DeviceName, + ephemeralPublic: Data, + syntheticIv: Data, + ciphertext: Data) { + self.proto = proto + self.ephemeralPublic = ephemeralPublic + self.syntheticIv = syntheticIv + self.ciphertext = ciphertext + } + + @objc + public func serializedData() throws -> Data { + return try self.proto.serializedData() + } + + @objc public class func parseData(_ serializedData: Data) throws -> SignalIOSProtoDeviceName { + let proto = try IOSProtos_DeviceName(serializedData: serializedData) + return try parseProto(proto) + } + + fileprivate class func parseProto(_ proto: IOSProtos_DeviceName) throws -> SignalIOSProtoDeviceName { + guard proto.hasEphemeralPublic else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: ephemeralPublic") + } + let ephemeralPublic = proto.ephemeralPublic + + guard proto.hasSyntheticIv else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: syntheticIv") + } + let syntheticIv = proto.syntheticIv + + guard proto.hasCiphertext else { + throw SignalIOSProtoError.invalidProtobuf(description: "\(logTag) missing required field: ciphertext") + } + let ciphertext = proto.ciphertext + + // MARK: - Begin Validation Logic for SignalIOSProtoDeviceName - + + // MARK: - End Validation Logic for SignalIOSProtoDeviceName - + + let result = SignalIOSProtoDeviceName(proto: proto, + ephemeralPublic: ephemeralPublic, + syntheticIv: syntheticIv, + ciphertext: ciphertext) + return result + } + + @objc public override var debugDescription: String { + return "\(proto)" + } +} + +#if DEBUG + +extension SignalIOSProtoDeviceName { + @objc public func serializedDataIgnoringErrors() -> Data? { + return try! self.serializedData() + } +} + +extension SignalIOSProtoDeviceName.SignalIOSProtoDeviceNameBuilder { + @objc public func buildIgnoringErrors() -> SignalIOSProtoDeviceName? { + return try! self.build() + } +} + +#endif diff --git a/SignalUtilitiesKit/SignalMessage.swift b/SignalUtilitiesKit/SignalMessage.swift new file mode 100644 index 000000000..c4cb88249 --- /dev/null +++ b/SignalUtilitiesKit/SignalMessage.swift @@ -0,0 +1,28 @@ + +@objc(LKSignalMessage) +public final class SignalMessage : NSObject { + @objc public let type: SSKProtoEnvelope.SSKProtoEnvelopeType + @objc public let timestamp: UInt64 + @objc public let senderPublicKey: String + @objc public let senderDeviceID: UInt32 + @objc public let content: String + @objc public let recipientPublicKey: String + @objc(ttl) + public let objc_ttl: UInt64 + @objc public let isPing: Bool + + public var ttl: UInt64? { return objc_ttl != 0 ? objc_ttl : nil } + + @objc public init(type: SSKProtoEnvelope.SSKProtoEnvelopeType, timestamp: UInt64, senderID: String, senderDeviceID: UInt32, + content: String, recipientID: String, ttl: UInt64, isPing: Bool) { + self.type = type + self.timestamp = timestamp + self.senderPublicKey = senderID + self.senderDeviceID = senderDeviceID + self.content = content + self.recipientPublicKey = recipientID + self.objc_ttl = ttl + self.isPing = isPing + super.init() + } +} diff --git a/SignalUtilitiesKit/SignalRecipient.h b/SignalUtilitiesKit/SignalRecipient.h new file mode 100644 index 000000000..3fd396f73 --- /dev/null +++ b/SignalUtilitiesKit/SignalRecipient.h @@ -0,0 +1,50 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +// SignalRecipient serves two purposes: +// +// a) It serves as a cache of "known" Signal accounts. When the service indicates +// that an account exists, we make sure that an instance of SignalRecipient exists +// for that recipient id (using mark as registered) and has at least one device. +// When the service indicates that an account does not exist, we remove any devices +// from that SignalRecipient - but do not remove it from the database. +// Note that SignalRecipients without any devices are not considered registered. +//// b) We hang the "known device list" for known signal accounts on this entity. +@interface SignalRecipient : TSYapDatabaseObject + +@property (nonatomic, readonly) NSOrderedSet *devices; + +- (instancetype)init NS_UNAVAILABLE; + ++ (nullable instancetype)registeredRecipientForRecipientId:(NSString *)recipientId + mustHaveDevices:(BOOL)mustHaveDevices + transaction:(YapDatabaseReadTransaction *)transaction; ++ (instancetype)getOrBuildUnsavedRecipientForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadTransaction *)transaction; + +- (void)updateRegisteredRecipientWithDevicesToAdd:(nullable NSArray *)devicesToAdd + devicesToRemove:(nullable NSArray *)devicesToRemove + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (NSString *)recipientId; + +- (NSComparisonResult)compare:(SignalRecipient *)other; + ++ (BOOL)isRegisteredRecipient:(NSString *)recipientId transaction:(YapDatabaseReadTransaction *)transaction; + ++ (SignalRecipient *)markRecipientAsRegisteredAndGet:(NSString *)recipientId + transaction:(YapDatabaseReadWriteTransaction *)transaction; ++ (void)markRecipientAsRegistered:(NSString *)recipientId + deviceId:(UInt32)deviceId + transaction:(YapDatabaseReadWriteTransaction *)transaction; + ++ (void)markRecipientAsUnregistered:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SignalRecipient.m b/SignalUtilitiesKit/SignalRecipient.m new file mode 100644 index 000000000..1e9d082f0 --- /dev/null +++ b/SignalUtilitiesKit/SignalRecipient.m @@ -0,0 +1,283 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "SignalRecipient.h" +#import "OWSDevice.h" +#import "ProfileManagerProtocol.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSSocketManager.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SignalRecipient () + +@property (nonatomic) NSOrderedSet *devices; + +@end + +#pragma mark - + +@implementation SignalRecipient + +#pragma mark - Dependencies + +- (id)profileManager +{ + return SSKEnvironment.shared.profileManager; +} + +- (id)udManager +{ + return SSKEnvironment.shared.udManager; +} + +- (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +- (TSSocketManager *)socketManager +{ + OWSAssertDebug(SSKEnvironment.shared.socketManager); + + return SSKEnvironment.shared.socketManager; +} + +#pragma mark - + ++ (instancetype)getOrBuildUnsavedRecipientForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(recipientId.length > 0); + + SignalRecipient *_Nullable recipient = + [self registeredRecipientForRecipientId:recipientId mustHaveDevices:NO transaction:transaction]; + if (!recipient) { + recipient = [[self alloc] initWithTextSecureIdentifier:recipientId]; + } + return recipient; +} + +- (instancetype)initWithTextSecureIdentifier:(NSString *)textSecureIdentifier +{ + self = [super initWithUniqueId:textSecureIdentifier]; + if (!self) { + return self; + } + + _devices = [NSOrderedSet orderedSetWithObject:@(OWSDevicePrimaryDeviceId)]; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (_devices == nil) { + _devices = [NSOrderedSet new]; + } + + // Since we use device count to determine whether a user is registered or not, + // ensure the local user always has at least *this* device. + if (![_devices containsObject:@(OWSDevicePrimaryDeviceId)]) { + if ([self.uniqueId isEqualToString:self.tsAccountManager.localNumber]) { + DDLogInfo(@"Adding primary device to self recipient."); + [self addDevices:[NSSet setWithObject:@(OWSDevicePrimaryDeviceId)]]; + } + } + + return self; +} + ++ (nullable instancetype)registeredRecipientForRecipientId:(NSString *)recipientId + mustHaveDevices:(BOOL)mustHaveDevices + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(recipientId.length > 0); + + SignalRecipient *_Nullable signalRecipient = [self fetchObjectWithUniqueID:recipientId transaction:transaction]; + if (mustHaveDevices && signalRecipient.devices.count < 1) { + return nil; + } + return signalRecipient; +} + +- (void)addDevices:(NSSet *)devices +{ + OWSAssertDebug(devices.count > 0); + + NSMutableOrderedSet *updatedDevices = [self.devices mutableCopy]; + [updatedDevices unionSet:devices]; + self.devices = [updatedDevices copy]; +} + +- (void)removeDevices:(NSSet *)devices +{ + OWSAssertDebug(devices.count > 0); + + NSMutableOrderedSet *updatedDevices = [self.devices mutableCopy]; + [updatedDevices minusSet:devices]; + self.devices = [updatedDevices copy]; +} + +- (void)updateRegisteredRecipientWithDevicesToAdd:(nullable NSArray *)devicesToAdd + devicesToRemove:(nullable NSArray *)devicesToRemove + transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssertDebug(transaction); + OWSAssertDebug(devicesToAdd.count > 0 || devicesToRemove.count > 0); + + // Add before we remove, since removeDevicesFromRecipient:... + // can markRecipientAsUnregistered:... if the recipient has + // no devices left. + if (devicesToAdd.count > 0) { + [self addDevicesToRegisteredRecipient:[NSSet setWithArray:devicesToAdd] transaction:transaction]; + } + if (devicesToRemove.count > 0) { + [self removeDevicesFromRecipient:[NSSet setWithArray:devicesToRemove] transaction:transaction]; + } + + // Device changes + dispatch_async(dispatch_get_main_queue(), ^{ + // Device changes can affect the UD access mode for a recipient, + // so we need to fetch the profile for this user to update UD access mode. + [self.profileManager fetchProfileForRecipientId:self.recipientId]; + + if ([self.recipientId isEqualToString:self.tsAccountManager.localNumber]) { + [self.socketManager cycleSocket]; + } + }); +} + +- (void)addDevicesToRegisteredRecipient:(NSSet *)devices transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(devices.count > 0); + OWSLogDebug(@"adding devices: %@, to recipient: %@", devices, self); + + [self reloadWithTransaction:transaction]; + [self addDevices:devices]; + [self saveWithTransaction_internal:transaction]; +} + +- (void)removeDevicesFromRecipient:(NSSet *)devices transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(devices.count > 0); + + OWSLogDebug(@"removing devices: %@, from registered recipient: %@", devices, self); + [self reloadWithTransaction:transaction ignoreMissing:YES]; + [self removeDevices:devices]; + [self saveWithTransaction_internal:transaction]; +} + +- (NSString *)recipientId +{ + return self.uniqueId; +} + +- (NSComparisonResult)compare:(SignalRecipient *)other +{ + return [self.recipientId compare:other.recipientId]; +} + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + // We need to distinguish between "users we know to be unregistered" and + // "users whose registration status is unknown". The former correspond to + // instances of SignalRecipient with no devices. The latter do not + // correspond to an instance of SignalRecipient in the database (although + // they may correspond to an "unsaved" instance of SignalRecipient built + // by getOrBuildUnsavedRecipientForRecipientId. + OWSFailDebug(@"Don't call removeWithTransaction."); + + [super removeWithTransaction:transaction]; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + // We only want to mutate the persisted SignalRecipients in the database + // using other methods of this class, e.g. markRecipientAsRegistered... + // to create, addDevices and removeDevices to mutate. We're trying to + // be strict about using persisted SignalRecipients as a cache to + // reflect "last known registration status". Forcing our codebase to + // use those methods helps ensure that we update the cache deliberately. + OWSFailDebug(@"Don't call saveWithTransaction from outside this class."); + + [self saveWithTransaction_internal:transaction]; +} + +- (void)saveWithTransaction_internal:(YapDatabaseReadWriteTransaction *)transaction +{ + [super saveWithTransaction:transaction]; + + OWSLogVerbose(@"saved signal recipient: %@ (%lu)", self.recipientId, (unsigned long) self.devices.count); +} + ++ (BOOL)isRegisteredRecipient:(NSString *)recipientId transaction:(YapDatabaseReadTransaction *)transaction +{ + return nil != [self registeredRecipientForRecipientId:recipientId mustHaveDevices:YES transaction:transaction]; +} + ++ (SignalRecipient *)markRecipientAsRegisteredAndGet:(NSString *)recipientId + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(recipientId.length > 0); + + SignalRecipient *_Nullable instance = + [self registeredRecipientForRecipientId:recipientId mustHaveDevices:YES transaction:transaction]; + + if (!instance) { + OWSLogDebug(@"creating recipient: %@", recipientId); + + instance = [[self alloc] initWithTextSecureIdentifier:recipientId]; + [instance saveWithTransaction_internal:transaction]; + } + return instance; +} + ++ (void)markRecipientAsRegistered:(NSString *)recipientId + deviceId:(UInt32)deviceId + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(recipientId.length > 0); + + SignalRecipient *recipient = [self markRecipientAsRegisteredAndGet:recipientId transaction:transaction]; + if (![recipient.devices containsObject:@(deviceId)]) { + OWSLogDebug(@"Adding device %u to existing recipient.", (unsigned int)deviceId); + + [recipient addDevices:[NSSet setWithObject:@(deviceId)]]; + [recipient saveWithTransaction_internal:transaction]; + } +} + ++ (void)markRecipientAsUnregistered:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(recipientId.length > 0); + + SignalRecipient *instance = [self getOrBuildUnsavedRecipientForRecipientId:recipientId + transaction:transaction]; + OWSLogDebug(@"Marking recipient as not registered: %@", recipientId); + if (instance.devices.count > 0) { + [instance removeDevices:instance.devices.set]; + } + [instance saveWithTransaction_internal:transaction]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/SignalService.pb.swift b/SignalUtilitiesKit/SignalService.pb.swift new file mode 100644 index 000000000..ac386523f --- /dev/null +++ b/SignalUtilitiesKit/SignalService.pb.swift @@ -0,0 +1,5199 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: SignalService.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +//* +// Copyright (C) 2014-2016 Open Whisper Systems +// +// Licensed according to the LICENSE file in this repository. + +/// iOS - since we use a modern proto-compiler, we must specify +/// the legacy proto format. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct SignalServiceProtos_Envelope { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var type: SignalServiceProtos_Envelope.TypeEnum { + get {return _type ?? .unknown} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var source: String { + get {return _source ?? String()} + set {_source = newValue} + } + /// Returns true if `source` has been explicitly set. + var hasSource: Bool {return self._source != nil} + /// Clears the value of `source`. Subsequent reads from it will return its default value. + mutating func clearSource() {self._source = nil} + + var sourceDevice: UInt32 { + get {return _sourceDevice ?? 0} + set {_sourceDevice = newValue} + } + /// Returns true if `sourceDevice` has been explicitly set. + var hasSourceDevice: Bool {return self._sourceDevice != nil} + /// Clears the value of `sourceDevice`. Subsequent reads from it will return its default value. + mutating func clearSourceDevice() {self._sourceDevice = nil} + + var relay: String { + get {return _relay ?? String()} + set {_relay = newValue} + } + /// Returns true if `relay` has been explicitly set. + var hasRelay: Bool {return self._relay != nil} + /// Clears the value of `relay`. Subsequent reads from it will return its default value. + mutating func clearRelay() {self._relay = nil} + + /// @required + var timestamp: UInt64 { + get {return _timestamp ?? 0} + set {_timestamp = newValue} + } + /// Returns true if `timestamp` has been explicitly set. + var hasTimestamp: Bool {return self._timestamp != nil} + /// Clears the value of `timestamp`. Subsequent reads from it will return its default value. + mutating func clearTimestamp() {self._timestamp = nil} + + /// Contains an encrypted DataMessage + var legacyMessage: Data { + get {return _legacyMessage ?? SwiftProtobuf.Internal.emptyData} + set {_legacyMessage = newValue} + } + /// Returns true if `legacyMessage` has been explicitly set. + var hasLegacyMessage: Bool {return self._legacyMessage != nil} + /// Clears the value of `legacyMessage`. Subsequent reads from it will return its default value. + mutating func clearLegacyMessage() {self._legacyMessage = nil} + + /// Contains an encrypted Content + var content: Data { + get {return _content ?? SwiftProtobuf.Internal.emptyData} + set {_content = newValue} + } + /// Returns true if `content` has been explicitly set. + var hasContent: Bool {return self._content != nil} + /// Clears the value of `content`. Subsequent reads from it will return its default value. + mutating func clearContent() {self._content = nil} + + /// We may eventually want to make this required. + var serverGuid: String { + get {return _serverGuid ?? String()} + set {_serverGuid = newValue} + } + /// Returns true if `serverGuid` has been explicitly set. + var hasServerGuid: Bool {return self._serverGuid != nil} + /// Clears the value of `serverGuid`. Subsequent reads from it will return its default value. + mutating func clearServerGuid() {self._serverGuid = nil} + + /// We may eventually want to make this required. + var serverTimestamp: UInt64 { + get {return _serverTimestamp ?? 0} + set {_serverTimestamp = newValue} + } + /// Returns true if `serverTimestamp` has been explicitly set. + var hasServerTimestamp: Bool {return self._serverTimestamp != nil} + /// Clears the value of `serverTimestamp`. Subsequent reads from it will return its default value. + mutating func clearServerTimestamp() {self._serverTimestamp = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case ciphertext // = 1 + case keyExchange // = 2 + case prekeyBundle // = 3 + case receipt // = 5 + case unidentifiedSender // = 6 + + /// Loki + case closedGroupCiphertext // = 7 + + /// Loki: Encrypted using the fallback session cipher. Contains a pre key bundle if it's a session request. + case fallbackMessage // = 101 + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .ciphertext + case 2: self = .keyExchange + case 3: self = .prekeyBundle + case 5: self = .receipt + case 6: self = .unidentifiedSender + case 7: self = .closedGroupCiphertext + case 101: self = .fallbackMessage + default: return nil + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .ciphertext: return 1 + case .keyExchange: return 2 + case .prekeyBundle: return 3 + case .receipt: return 5 + case .unidentifiedSender: return 6 + case .closedGroupCiphertext: return 7 + case .fallbackMessage: return 101 + } + } + + } + + init() {} + + fileprivate var _type: SignalServiceProtos_Envelope.TypeEnum? = nil + fileprivate var _source: String? = nil + fileprivate var _sourceDevice: UInt32? = nil + fileprivate var _relay: String? = nil + fileprivate var _timestamp: UInt64? = nil + fileprivate var _legacyMessage: Data? = nil + fileprivate var _content: Data? = nil + fileprivate var _serverGuid: String? = nil + fileprivate var _serverTimestamp: UInt64? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_Envelope.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_TypingMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var timestamp: UInt64 { + get {return _timestamp ?? 0} + set {_timestamp = newValue} + } + /// Returns true if `timestamp` has been explicitly set. + var hasTimestamp: Bool {return self._timestamp != nil} + /// Clears the value of `timestamp`. Subsequent reads from it will return its default value. + mutating func clearTimestamp() {self._timestamp = nil} + + /// @required + var action: SignalServiceProtos_TypingMessage.Action { + get {return _action ?? .started} + set {_action = newValue} + } + /// Returns true if `action` has been explicitly set. + var hasAction: Bool {return self._action != nil} + /// Clears the value of `action`. Subsequent reads from it will return its default value. + mutating func clearAction() {self._action = nil} + + var groupID: Data { + get {return _groupID ?? SwiftProtobuf.Internal.emptyData} + set {_groupID = newValue} + } + /// Returns true if `groupID` has been explicitly set. + var hasGroupID: Bool {return self._groupID != nil} + /// Clears the value of `groupID`. Subsequent reads from it will return its default value. + mutating func clearGroupID() {self._groupID = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum Action: SwiftProtobuf.Enum { + typealias RawValue = Int + case started // = 0 + case stopped // = 1 + + init() { + self = .started + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .started + case 1: self = .stopped + default: return nil + } + } + + var rawValue: Int { + switch self { + case .started: return 0 + case .stopped: return 1 + } + } + + } + + init() {} + + fileprivate var _timestamp: UInt64? = nil + fileprivate var _action: SignalServiceProtos_TypingMessage.Action? = nil + fileprivate var _groupID: Data? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_TypingMessage.Action: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_Content { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var dataMessage: SignalServiceProtos_DataMessage { + get {return _dataMessage ?? SignalServiceProtos_DataMessage()} + set {_dataMessage = newValue} + } + /// Returns true if `dataMessage` has been explicitly set. + var hasDataMessage: Bool {return self._dataMessage != nil} + /// Clears the value of `dataMessage`. Subsequent reads from it will return its default value. + mutating func clearDataMessage() {self._dataMessage = nil} + + var syncMessage: SignalServiceProtos_SyncMessage { + get {return _syncMessage ?? SignalServiceProtos_SyncMessage()} + set {_syncMessage = newValue} + } + /// Returns true if `syncMessage` has been explicitly set. + var hasSyncMessage: Bool {return self._syncMessage != nil} + /// Clears the value of `syncMessage`. Subsequent reads from it will return its default value. + mutating func clearSyncMessage() {self._syncMessage = nil} + + var callMessage: SignalServiceProtos_CallMessage { + get {return _callMessage ?? SignalServiceProtos_CallMessage()} + set {_callMessage = newValue} + } + /// Returns true if `callMessage` has been explicitly set. + var hasCallMessage: Bool {return self._callMessage != nil} + /// Clears the value of `callMessage`. Subsequent reads from it will return its default value. + mutating func clearCallMessage() {self._callMessage = nil} + + var nullMessage: SignalServiceProtos_NullMessage { + get {return _nullMessage ?? SignalServiceProtos_NullMessage()} + set {_nullMessage = newValue} + } + /// Returns true if `nullMessage` has been explicitly set. + var hasNullMessage: Bool {return self._nullMessage != nil} + /// Clears the value of `nullMessage`. Subsequent reads from it will return its default value. + mutating func clearNullMessage() {self._nullMessage = nil} + + var receiptMessage: SignalServiceProtos_ReceiptMessage { + get {return _receiptMessage ?? SignalServiceProtos_ReceiptMessage()} + set {_receiptMessage = newValue} + } + /// Returns true if `receiptMessage` has been explicitly set. + var hasReceiptMessage: Bool {return self._receiptMessage != nil} + /// Clears the value of `receiptMessage`. Subsequent reads from it will return its default value. + mutating func clearReceiptMessage() {self._receiptMessage = nil} + + var typingMessage: SignalServiceProtos_TypingMessage { + get {return _typingMessage ?? SignalServiceProtos_TypingMessage()} + set {_typingMessage = newValue} + } + /// Returns true if `typingMessage` has been explicitly set. + var hasTypingMessage: Bool {return self._typingMessage != nil} + /// Clears the value of `typingMessage`. Subsequent reads from it will return its default value. + mutating func clearTypingMessage() {self._typingMessage = nil} + + /// Loki + var prekeyBundleMessage: SignalServiceProtos_PrekeyBundleMessage { + get {return _prekeyBundleMessage ?? SignalServiceProtos_PrekeyBundleMessage()} + set {_prekeyBundleMessage = newValue} + } + /// Returns true if `prekeyBundleMessage` has been explicitly set. + var hasPrekeyBundleMessage: Bool {return self._prekeyBundleMessage != nil} + /// Clears the value of `prekeyBundleMessage`. Subsequent reads from it will return its default value. + mutating func clearPrekeyBundleMessage() {self._prekeyBundleMessage = nil} + + /// Loki + var lokiDeviceLinkMessage: SignalServiceProtos_LokiDeviceLinkMessage { + get {return _lokiDeviceLinkMessage ?? SignalServiceProtos_LokiDeviceLinkMessage()} + set {_lokiDeviceLinkMessage = newValue} + } + /// Returns true if `lokiDeviceLinkMessage` has been explicitly set. + var hasLokiDeviceLinkMessage: Bool {return self._lokiDeviceLinkMessage != nil} + /// Clears the value of `lokiDeviceLinkMessage`. Subsequent reads from it will return its default value. + mutating func clearLokiDeviceLinkMessage() {self._lokiDeviceLinkMessage = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _dataMessage: SignalServiceProtos_DataMessage? = nil + fileprivate var _syncMessage: SignalServiceProtos_SyncMessage? = nil + fileprivate var _callMessage: SignalServiceProtos_CallMessage? = nil + fileprivate var _nullMessage: SignalServiceProtos_NullMessage? = nil + fileprivate var _receiptMessage: SignalServiceProtos_ReceiptMessage? = nil + fileprivate var _typingMessage: SignalServiceProtos_TypingMessage? = nil + fileprivate var _prekeyBundleMessage: SignalServiceProtos_PrekeyBundleMessage? = nil + fileprivate var _lokiDeviceLinkMessage: SignalServiceProtos_LokiDeviceLinkMessage? = nil +} + +/// Loki +struct SignalServiceProtos_PrekeyBundleMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var identityKey: Data { + get {return _identityKey ?? SwiftProtobuf.Internal.emptyData} + set {_identityKey = newValue} + } + /// Returns true if `identityKey` has been explicitly set. + var hasIdentityKey: Bool {return self._identityKey != nil} + /// Clears the value of `identityKey`. Subsequent reads from it will return its default value. + mutating func clearIdentityKey() {self._identityKey = nil} + + var deviceID: UInt32 { + get {return _deviceID ?? 0} + set {_deviceID = newValue} + } + /// Returns true if `deviceID` has been explicitly set. + var hasDeviceID: Bool {return self._deviceID != nil} + /// Clears the value of `deviceID`. Subsequent reads from it will return its default value. + mutating func clearDeviceID() {self._deviceID = nil} + + var prekeyID: UInt32 { + get {return _prekeyID ?? 0} + set {_prekeyID = newValue} + } + /// Returns true if `prekeyID` has been explicitly set. + var hasPrekeyID: Bool {return self._prekeyID != nil} + /// Clears the value of `prekeyID`. Subsequent reads from it will return its default value. + mutating func clearPrekeyID() {self._prekeyID = nil} + + var signedKeyID: UInt32 { + get {return _signedKeyID ?? 0} + set {_signedKeyID = newValue} + } + /// Returns true if `signedKeyID` has been explicitly set. + var hasSignedKeyID: Bool {return self._signedKeyID != nil} + /// Clears the value of `signedKeyID`. Subsequent reads from it will return its default value. + mutating func clearSignedKeyID() {self._signedKeyID = nil} + + var prekey: Data { + get {return _prekey ?? SwiftProtobuf.Internal.emptyData} + set {_prekey = newValue} + } + /// Returns true if `prekey` has been explicitly set. + var hasPrekey: Bool {return self._prekey != nil} + /// Clears the value of `prekey`. Subsequent reads from it will return its default value. + mutating func clearPrekey() {self._prekey = nil} + + var signedKey: Data { + get {return _signedKey ?? SwiftProtobuf.Internal.emptyData} + set {_signedKey = newValue} + } + /// Returns true if `signedKey` has been explicitly set. + var hasSignedKey: Bool {return self._signedKey != nil} + /// Clears the value of `signedKey`. Subsequent reads from it will return its default value. + mutating func clearSignedKey() {self._signedKey = nil} + + var signature: Data { + get {return _signature ?? SwiftProtobuf.Internal.emptyData} + set {_signature = newValue} + } + /// Returns true if `signature` has been explicitly set. + var hasSignature: Bool {return self._signature != nil} + /// Clears the value of `signature`. Subsequent reads from it will return its default value. + mutating func clearSignature() {self._signature = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _identityKey: Data? = nil + fileprivate var _deviceID: UInt32? = nil + fileprivate var _prekeyID: UInt32? = nil + fileprivate var _signedKeyID: UInt32? = nil + fileprivate var _prekey: Data? = nil + fileprivate var _signedKey: Data? = nil + fileprivate var _signature: Data? = nil +} + +/// Loki +struct SignalServiceProtos_LokiDeviceLinkMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var masterPublicKey: String { + get {return _masterPublicKey ?? String()} + set {_masterPublicKey = newValue} + } + /// Returns true if `masterPublicKey` has been explicitly set. + var hasMasterPublicKey: Bool {return self._masterPublicKey != nil} + /// Clears the value of `masterPublicKey`. Subsequent reads from it will return its default value. + mutating func clearMasterPublicKey() {self._masterPublicKey = nil} + + var slavePublicKey: String { + get {return _slavePublicKey ?? String()} + set {_slavePublicKey = newValue} + } + /// Returns true if `slavePublicKey` has been explicitly set. + var hasSlavePublicKey: Bool {return self._slavePublicKey != nil} + /// Clears the value of `slavePublicKey`. Subsequent reads from it will return its default value. + mutating func clearSlavePublicKey() {self._slavePublicKey = nil} + + var slaveSignature: Data { + get {return _slaveSignature ?? SwiftProtobuf.Internal.emptyData} + set {_slaveSignature = newValue} + } + /// Returns true if `slaveSignature` has been explicitly set. + var hasSlaveSignature: Bool {return self._slaveSignature != nil} + /// Clears the value of `slaveSignature`. Subsequent reads from it will return its default value. + mutating func clearSlaveSignature() {self._slaveSignature = nil} + + var masterSignature: Data { + get {return _masterSignature ?? SwiftProtobuf.Internal.emptyData} + set {_masterSignature = newValue} + } + /// Returns true if `masterSignature` has been explicitly set. + var hasMasterSignature: Bool {return self._masterSignature != nil} + /// Clears the value of `masterSignature`. Subsequent reads from it will return its default value. + mutating func clearMasterSignature() {self._masterSignature = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _masterPublicKey: String? = nil + fileprivate var _slavePublicKey: String? = nil + fileprivate var _slaveSignature: Data? = nil + fileprivate var _masterSignature: Data? = nil +} + +struct SignalServiceProtos_CallMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var offer: SignalServiceProtos_CallMessage.Offer { + get {return _offer ?? SignalServiceProtos_CallMessage.Offer()} + set {_offer = newValue} + } + /// Returns true if `offer` has been explicitly set. + var hasOffer: Bool {return self._offer != nil} + /// Clears the value of `offer`. Subsequent reads from it will return its default value. + mutating func clearOffer() {self._offer = nil} + + var answer: SignalServiceProtos_CallMessage.Answer { + get {return _answer ?? SignalServiceProtos_CallMessage.Answer()} + set {_answer = newValue} + } + /// Returns true if `answer` has been explicitly set. + var hasAnswer: Bool {return self._answer != nil} + /// Clears the value of `answer`. Subsequent reads from it will return its default value. + mutating func clearAnswer() {self._answer = nil} + + var iceUpdate: [SignalServiceProtos_CallMessage.IceUpdate] = [] + + var hangup: SignalServiceProtos_CallMessage.Hangup { + get {return _hangup ?? SignalServiceProtos_CallMessage.Hangup()} + set {_hangup = newValue} + } + /// Returns true if `hangup` has been explicitly set. + var hasHangup: Bool {return self._hangup != nil} + /// Clears the value of `hangup`. Subsequent reads from it will return its default value. + mutating func clearHangup() {self._hangup = nil} + + var busy: SignalServiceProtos_CallMessage.Busy { + get {return _busy ?? SignalServiceProtos_CallMessage.Busy()} + set {_busy = newValue} + } + /// Returns true if `busy` has been explicitly set. + var hasBusy: Bool {return self._busy != nil} + /// Clears the value of `busy`. Subsequent reads from it will return its default value. + mutating func clearBusy() {self._busy = nil} + + /// Signal-iOS sends profile key with call messages + /// for earlier discovery + var profileKey: Data { + get {return _profileKey ?? SwiftProtobuf.Internal.emptyData} + set {_profileKey = newValue} + } + /// Returns true if `profileKey` has been explicitly set. + var hasProfileKey: Bool {return self._profileKey != nil} + /// Clears the value of `profileKey`. Subsequent reads from it will return its default value. + mutating func clearProfileKey() {self._profileKey = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct Offer { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + /// Signal-iOS renamed the description field to avoid + /// conflicts with [NSObject description]. + /// @required + var sessionDescription: String { + get {return _sessionDescription ?? String()} + set {_sessionDescription = newValue} + } + /// Returns true if `sessionDescription` has been explicitly set. + var hasSessionDescription: Bool {return self._sessionDescription != nil} + /// Clears the value of `sessionDescription`. Subsequent reads from it will return its default value. + mutating func clearSessionDescription() {self._sessionDescription = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _id: UInt64? = nil + fileprivate var _sessionDescription: String? = nil + } + + struct Answer { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + /// Signal-iOS renamed the description field to avoid + /// conflicts with [NSObject description]. + /// @required + var sessionDescription: String { + get {return _sessionDescription ?? String()} + set {_sessionDescription = newValue} + } + /// Returns true if `sessionDescription` has been explicitly set. + var hasSessionDescription: Bool {return self._sessionDescription != nil} + /// Clears the value of `sessionDescription`. Subsequent reads from it will return its default value. + mutating func clearSessionDescription() {self._sessionDescription = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _id: UInt64? = nil + fileprivate var _sessionDescription: String? = nil + } + + struct IceUpdate { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + /// @required + var sdpMid: String { + get {return _sdpMid ?? String()} + set {_sdpMid = newValue} + } + /// Returns true if `sdpMid` has been explicitly set. + var hasSdpMid: Bool {return self._sdpMid != nil} + /// Clears the value of `sdpMid`. Subsequent reads from it will return its default value. + mutating func clearSdpMid() {self._sdpMid = nil} + + /// @required + var sdpMlineIndex: UInt32 { + get {return _sdpMlineIndex ?? 0} + set {_sdpMlineIndex = newValue} + } + /// Returns true if `sdpMlineIndex` has been explicitly set. + var hasSdpMlineIndex: Bool {return self._sdpMlineIndex != nil} + /// Clears the value of `sdpMlineIndex`. Subsequent reads from it will return its default value. + mutating func clearSdpMlineIndex() {self._sdpMlineIndex = nil} + + /// @required + var sdp: String { + get {return _sdp ?? String()} + set {_sdp = newValue} + } + /// Returns true if `sdp` has been explicitly set. + var hasSdp: Bool {return self._sdp != nil} + /// Clears the value of `sdp`. Subsequent reads from it will return its default value. + mutating func clearSdp() {self._sdp = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _id: UInt64? = nil + fileprivate var _sdpMid: String? = nil + fileprivate var _sdpMlineIndex: UInt32? = nil + fileprivate var _sdp: String? = nil + } + + struct Busy { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _id: UInt64? = nil + } + + struct Hangup { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _id: UInt64? = nil + } + + init() {} + + fileprivate var _offer: SignalServiceProtos_CallMessage.Offer? = nil + fileprivate var _answer: SignalServiceProtos_CallMessage.Answer? = nil + fileprivate var _hangup: SignalServiceProtos_CallMessage.Hangup? = nil + fileprivate var _busy: SignalServiceProtos_CallMessage.Busy? = nil + fileprivate var _profileKey: Data? = nil +} + +struct SignalServiceProtos_ClosedGroupCiphertextMessageWrapper { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var ciphertext: Data { + get {return _ciphertext ?? SwiftProtobuf.Internal.emptyData} + set {_ciphertext = newValue} + } + /// Returns true if `ciphertext` has been explicitly set. + var hasCiphertext: Bool {return self._ciphertext != nil} + /// Clears the value of `ciphertext`. Subsequent reads from it will return its default value. + mutating func clearCiphertext() {self._ciphertext = nil} + + /// @required + var ephemeralPublicKey: Data { + get {return _ephemeralPublicKey ?? SwiftProtobuf.Internal.emptyData} + set {_ephemeralPublicKey = newValue} + } + /// Returns true if `ephemeralPublicKey` has been explicitly set. + var hasEphemeralPublicKey: Bool {return self._ephemeralPublicKey != nil} + /// Clears the value of `ephemeralPublicKey`. Subsequent reads from it will return its default value. + mutating func clearEphemeralPublicKey() {self._ephemeralPublicKey = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _ciphertext: Data? = nil + fileprivate var _ephemeralPublicKey: Data? = nil +} + +struct SignalServiceProtos_DataMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var body: String { + get {return _body ?? String()} + set {_body = newValue} + } + /// Returns true if `body` has been explicitly set. + var hasBody: Bool {return self._body != nil} + /// Clears the value of `body`. Subsequent reads from it will return its default value. + mutating func clearBody() {self._body = nil} + + var attachments: [SignalServiceProtos_AttachmentPointer] = [] + + var group: SignalServiceProtos_GroupContext { + get {return _group ?? SignalServiceProtos_GroupContext()} + set {_group = newValue} + } + /// Returns true if `group` has been explicitly set. + var hasGroup: Bool {return self._group != nil} + /// Clears the value of `group`. Subsequent reads from it will return its default value. + mutating func clearGroup() {self._group = nil} + + var flags: UInt32 { + get {return _flags ?? 0} + set {_flags = newValue} + } + /// Returns true if `flags` has been explicitly set. + var hasFlags: Bool {return self._flags != nil} + /// Clears the value of `flags`. Subsequent reads from it will return its default value. + mutating func clearFlags() {self._flags = nil} + + var expireTimer: UInt32 { + get {return _expireTimer ?? 0} + set {_expireTimer = newValue} + } + /// Returns true if `expireTimer` has been explicitly set. + var hasExpireTimer: Bool {return self._expireTimer != nil} + /// Clears the value of `expireTimer`. Subsequent reads from it will return its default value. + mutating func clearExpireTimer() {self._expireTimer = nil} + + var profileKey: Data { + get {return _profileKey ?? SwiftProtobuf.Internal.emptyData} + set {_profileKey = newValue} + } + /// Returns true if `profileKey` has been explicitly set. + var hasProfileKey: Bool {return self._profileKey != nil} + /// Clears the value of `profileKey`. Subsequent reads from it will return its default value. + mutating func clearProfileKey() {self._profileKey = nil} + + var timestamp: UInt64 { + get {return _timestamp ?? 0} + set {_timestamp = newValue} + } + /// Returns true if `timestamp` has been explicitly set. + var hasTimestamp: Bool {return self._timestamp != nil} + /// Clears the value of `timestamp`. Subsequent reads from it will return its default value. + mutating func clearTimestamp() {self._timestamp = nil} + + var quote: SignalServiceProtos_DataMessage.Quote { + get {return _quote ?? SignalServiceProtos_DataMessage.Quote()} + set {_quote = newValue} + } + /// Returns true if `quote` has been explicitly set. + var hasQuote: Bool {return self._quote != nil} + /// Clears the value of `quote`. Subsequent reads from it will return its default value. + mutating func clearQuote() {self._quote = nil} + + var contact: [SignalServiceProtos_DataMessage.Contact] = [] + + var preview: [SignalServiceProtos_DataMessage.Preview] = [] + + /// Loki: The current user's profile + var profile: SignalServiceProtos_DataMessage.LokiProfile { + get {return _profile ?? SignalServiceProtos_DataMessage.LokiProfile()} + set {_profile = newValue} + } + /// Returns true if `profile` has been explicitly set. + var hasProfile: Bool {return self._profile != nil} + /// Clears the value of `profile`. Subsequent reads from it will return its default value. + mutating func clearProfile() {self._profile = nil} + + /// Loki + var closedGroupUpdate: SignalServiceProtos_DataMessage.ClosedGroupUpdate { + get {return _closedGroupUpdate ?? SignalServiceProtos_DataMessage.ClosedGroupUpdate()} + set {_closedGroupUpdate = newValue} + } + /// Returns true if `closedGroupUpdate` has been explicitly set. + var hasClosedGroupUpdate: Bool {return self._closedGroupUpdate != nil} + /// Clears the value of `closedGroupUpdate`. Subsequent reads from it will return its default value. + mutating func clearClosedGroupUpdate() {self._closedGroupUpdate = nil} + + /// Loki: Internal public chat info + var publicChatInfo: SignalServiceProtos_PublicChatInfo { + get {return _publicChatInfo ?? SignalServiceProtos_PublicChatInfo()} + set {_publicChatInfo = newValue} + } + /// Returns true if `publicChatInfo` has been explicitly set. + var hasPublicChatInfo: Bool {return self._publicChatInfo != nil} + /// Clears the value of `publicChatInfo`. Subsequent reads from it will return its default value. + mutating func clearPublicChatInfo() {self._publicChatInfo = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum Flags: SwiftProtobuf.Enum { + typealias RawValue = Int + case endSession // = 1 + case expirationTimerUpdate // = 2 + case profileKeyUpdate // = 4 + case unlinkDevice // = 128 + + init() { + self = .endSession + } + + init?(rawValue: Int) { + switch rawValue { + case 1: self = .endSession + case 2: self = .expirationTimerUpdate + case 4: self = .profileKeyUpdate + case 128: self = .unlinkDevice + default: return nil + } + } + + var rawValue: Int { + switch self { + case .endSession: return 1 + case .expirationTimerUpdate: return 2 + case .profileKeyUpdate: return 4 + case .unlinkDevice: return 128 + } + } + + } + + struct Quote { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + /// @required + var author: String { + get {return _author ?? String()} + set {_author = newValue} + } + /// Returns true if `author` has been explicitly set. + var hasAuthor: Bool {return self._author != nil} + /// Clears the value of `author`. Subsequent reads from it will return its default value. + mutating func clearAuthor() {self._author = nil} + + var text: String { + get {return _text ?? String()} + set {_text = newValue} + } + /// Returns true if `text` has been explicitly set. + var hasText: Bool {return self._text != nil} + /// Clears the value of `text`. Subsequent reads from it will return its default value. + mutating func clearText() {self._text = nil} + + var attachments: [SignalServiceProtos_DataMessage.Quote.QuotedAttachment] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct QuotedAttachment { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var contentType: String { + get {return _contentType ?? String()} + set {_contentType = newValue} + } + /// Returns true if `contentType` has been explicitly set. + var hasContentType: Bool {return self._contentType != nil} + /// Clears the value of `contentType`. Subsequent reads from it will return its default value. + mutating func clearContentType() {self._contentType = nil} + + var fileName: String { + get {return _fileName ?? String()} + set {_fileName = newValue} + } + /// Returns true if `fileName` has been explicitly set. + var hasFileName: Bool {return self._fileName != nil} + /// Clears the value of `fileName`. Subsequent reads from it will return its default value. + mutating func clearFileName() {self._fileName = nil} + + var thumbnail: SignalServiceProtos_AttachmentPointer { + get {return _thumbnail ?? SignalServiceProtos_AttachmentPointer()} + set {_thumbnail = newValue} + } + /// Returns true if `thumbnail` has been explicitly set. + var hasThumbnail: Bool {return self._thumbnail != nil} + /// Clears the value of `thumbnail`. Subsequent reads from it will return its default value. + mutating func clearThumbnail() {self._thumbnail = nil} + + var flags: UInt32 { + get {return _flags ?? 0} + set {_flags = newValue} + } + /// Returns true if `flags` has been explicitly set. + var hasFlags: Bool {return self._flags != nil} + /// Clears the value of `flags`. Subsequent reads from it will return its default value. + mutating func clearFlags() {self._flags = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum Flags: SwiftProtobuf.Enum { + typealias RawValue = Int + case voiceMessage // = 1 + + init() { + self = .voiceMessage + } + + init?(rawValue: Int) { + switch rawValue { + case 1: self = .voiceMessage + default: return nil + } + } + + var rawValue: Int { + switch self { + case .voiceMessage: return 1 + } + } + + } + + init() {} + + fileprivate var _contentType: String? = nil + fileprivate var _fileName: String? = nil + fileprivate var _thumbnail: SignalServiceProtos_AttachmentPointer? = nil + fileprivate var _flags: UInt32? = nil + } + + init() {} + + fileprivate var _id: UInt64? = nil + fileprivate var _author: String? = nil + fileprivate var _text: String? = nil + } + + struct Contact { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: SignalServiceProtos_DataMessage.Contact.Name { + get {return _name ?? SignalServiceProtos_DataMessage.Contact.Name()} + set {_name = newValue} + } + /// Returns true if `name` has been explicitly set. + var hasName: Bool {return self._name != nil} + /// Clears the value of `name`. Subsequent reads from it will return its default value. + mutating func clearName() {self._name = nil} + + var number: [SignalServiceProtos_DataMessage.Contact.Phone] = [] + + var email: [SignalServiceProtos_DataMessage.Contact.Email] = [] + + var address: [SignalServiceProtos_DataMessage.Contact.PostalAddress] = [] + + var avatar: SignalServiceProtos_DataMessage.Contact.Avatar { + get {return _avatar ?? SignalServiceProtos_DataMessage.Contact.Avatar()} + set {_avatar = newValue} + } + /// Returns true if `avatar` has been explicitly set. + var hasAvatar: Bool {return self._avatar != nil} + /// Clears the value of `avatar`. Subsequent reads from it will return its default value. + mutating func clearAvatar() {self._avatar = nil} + + var organization: String { + get {return _organization ?? String()} + set {_organization = newValue} + } + /// Returns true if `organization` has been explicitly set. + var hasOrganization: Bool {return self._organization != nil} + /// Clears the value of `organization`. Subsequent reads from it will return its default value. + mutating func clearOrganization() {self._organization = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct Name { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var givenName: String { + get {return _givenName ?? String()} + set {_givenName = newValue} + } + /// Returns true if `givenName` has been explicitly set. + var hasGivenName: Bool {return self._givenName != nil} + /// Clears the value of `givenName`. Subsequent reads from it will return its default value. + mutating func clearGivenName() {self._givenName = nil} + + var familyName: String { + get {return _familyName ?? String()} + set {_familyName = newValue} + } + /// Returns true if `familyName` has been explicitly set. + var hasFamilyName: Bool {return self._familyName != nil} + /// Clears the value of `familyName`. Subsequent reads from it will return its default value. + mutating func clearFamilyName() {self._familyName = nil} + + var prefix: String { + get {return _prefix ?? String()} + set {_prefix = newValue} + } + /// Returns true if `prefix` has been explicitly set. + var hasPrefix: Bool {return self._prefix != nil} + /// Clears the value of `prefix`. Subsequent reads from it will return its default value. + mutating func clearPrefix() {self._prefix = nil} + + var suffix: String { + get {return _suffix ?? String()} + set {_suffix = newValue} + } + /// Returns true if `suffix` has been explicitly set. + var hasSuffix: Bool {return self._suffix != nil} + /// Clears the value of `suffix`. Subsequent reads from it will return its default value. + mutating func clearSuffix() {self._suffix = nil} + + var middleName: String { + get {return _middleName ?? String()} + set {_middleName = newValue} + } + /// Returns true if `middleName` has been explicitly set. + var hasMiddleName: Bool {return self._middleName != nil} + /// Clears the value of `middleName`. Subsequent reads from it will return its default value. + mutating func clearMiddleName() {self._middleName = nil} + + var displayName: String { + get {return _displayName ?? String()} + set {_displayName = newValue} + } + /// Returns true if `displayName` has been explicitly set. + var hasDisplayName: Bool {return self._displayName != nil} + /// Clears the value of `displayName`. Subsequent reads from it will return its default value. + mutating func clearDisplayName() {self._displayName = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _givenName: String? = nil + fileprivate var _familyName: String? = nil + fileprivate var _prefix: String? = nil + fileprivate var _suffix: String? = nil + fileprivate var _middleName: String? = nil + fileprivate var _displayName: String? = nil + } + + struct Phone { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var value: String { + get {return _value ?? String()} + set {_value = newValue} + } + /// Returns true if `value` has been explicitly set. + var hasValue: Bool {return self._value != nil} + /// Clears the value of `value`. Subsequent reads from it will return its default value. + mutating func clearValue() {self._value = nil} + + var type: SignalServiceProtos_DataMessage.Contact.Phone.TypeEnum { + get {return _type ?? .home} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var label: String { + get {return _label ?? String()} + set {_label = newValue} + } + /// Returns true if `label` has been explicitly set. + var hasLabel: Bool {return self._label != nil} + /// Clears the value of `label`. Subsequent reads from it will return its default value. + mutating func clearLabel() {self._label = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case home // = 1 + case mobile // = 2 + case work // = 3 + case custom // = 4 + + init() { + self = .home + } + + init?(rawValue: Int) { + switch rawValue { + case 1: self = .home + case 2: self = .mobile + case 3: self = .work + case 4: self = .custom + default: return nil + } + } + + var rawValue: Int { + switch self { + case .home: return 1 + case .mobile: return 2 + case .work: return 3 + case .custom: return 4 + } + } + + } + + init() {} + + fileprivate var _value: String? = nil + fileprivate var _type: SignalServiceProtos_DataMessage.Contact.Phone.TypeEnum? = nil + fileprivate var _label: String? = nil + } + + struct Email { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var value: String { + get {return _value ?? String()} + set {_value = newValue} + } + /// Returns true if `value` has been explicitly set. + var hasValue: Bool {return self._value != nil} + /// Clears the value of `value`. Subsequent reads from it will return its default value. + mutating func clearValue() {self._value = nil} + + var type: SignalServiceProtos_DataMessage.Contact.Email.TypeEnum { + get {return _type ?? .home} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var label: String { + get {return _label ?? String()} + set {_label = newValue} + } + /// Returns true if `label` has been explicitly set. + var hasLabel: Bool {return self._label != nil} + /// Clears the value of `label`. Subsequent reads from it will return its default value. + mutating func clearLabel() {self._label = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case home // = 1 + case mobile // = 2 + case work // = 3 + case custom // = 4 + + init() { + self = .home + } + + init?(rawValue: Int) { + switch rawValue { + case 1: self = .home + case 2: self = .mobile + case 3: self = .work + case 4: self = .custom + default: return nil + } + } + + var rawValue: Int { + switch self { + case .home: return 1 + case .mobile: return 2 + case .work: return 3 + case .custom: return 4 + } + } + + } + + init() {} + + fileprivate var _value: String? = nil + fileprivate var _type: SignalServiceProtos_DataMessage.Contact.Email.TypeEnum? = nil + fileprivate var _label: String? = nil + } + + struct PostalAddress { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var type: SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum { + get {return _type ?? .home} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var label: String { + get {return _label ?? String()} + set {_label = newValue} + } + /// Returns true if `label` has been explicitly set. + var hasLabel: Bool {return self._label != nil} + /// Clears the value of `label`. Subsequent reads from it will return its default value. + mutating func clearLabel() {self._label = nil} + + var street: String { + get {return _street ?? String()} + set {_street = newValue} + } + /// Returns true if `street` has been explicitly set. + var hasStreet: Bool {return self._street != nil} + /// Clears the value of `street`. Subsequent reads from it will return its default value. + mutating func clearStreet() {self._street = nil} + + var pobox: String { + get {return _pobox ?? String()} + set {_pobox = newValue} + } + /// Returns true if `pobox` has been explicitly set. + var hasPobox: Bool {return self._pobox != nil} + /// Clears the value of `pobox`. Subsequent reads from it will return its default value. + mutating func clearPobox() {self._pobox = nil} + + var neighborhood: String { + get {return _neighborhood ?? String()} + set {_neighborhood = newValue} + } + /// Returns true if `neighborhood` has been explicitly set. + var hasNeighborhood: Bool {return self._neighborhood != nil} + /// Clears the value of `neighborhood`. Subsequent reads from it will return its default value. + mutating func clearNeighborhood() {self._neighborhood = nil} + + var city: String { + get {return _city ?? String()} + set {_city = newValue} + } + /// Returns true if `city` has been explicitly set. + var hasCity: Bool {return self._city != nil} + /// Clears the value of `city`. Subsequent reads from it will return its default value. + mutating func clearCity() {self._city = nil} + + var region: String { + get {return _region ?? String()} + set {_region = newValue} + } + /// Returns true if `region` has been explicitly set. + var hasRegion: Bool {return self._region != nil} + /// Clears the value of `region`. Subsequent reads from it will return its default value. + mutating func clearRegion() {self._region = nil} + + var postcode: String { + get {return _postcode ?? String()} + set {_postcode = newValue} + } + /// Returns true if `postcode` has been explicitly set. + var hasPostcode: Bool {return self._postcode != nil} + /// Clears the value of `postcode`. Subsequent reads from it will return its default value. + mutating func clearPostcode() {self._postcode = nil} + + var country: String { + get {return _country ?? String()} + set {_country = newValue} + } + /// Returns true if `country` has been explicitly set. + var hasCountry: Bool {return self._country != nil} + /// Clears the value of `country`. Subsequent reads from it will return its default value. + mutating func clearCountry() {self._country = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case home // = 1 + case work // = 2 + case custom // = 3 + + init() { + self = .home + } + + init?(rawValue: Int) { + switch rawValue { + case 1: self = .home + case 2: self = .work + case 3: self = .custom + default: return nil + } + } + + var rawValue: Int { + switch self { + case .home: return 1 + case .work: return 2 + case .custom: return 3 + } + } + + } + + init() {} + + fileprivate var _type: SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum? = nil + fileprivate var _label: String? = nil + fileprivate var _street: String? = nil + fileprivate var _pobox: String? = nil + fileprivate var _neighborhood: String? = nil + fileprivate var _city: String? = nil + fileprivate var _region: String? = nil + fileprivate var _postcode: String? = nil + fileprivate var _country: String? = nil + } + + struct Avatar { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var avatar: SignalServiceProtos_AttachmentPointer { + get {return _avatar ?? SignalServiceProtos_AttachmentPointer()} + set {_avatar = newValue} + } + /// Returns true if `avatar` has been explicitly set. + var hasAvatar: Bool {return self._avatar != nil} + /// Clears the value of `avatar`. Subsequent reads from it will return its default value. + mutating func clearAvatar() {self._avatar = nil} + + var isProfile: Bool { + get {return _isProfile ?? false} + set {_isProfile = newValue} + } + /// Returns true if `isProfile` has been explicitly set. + var hasIsProfile: Bool {return self._isProfile != nil} + /// Clears the value of `isProfile`. Subsequent reads from it will return its default value. + mutating func clearIsProfile() {self._isProfile = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _avatar: SignalServiceProtos_AttachmentPointer? = nil + fileprivate var _isProfile: Bool? = nil + } + + init() {} + + fileprivate var _name: SignalServiceProtos_DataMessage.Contact.Name? = nil + fileprivate var _avatar: SignalServiceProtos_DataMessage.Contact.Avatar? = nil + fileprivate var _organization: String? = nil + } + + struct Preview { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var url: String { + get {return _url ?? String()} + set {_url = newValue} + } + /// Returns true if `url` has been explicitly set. + var hasURL: Bool {return self._url != nil} + /// Clears the value of `url`. Subsequent reads from it will return its default value. + mutating func clearURL() {self._url = nil} + + var title: String { + get {return _title ?? String()} + set {_title = newValue} + } + /// Returns true if `title` has been explicitly set. + var hasTitle: Bool {return self._title != nil} + /// Clears the value of `title`. Subsequent reads from it will return its default value. + mutating func clearTitle() {self._title = nil} + + var image: SignalServiceProtos_AttachmentPointer { + get {return _image ?? SignalServiceProtos_AttachmentPointer()} + set {_image = newValue} + } + /// Returns true if `image` has been explicitly set. + var hasImage: Bool {return self._image != nil} + /// Clears the value of `image`. Subsequent reads from it will return its default value. + mutating func clearImage() {self._image = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _url: String? = nil + fileprivate var _title: String? = nil + fileprivate var _image: SignalServiceProtos_AttachmentPointer? = nil + } + + /// Loki + struct LokiProfile { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var displayName: String { + get {return _displayName ?? String()} + set {_displayName = newValue} + } + /// Returns true if `displayName` has been explicitly set. + var hasDisplayName: Bool {return self._displayName != nil} + /// Clears the value of `displayName`. Subsequent reads from it will return its default value. + mutating func clearDisplayName() {self._displayName = nil} + + var profilePicture: String { + get {return _profilePicture ?? String()} + set {_profilePicture = newValue} + } + /// Returns true if `profilePicture` has been explicitly set. + var hasProfilePicture: Bool {return self._profilePicture != nil} + /// Clears the value of `profilePicture`. Subsequent reads from it will return its default value. + mutating func clearProfilePicture() {self._profilePicture = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _displayName: String? = nil + fileprivate var _profilePicture: String? = nil + } + + /// Loki + struct ClosedGroupUpdate { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: String { + get {return _name ?? String()} + set {_name = newValue} + } + /// Returns true if `name` has been explicitly set. + var hasName: Bool {return self._name != nil} + /// Clears the value of `name`. Subsequent reads from it will return its default value. + mutating func clearName() {self._name = nil} + + /// @required + var groupPublicKey: Data { + get {return _groupPublicKey ?? SwiftProtobuf.Internal.emptyData} + set {_groupPublicKey = newValue} + } + /// Returns true if `groupPublicKey` has been explicitly set. + var hasGroupPublicKey: Bool {return self._groupPublicKey != nil} + /// Clears the value of `groupPublicKey`. Subsequent reads from it will return its default value. + mutating func clearGroupPublicKey() {self._groupPublicKey = nil} + + var groupPrivateKey: Data { + get {return _groupPrivateKey ?? SwiftProtobuf.Internal.emptyData} + set {_groupPrivateKey = newValue} + } + /// Returns true if `groupPrivateKey` has been explicitly set. + var hasGroupPrivateKey: Bool {return self._groupPrivateKey != nil} + /// Clears the value of `groupPrivateKey`. Subsequent reads from it will return its default value. + mutating func clearGroupPrivateKey() {self._groupPrivateKey = nil} + + var senderKeys: [SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey] = [] + + var members: [Data] = [] + + var admins: [Data] = [] + + /// @required + var type: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum { + get {return _type ?? .new} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// groupPublicKey, name, groupPrivateKey, senderKeys, members, admins + case new // = 0 + + /// groupPublicKey, name, senderKeys, members, admins + case info // = 1 + + /// groupPublicKey + case senderKeyRequest // = 2 + + /// groupPublicKey, senderKeys + case senderKey // = 3 + + init() { + self = .new + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .new + case 1: self = .info + case 2: self = .senderKeyRequest + case 3: self = .senderKey + default: return nil + } + } + + var rawValue: Int { + switch self { + case .new: return 0 + case .info: return 1 + case .senderKeyRequest: return 2 + case .senderKey: return 3 + } + } + + } + + struct SenderKey { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var chainKey: Data { + get {return _chainKey ?? SwiftProtobuf.Internal.emptyData} + set {_chainKey = newValue} + } + /// Returns true if `chainKey` has been explicitly set. + var hasChainKey: Bool {return self._chainKey != nil} + /// Clears the value of `chainKey`. Subsequent reads from it will return its default value. + mutating func clearChainKey() {self._chainKey = nil} + + /// @required + var keyIndex: UInt32 { + get {return _keyIndex ?? 0} + set {_keyIndex = newValue} + } + /// Returns true if `keyIndex` has been explicitly set. + var hasKeyIndex: Bool {return self._keyIndex != nil} + /// Clears the value of `keyIndex`. Subsequent reads from it will return its default value. + mutating func clearKeyIndex() {self._keyIndex = nil} + + /// @required + var publicKey: Data { + get {return _publicKey ?? SwiftProtobuf.Internal.emptyData} + set {_publicKey = newValue} + } + /// Returns true if `publicKey` has been explicitly set. + var hasPublicKey: Bool {return self._publicKey != nil} + /// Clears the value of `publicKey`. Subsequent reads from it will return its default value. + mutating func clearPublicKey() {self._publicKey = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _chainKey: Data? = nil + fileprivate var _keyIndex: UInt32? = nil + fileprivate var _publicKey: Data? = nil + } + + init() {} + + fileprivate var _name: String? = nil + fileprivate var _groupPublicKey: Data? = nil + fileprivate var _groupPrivateKey: Data? = nil + fileprivate var _type: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum? = nil + } + + init() {} + + fileprivate var _body: String? = nil + fileprivate var _group: SignalServiceProtos_GroupContext? = nil + fileprivate var _flags: UInt32? = nil + fileprivate var _expireTimer: UInt32? = nil + fileprivate var _profileKey: Data? = nil + fileprivate var _timestamp: UInt64? = nil + fileprivate var _quote: SignalServiceProtos_DataMessage.Quote? = nil + fileprivate var _profile: SignalServiceProtos_DataMessage.LokiProfile? = nil + fileprivate var _closedGroupUpdate: SignalServiceProtos_DataMessage.ClosedGroupUpdate? = nil + fileprivate var _publicChatInfo: SignalServiceProtos_PublicChatInfo? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_DataMessage.Flags: CaseIterable { + // Support synthesized by the compiler. +} + +extension SignalServiceProtos_DataMessage.Quote.QuotedAttachment.Flags: CaseIterable { + // Support synthesized by the compiler. +} + +extension SignalServiceProtos_DataMessage.Contact.Phone.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +extension SignalServiceProtos_DataMessage.Contact.Email.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +extension SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_NullMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var padding: Data { + get {return _padding ?? SwiftProtobuf.Internal.emptyData} + set {_padding = newValue} + } + /// Returns true if `padding` has been explicitly set. + var hasPadding: Bool {return self._padding != nil} + /// Clears the value of `padding`. Subsequent reads from it will return its default value. + mutating func clearPadding() {self._padding = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _padding: Data? = nil +} + +struct SignalServiceProtos_ReceiptMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var type: SignalServiceProtos_ReceiptMessage.TypeEnum { + get {return _type ?? .delivery} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var timestamp: [UInt64] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case delivery // = 0 + case read // = 1 + + init() { + self = .delivery + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .delivery + case 1: self = .read + default: return nil + } + } + + var rawValue: Int { + switch self { + case .delivery: return 0 + case .read: return 1 + } + } + + } + + init() {} + + fileprivate var _type: SignalServiceProtos_ReceiptMessage.TypeEnum? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_ReceiptMessage.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_Verified { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var destination: String { + get {return _destination ?? String()} + set {_destination = newValue} + } + /// Returns true if `destination` has been explicitly set. + var hasDestination: Bool {return self._destination != nil} + /// Clears the value of `destination`. Subsequent reads from it will return its default value. + mutating func clearDestination() {self._destination = nil} + + var identityKey: Data { + get {return _identityKey ?? SwiftProtobuf.Internal.emptyData} + set {_identityKey = newValue} + } + /// Returns true if `identityKey` has been explicitly set. + var hasIdentityKey: Bool {return self._identityKey != nil} + /// Clears the value of `identityKey`. Subsequent reads from it will return its default value. + mutating func clearIdentityKey() {self._identityKey = nil} + + var state: SignalServiceProtos_Verified.State { + get {return _state ?? .default} + set {_state = newValue} + } + /// Returns true if `state` has been explicitly set. + var hasState: Bool {return self._state != nil} + /// Clears the value of `state`. Subsequent reads from it will return its default value. + mutating func clearState() {self._state = nil} + + var nullMessage: Data { + get {return _nullMessage ?? SwiftProtobuf.Internal.emptyData} + set {_nullMessage = newValue} + } + /// Returns true if `nullMessage` has been explicitly set. + var hasNullMessage: Bool {return self._nullMessage != nil} + /// Clears the value of `nullMessage`. Subsequent reads from it will return its default value. + mutating func clearNullMessage() {self._nullMessage = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum State: SwiftProtobuf.Enum { + typealias RawValue = Int + case `default` // = 0 + case verified // = 1 + case unverified // = 2 + + init() { + self = .default + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .default + case 1: self = .verified + case 2: self = .unverified + default: return nil + } + } + + var rawValue: Int { + switch self { + case .default: return 0 + case .verified: return 1 + case .unverified: return 2 + } + } + + } + + init() {} + + fileprivate var _destination: String? = nil + fileprivate var _identityKey: Data? = nil + fileprivate var _state: SignalServiceProtos_Verified.State? = nil + fileprivate var _nullMessage: Data? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_Verified.State: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_SyncMessage { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var sent: SignalServiceProtos_SyncMessage.Sent { + get {return _sent ?? SignalServiceProtos_SyncMessage.Sent()} + set {_sent = newValue} + } + /// Returns true if `sent` has been explicitly set. + var hasSent: Bool {return self._sent != nil} + /// Clears the value of `sent`. Subsequent reads from it will return its default value. + mutating func clearSent() {self._sent = nil} + + var contacts: SignalServiceProtos_SyncMessage.Contacts { + get {return _contacts ?? SignalServiceProtos_SyncMessage.Contacts()} + set {_contacts = newValue} + } + /// Returns true if `contacts` has been explicitly set. + var hasContacts: Bool {return self._contacts != nil} + /// Clears the value of `contacts`. Subsequent reads from it will return its default value. + mutating func clearContacts() {self._contacts = nil} + + var groups: SignalServiceProtos_SyncMessage.Groups { + get {return _groups ?? SignalServiceProtos_SyncMessage.Groups()} + set {_groups = newValue} + } + /// Returns true if `groups` has been explicitly set. + var hasGroups: Bool {return self._groups != nil} + /// Clears the value of `groups`. Subsequent reads from it will return its default value. + mutating func clearGroups() {self._groups = nil} + + var request: SignalServiceProtos_SyncMessage.Request { + get {return _request ?? SignalServiceProtos_SyncMessage.Request()} + set {_request = newValue} + } + /// Returns true if `request` has been explicitly set. + var hasRequest: Bool {return self._request != nil} + /// Clears the value of `request`. Subsequent reads from it will return its default value. + mutating func clearRequest() {self._request = nil} + + var read: [SignalServiceProtos_SyncMessage.Read] = [] + + var blocked: SignalServiceProtos_SyncMessage.Blocked { + get {return _blocked ?? SignalServiceProtos_SyncMessage.Blocked()} + set {_blocked = newValue} + } + /// Returns true if `blocked` has been explicitly set. + var hasBlocked: Bool {return self._blocked != nil} + /// Clears the value of `blocked`. Subsequent reads from it will return its default value. + mutating func clearBlocked() {self._blocked = nil} + + var verified: SignalServiceProtos_Verified { + get {return _verified ?? SignalServiceProtos_Verified()} + set {_verified = newValue} + } + /// Returns true if `verified` has been explicitly set. + var hasVerified: Bool {return self._verified != nil} + /// Clears the value of `verified`. Subsequent reads from it will return its default value. + mutating func clearVerified() {self._verified = nil} + + var configuration: SignalServiceProtos_SyncMessage.Configuration { + get {return _configuration ?? SignalServiceProtos_SyncMessage.Configuration()} + set {_configuration = newValue} + } + /// Returns true if `configuration` has been explicitly set. + var hasConfiguration: Bool {return self._configuration != nil} + /// Clears the value of `configuration`. Subsequent reads from it will return its default value. + mutating func clearConfiguration() {self._configuration = nil} + + var padding: Data { + get {return _padding ?? SwiftProtobuf.Internal.emptyData} + set {_padding = newValue} + } + /// Returns true if `padding` has been explicitly set. + var hasPadding: Bool {return self._padding != nil} + /// Clears the value of `padding`. Subsequent reads from it will return its default value. + mutating func clearPadding() {self._padding = nil} + + var openGroups: [SignalServiceProtos_SyncMessage.OpenGroupDetails] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct Sent { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var destination: String { + get {return _destination ?? String()} + set {_destination = newValue} + } + /// Returns true if `destination` has been explicitly set. + var hasDestination: Bool {return self._destination != nil} + /// Clears the value of `destination`. Subsequent reads from it will return its default value. + mutating func clearDestination() {self._destination = nil} + + var timestamp: UInt64 { + get {return _timestamp ?? 0} + set {_timestamp = newValue} + } + /// Returns true if `timestamp` has been explicitly set. + var hasTimestamp: Bool {return self._timestamp != nil} + /// Clears the value of `timestamp`. Subsequent reads from it will return its default value. + mutating func clearTimestamp() {self._timestamp = nil} + + var message: SignalServiceProtos_DataMessage { + get {return _message ?? SignalServiceProtos_DataMessage()} + set {_message = newValue} + } + /// Returns true if `message` has been explicitly set. + var hasMessage: Bool {return self._message != nil} + /// Clears the value of `message`. Subsequent reads from it will return its default value. + mutating func clearMessage() {self._message = nil} + + var expirationStartTimestamp: UInt64 { + get {return _expirationStartTimestamp ?? 0} + set {_expirationStartTimestamp = newValue} + } + /// Returns true if `expirationStartTimestamp` has been explicitly set. + var hasExpirationStartTimestamp: Bool {return self._expirationStartTimestamp != nil} + /// Clears the value of `expirationStartTimestamp`. Subsequent reads from it will return its default value. + mutating func clearExpirationStartTimestamp() {self._expirationStartTimestamp = nil} + + var unidentifiedStatus: [SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus] = [] + + var isRecipientUpdate: Bool { + get {return _isRecipientUpdate ?? false} + set {_isRecipientUpdate = newValue} + } + /// Returns true if `isRecipientUpdate` has been explicitly set. + var hasIsRecipientUpdate: Bool {return self._isRecipientUpdate != nil} + /// Clears the value of `isRecipientUpdate`. Subsequent reads from it will return its default value. + mutating func clearIsRecipientUpdate() {self._isRecipientUpdate = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct UnidentifiedDeliveryStatus { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var destination: String { + get {return _destination ?? String()} + set {_destination = newValue} + } + /// Returns true if `destination` has been explicitly set. + var hasDestination: Bool {return self._destination != nil} + /// Clears the value of `destination`. Subsequent reads from it will return its default value. + mutating func clearDestination() {self._destination = nil} + + var unidentified: Bool { + get {return _unidentified ?? false} + set {_unidentified = newValue} + } + /// Returns true if `unidentified` has been explicitly set. + var hasUnidentified: Bool {return self._unidentified != nil} + /// Clears the value of `unidentified`. Subsequent reads from it will return its default value. + mutating func clearUnidentified() {self._unidentified = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _destination: String? = nil + fileprivate var _unidentified: Bool? = nil + } + + init() {} + + fileprivate var _destination: String? = nil + fileprivate var _timestamp: UInt64? = nil + fileprivate var _message: SignalServiceProtos_DataMessage? = nil + fileprivate var _expirationStartTimestamp: UInt64? = nil + fileprivate var _isRecipientUpdate: Bool? = nil + } + + struct Contacts { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var blob: SignalServiceProtos_AttachmentPointer { + get {return _blob ?? SignalServiceProtos_AttachmentPointer()} + set {_blob = newValue} + } + /// Returns true if `blob` has been explicitly set. + var hasBlob: Bool {return self._blob != nil} + /// Clears the value of `blob`. Subsequent reads from it will return its default value. + mutating func clearBlob() {self._blob = nil} + + /// Signal-iOS renamed this property. + var isComplete: Bool { + get {return _isComplete ?? false} + set {_isComplete = newValue} + } + /// Returns true if `isComplete` has been explicitly set. + var hasIsComplete: Bool {return self._isComplete != nil} + /// Clears the value of `isComplete`. Subsequent reads from it will return its default value. + mutating func clearIsComplete() {self._isComplete = nil} + + /// Loki + var data: Data { + get {return _data ?? SwiftProtobuf.Internal.emptyData} + set {_data = newValue} + } + /// Returns true if `data` has been explicitly set. + var hasData: Bool {return self._data != nil} + /// Clears the value of `data`. Subsequent reads from it will return its default value. + mutating func clearData() {self._data = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _blob: SignalServiceProtos_AttachmentPointer? = nil + fileprivate var _isComplete: Bool? = nil + fileprivate var _data: Data? = nil + } + + struct Groups { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var blob: SignalServiceProtos_AttachmentPointer { + get {return _blob ?? SignalServiceProtos_AttachmentPointer()} + set {_blob = newValue} + } + /// Returns true if `blob` has been explicitly set. + var hasBlob: Bool {return self._blob != nil} + /// Clears the value of `blob`. Subsequent reads from it will return its default value. + mutating func clearBlob() {self._blob = nil} + + /// Loki + var data: Data { + get {return _data ?? SwiftProtobuf.Internal.emptyData} + set {_data = newValue} + } + /// Returns true if `data` has been explicitly set. + var hasData: Bool {return self._data != nil} + /// Clears the value of `data`. Subsequent reads from it will return its default value. + mutating func clearData() {self._data = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _blob: SignalServiceProtos_AttachmentPointer? = nil + fileprivate var _data: Data? = nil + } + + /// Loki + struct OpenGroupDetails { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var url: String { + get {return _url ?? String()} + set {_url = newValue} + } + /// Returns true if `url` has been explicitly set. + var hasURL: Bool {return self._url != nil} + /// Clears the value of `url`. Subsequent reads from it will return its default value. + mutating func clearURL() {self._url = nil} + + /// @required + var channelID: UInt64 { + get {return _channelID ?? 0} + set {_channelID = newValue} + } + /// Returns true if `channelID` has been explicitly set. + var hasChannelID: Bool {return self._channelID != nil} + /// Clears the value of `channelID`. Subsequent reads from it will return its default value. + mutating func clearChannelID() {self._channelID = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _url: String? = nil + fileprivate var _channelID: UInt64? = nil + } + + struct Blocked { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var numbers: [String] = [] + + var groupIds: [Data] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + struct Request { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var type: SignalServiceProtos_SyncMessage.Request.TypeEnum { + get {return _type ?? .unknown} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case contacts // = 1 + case groups // = 2 + case blocked // = 3 + case configuration // = 4 + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .contacts + case 2: self = .groups + case 3: self = .blocked + case 4: self = .configuration + default: return nil + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .contacts: return 1 + case .groups: return 2 + case .blocked: return 3 + case .configuration: return 4 + } + } + + } + + init() {} + + fileprivate var _type: SignalServiceProtos_SyncMessage.Request.TypeEnum? = nil + } + + struct Read { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var sender: String { + get {return _sender ?? String()} + set {_sender = newValue} + } + /// Returns true if `sender` has been explicitly set. + var hasSender: Bool {return self._sender != nil} + /// Clears the value of `sender`. Subsequent reads from it will return its default value. + mutating func clearSender() {self._sender = nil} + + /// @required + var timestamp: UInt64 { + get {return _timestamp ?? 0} + set {_timestamp = newValue} + } + /// Returns true if `timestamp` has been explicitly set. + var hasTimestamp: Bool {return self._timestamp != nil} + /// Clears the value of `timestamp`. Subsequent reads from it will return its default value. + mutating func clearTimestamp() {self._timestamp = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _sender: String? = nil + fileprivate var _timestamp: UInt64? = nil + } + + struct Configuration { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var readReceipts: Bool { + get {return _readReceipts ?? false} + set {_readReceipts = newValue} + } + /// Returns true if `readReceipts` has been explicitly set. + var hasReadReceipts: Bool {return self._readReceipts != nil} + /// Clears the value of `readReceipts`. Subsequent reads from it will return its default value. + mutating func clearReadReceipts() {self._readReceipts = nil} + + var unidentifiedDeliveryIndicators: Bool { + get {return _unidentifiedDeliveryIndicators ?? false} + set {_unidentifiedDeliveryIndicators = newValue} + } + /// Returns true if `unidentifiedDeliveryIndicators` has been explicitly set. + var hasUnidentifiedDeliveryIndicators: Bool {return self._unidentifiedDeliveryIndicators != nil} + /// Clears the value of `unidentifiedDeliveryIndicators`. Subsequent reads from it will return its default value. + mutating func clearUnidentifiedDeliveryIndicators() {self._unidentifiedDeliveryIndicators = nil} + + var typingIndicators: Bool { + get {return _typingIndicators ?? false} + set {_typingIndicators = newValue} + } + /// Returns true if `typingIndicators` has been explicitly set. + var hasTypingIndicators: Bool {return self._typingIndicators != nil} + /// Clears the value of `typingIndicators`. Subsequent reads from it will return its default value. + mutating func clearTypingIndicators() {self._typingIndicators = nil} + + var linkPreviews: Bool { + get {return _linkPreviews ?? false} + set {_linkPreviews = newValue} + } + /// Returns true if `linkPreviews` has been explicitly set. + var hasLinkPreviews: Bool {return self._linkPreviews != nil} + /// Clears the value of `linkPreviews`. Subsequent reads from it will return its default value. + mutating func clearLinkPreviews() {self._linkPreviews = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _readReceipts: Bool? = nil + fileprivate var _unidentifiedDeliveryIndicators: Bool? = nil + fileprivate var _typingIndicators: Bool? = nil + fileprivate var _linkPreviews: Bool? = nil + } + + init() {} + + fileprivate var _sent: SignalServiceProtos_SyncMessage.Sent? = nil + fileprivate var _contacts: SignalServiceProtos_SyncMessage.Contacts? = nil + fileprivate var _groups: SignalServiceProtos_SyncMessage.Groups? = nil + fileprivate var _request: SignalServiceProtos_SyncMessage.Request? = nil + fileprivate var _blocked: SignalServiceProtos_SyncMessage.Blocked? = nil + fileprivate var _verified: SignalServiceProtos_Verified? = nil + fileprivate var _configuration: SignalServiceProtos_SyncMessage.Configuration? = nil + fileprivate var _padding: Data? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_SyncMessage.Request.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_AttachmentPointer { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: UInt64 { + get {return _id ?? 0} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + var contentType: String { + get {return _contentType ?? String()} + set {_contentType = newValue} + } + /// Returns true if `contentType` has been explicitly set. + var hasContentType: Bool {return self._contentType != nil} + /// Clears the value of `contentType`. Subsequent reads from it will return its default value. + mutating func clearContentType() {self._contentType = nil} + + var key: Data { + get {return _key ?? SwiftProtobuf.Internal.emptyData} + set {_key = newValue} + } + /// Returns true if `key` has been explicitly set. + var hasKey: Bool {return self._key != nil} + /// Clears the value of `key`. Subsequent reads from it will return its default value. + mutating func clearKey() {self._key = nil} + + var size: UInt32 { + get {return _size ?? 0} + set {_size = newValue} + } + /// Returns true if `size` has been explicitly set. + var hasSize: Bool {return self._size != nil} + /// Clears the value of `size`. Subsequent reads from it will return its default value. + mutating func clearSize() {self._size = nil} + + var thumbnail: Data { + get {return _thumbnail ?? SwiftProtobuf.Internal.emptyData} + set {_thumbnail = newValue} + } + /// Returns true if `thumbnail` has been explicitly set. + var hasThumbnail: Bool {return self._thumbnail != nil} + /// Clears the value of `thumbnail`. Subsequent reads from it will return its default value. + mutating func clearThumbnail() {self._thumbnail = nil} + + var digest: Data { + get {return _digest ?? SwiftProtobuf.Internal.emptyData} + set {_digest = newValue} + } + /// Returns true if `digest` has been explicitly set. + var hasDigest: Bool {return self._digest != nil} + /// Clears the value of `digest`. Subsequent reads from it will return its default value. + mutating func clearDigest() {self._digest = nil} + + var fileName: String { + get {return _fileName ?? String()} + set {_fileName = newValue} + } + /// Returns true if `fileName` has been explicitly set. + var hasFileName: Bool {return self._fileName != nil} + /// Clears the value of `fileName`. Subsequent reads from it will return its default value. + mutating func clearFileName() {self._fileName = nil} + + var flags: UInt32 { + get {return _flags ?? 0} + set {_flags = newValue} + } + /// Returns true if `flags` has been explicitly set. + var hasFlags: Bool {return self._flags != nil} + /// Clears the value of `flags`. Subsequent reads from it will return its default value. + mutating func clearFlags() {self._flags = nil} + + var width: UInt32 { + get {return _width ?? 0} + set {_width = newValue} + } + /// Returns true if `width` has been explicitly set. + var hasWidth: Bool {return self._width != nil} + /// Clears the value of `width`. Subsequent reads from it will return its default value. + mutating func clearWidth() {self._width = nil} + + var height: UInt32 { + get {return _height ?? 0} + set {_height = newValue} + } + /// Returns true if `height` has been explicitly set. + var hasHeight: Bool {return self._height != nil} + /// Clears the value of `height`. Subsequent reads from it will return its default value. + mutating func clearHeight() {self._height = nil} + + var caption: String { + get {return _caption ?? String()} + set {_caption = newValue} + } + /// Returns true if `caption` has been explicitly set. + var hasCaption: Bool {return self._caption != nil} + /// Clears the value of `caption`. Subsequent reads from it will return its default value. + mutating func clearCaption() {self._caption = nil} + + /// Loki + var url: String { + get {return _url ?? String()} + set {_url = newValue} + } + /// Returns true if `url` has been explicitly set. + var hasURL: Bool {return self._url != nil} + /// Clears the value of `url`. Subsequent reads from it will return its default value. + mutating func clearURL() {self._url = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum Flags: SwiftProtobuf.Enum { + typealias RawValue = Int + case voiceMessage // = 1 + + init() { + self = .voiceMessage + } + + init?(rawValue: Int) { + switch rawValue { + case 1: self = .voiceMessage + default: return nil + } + } + + var rawValue: Int { + switch self { + case .voiceMessage: return 1 + } + } + + } + + init() {} + + fileprivate var _id: UInt64? = nil + fileprivate var _contentType: String? = nil + fileprivate var _key: Data? = nil + fileprivate var _size: UInt32? = nil + fileprivate var _thumbnail: Data? = nil + fileprivate var _digest: Data? = nil + fileprivate var _fileName: String? = nil + fileprivate var _flags: UInt32? = nil + fileprivate var _width: UInt32? = nil + fileprivate var _height: UInt32? = nil + fileprivate var _caption: String? = nil + fileprivate var _url: String? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_AttachmentPointer.Flags: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_GroupContext { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: Data { + get {return _id ?? SwiftProtobuf.Internal.emptyData} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + /// @required + var type: SignalServiceProtos_GroupContext.TypeEnum { + get {return _type ?? .unknown} + set {_type = newValue} + } + /// Returns true if `type` has been explicitly set. + var hasType: Bool {return self._type != nil} + /// Clears the value of `type`. Subsequent reads from it will return its default value. + mutating func clearType() {self._type = nil} + + var name: String { + get {return _name ?? String()} + set {_name = newValue} + } + /// Returns true if `name` has been explicitly set. + var hasName: Bool {return self._name != nil} + /// Clears the value of `name`. Subsequent reads from it will return its default value. + mutating func clearName() {self._name = nil} + + var members: [String] = [] + + var avatar: SignalServiceProtos_AttachmentPointer { + get {return _avatar ?? SignalServiceProtos_AttachmentPointer()} + set {_avatar = newValue} + } + /// Returns true if `avatar` has been explicitly set. + var hasAvatar: Bool {return self._avatar != nil} + /// Clears the value of `avatar`. Subsequent reads from it will return its default value. + mutating func clearAvatar() {self._avatar = nil} + + /// Loki + var admins: [String] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case update // = 1 + case deliver // = 2 + case quit // = 3 + case requestInfo // = 4 + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .update + case 2: self = .deliver + case 3: self = .quit + case 4: self = .requestInfo + default: return nil + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .update: return 1 + case .deliver: return 2 + case .quit: return 3 + case .requestInfo: return 4 + } + } + + } + + init() {} + + fileprivate var _id: Data? = nil + fileprivate var _type: SignalServiceProtos_GroupContext.TypeEnum? = nil + fileprivate var _name: String? = nil + fileprivate var _avatar: SignalServiceProtos_AttachmentPointer? = nil +} + +#if swift(>=4.2) + +extension SignalServiceProtos_GroupContext.TypeEnum: CaseIterable { + // Support synthesized by the compiler. +} + +#endif // swift(>=4.2) + +struct SignalServiceProtos_ContactDetails { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var number: String { + get {return _number ?? String()} + set {_number = newValue} + } + /// Returns true if `number` has been explicitly set. + var hasNumber: Bool {return self._number != nil} + /// Clears the value of `number`. Subsequent reads from it will return its default value. + mutating func clearNumber() {self._number = nil} + + var name: String { + get {return _name ?? String()} + set {_name = newValue} + } + /// Returns true if `name` has been explicitly set. + var hasName: Bool {return self._name != nil} + /// Clears the value of `name`. Subsequent reads from it will return its default value. + mutating func clearName() {self._name = nil} + + var avatar: SignalServiceProtos_ContactDetails.Avatar { + get {return _avatar ?? SignalServiceProtos_ContactDetails.Avatar()} + set {_avatar = newValue} + } + /// Returns true if `avatar` has been explicitly set. + var hasAvatar: Bool {return self._avatar != nil} + /// Clears the value of `avatar`. Subsequent reads from it will return its default value. + mutating func clearAvatar() {self._avatar = nil} + + var color: String { + get {return _color ?? String()} + set {_color = newValue} + } + /// Returns true if `color` has been explicitly set. + var hasColor: Bool {return self._color != nil} + /// Clears the value of `color`. Subsequent reads from it will return its default value. + mutating func clearColor() {self._color = nil} + + var verified: SignalServiceProtos_Verified { + get {return _verified ?? SignalServiceProtos_Verified()} + set {_verified = newValue} + } + /// Returns true if `verified` has been explicitly set. + var hasVerified: Bool {return self._verified != nil} + /// Clears the value of `verified`. Subsequent reads from it will return its default value. + mutating func clearVerified() {self._verified = nil} + + var profileKey: Data { + get {return _profileKey ?? SwiftProtobuf.Internal.emptyData} + set {_profileKey = newValue} + } + /// Returns true if `profileKey` has been explicitly set. + var hasProfileKey: Bool {return self._profileKey != nil} + /// Clears the value of `profileKey`. Subsequent reads from it will return its default value. + mutating func clearProfileKey() {self._profileKey = nil} + + var blocked: Bool { + get {return _blocked ?? false} + set {_blocked = newValue} + } + /// Returns true if `blocked` has been explicitly set. + var hasBlocked: Bool {return self._blocked != nil} + /// Clears the value of `blocked`. Subsequent reads from it will return its default value. + mutating func clearBlocked() {self._blocked = nil} + + var expireTimer: UInt32 { + get {return _expireTimer ?? 0} + set {_expireTimer = newValue} + } + /// Returns true if `expireTimer` has been explicitly set. + var hasExpireTimer: Bool {return self._expireTimer != nil} + /// Clears the value of `expireTimer`. Subsequent reads from it will return its default value. + mutating func clearExpireTimer() {self._expireTimer = nil} + + /// Loki + var nickname: String { + get {return _nickname ?? String()} + set {_nickname = newValue} + } + /// Returns true if `nickname` has been explicitly set. + var hasNickname: Bool {return self._nickname != nil} + /// Clears the value of `nickname`. Subsequent reads from it will return its default value. + mutating func clearNickname() {self._nickname = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct Avatar { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var contentType: String { + get {return _contentType ?? String()} + set {_contentType = newValue} + } + /// Returns true if `contentType` has been explicitly set. + var hasContentType: Bool {return self._contentType != nil} + /// Clears the value of `contentType`. Subsequent reads from it will return its default value. + mutating func clearContentType() {self._contentType = nil} + + var length: UInt32 { + get {return _length ?? 0} + set {_length = newValue} + } + /// Returns true if `length` has been explicitly set. + var hasLength: Bool {return self._length != nil} + /// Clears the value of `length`. Subsequent reads from it will return its default value. + mutating func clearLength() {self._length = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _contentType: String? = nil + fileprivate var _length: UInt32? = nil + } + + init() {} + + fileprivate var _number: String? = nil + fileprivate var _name: String? = nil + fileprivate var _avatar: SignalServiceProtos_ContactDetails.Avatar? = nil + fileprivate var _color: String? = nil + fileprivate var _verified: SignalServiceProtos_Verified? = nil + fileprivate var _profileKey: Data? = nil + fileprivate var _blocked: Bool? = nil + fileprivate var _expireTimer: UInt32? = nil + fileprivate var _nickname: String? = nil +} + +struct SignalServiceProtos_GroupDetails { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// @required + var id: Data { + get {return _id ?? SwiftProtobuf.Internal.emptyData} + set {_id = newValue} + } + /// Returns true if `id` has been explicitly set. + var hasID: Bool {return self._id != nil} + /// Clears the value of `id`. Subsequent reads from it will return its default value. + mutating func clearID() {self._id = nil} + + var name: String { + get {return _name ?? String()} + set {_name = newValue} + } + /// Returns true if `name` has been explicitly set. + var hasName: Bool {return self._name != nil} + /// Clears the value of `name`. Subsequent reads from it will return its default value. + mutating func clearName() {self._name = nil} + + var members: [String] = [] + + var avatar: SignalServiceProtos_GroupDetails.Avatar { + get {return _avatar ?? SignalServiceProtos_GroupDetails.Avatar()} + set {_avatar = newValue} + } + /// Returns true if `avatar` has been explicitly set. + var hasAvatar: Bool {return self._avatar != nil} + /// Clears the value of `avatar`. Subsequent reads from it will return its default value. + mutating func clearAvatar() {self._avatar = nil} + + var active: Bool { + get {return _active ?? true} + set {_active = newValue} + } + /// Returns true if `active` has been explicitly set. + var hasActive: Bool {return self._active != nil} + /// Clears the value of `active`. Subsequent reads from it will return its default value. + mutating func clearActive() {self._active = nil} + + var expireTimer: UInt32 { + get {return _expireTimer ?? 0} + set {_expireTimer = newValue} + } + /// Returns true if `expireTimer` has been explicitly set. + var hasExpireTimer: Bool {return self._expireTimer != nil} + /// Clears the value of `expireTimer`. Subsequent reads from it will return its default value. + mutating func clearExpireTimer() {self._expireTimer = nil} + + var color: String { + get {return _color ?? String()} + set {_color = newValue} + } + /// Returns true if `color` has been explicitly set. + var hasColor: Bool {return self._color != nil} + /// Clears the value of `color`. Subsequent reads from it will return its default value. + mutating func clearColor() {self._color = nil} + + var blocked: Bool { + get {return _blocked ?? false} + set {_blocked = newValue} + } + /// Returns true if `blocked` has been explicitly set. + var hasBlocked: Bool {return self._blocked != nil} + /// Clears the value of `blocked`. Subsequent reads from it will return its default value. + mutating func clearBlocked() {self._blocked = nil} + + /// Loki + var admins: [String] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct Avatar { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var contentType: String { + get {return _contentType ?? String()} + set {_contentType = newValue} + } + /// Returns true if `contentType` has been explicitly set. + var hasContentType: Bool {return self._contentType != nil} + /// Clears the value of `contentType`. Subsequent reads from it will return its default value. + mutating func clearContentType() {self._contentType = nil} + + var length: UInt32 { + get {return _length ?? 0} + set {_length = newValue} + } + /// Returns true if `length` has been explicitly set. + var hasLength: Bool {return self._length != nil} + /// Clears the value of `length`. Subsequent reads from it will return its default value. + mutating func clearLength() {self._length = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _contentType: String? = nil + fileprivate var _length: UInt32? = nil + } + + init() {} + + fileprivate var _id: Data? = nil + fileprivate var _name: String? = nil + fileprivate var _avatar: SignalServiceProtos_GroupDetails.Avatar? = nil + fileprivate var _active: Bool? = nil + fileprivate var _expireTimer: UInt32? = nil + fileprivate var _color: String? = nil + fileprivate var _blocked: Bool? = nil +} + +/// Internal - DO NOT SEND +struct SignalServiceProtos_PublicChatInfo { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var serverID: UInt64 { + get {return _serverID ?? 0} + set {_serverID = newValue} + } + /// Returns true if `serverID` has been explicitly set. + var hasServerID: Bool {return self._serverID != nil} + /// Clears the value of `serverID`. Subsequent reads from it will return its default value. + mutating func clearServerID() {self._serverID = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _serverID: UInt64? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "SignalServiceProtos" + +extension SignalServiceProtos_Envelope: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Envelope" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "source"), + 7: .same(proto: "sourceDevice"), + 3: .same(proto: "relay"), + 5: .same(proto: "timestamp"), + 6: .same(proto: "legacyMessage"), + 8: .same(proto: "content"), + 9: .same(proto: "serverGuid"), + 10: .same(proto: "serverTimestamp"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularEnumField(value: &self._type) + case 2: try decoder.decodeSingularStringField(value: &self._source) + case 3: try decoder.decodeSingularStringField(value: &self._relay) + case 5: try decoder.decodeSingularUInt64Field(value: &self._timestamp) + case 6: try decoder.decodeSingularBytesField(value: &self._legacyMessage) + case 7: try decoder.decodeSingularUInt32Field(value: &self._sourceDevice) + case 8: try decoder.decodeSingularBytesField(value: &self._content) + case 9: try decoder.decodeSingularStringField(value: &self._serverGuid) + case 10: try decoder.decodeSingularUInt64Field(value: &self._serverTimestamp) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 1) + } + if let v = self._source { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._relay { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if let v = self._timestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 5) + } + if let v = self._legacyMessage { + try visitor.visitSingularBytesField(value: v, fieldNumber: 6) + } + if let v = self._sourceDevice { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) + } + if let v = self._content { + try visitor.visitSingularBytesField(value: v, fieldNumber: 8) + } + if let v = self._serverGuid { + try visitor.visitSingularStringField(value: v, fieldNumber: 9) + } + if let v = self._serverTimestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 10) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_Envelope, rhs: SignalServiceProtos_Envelope) -> Bool { + if lhs._type != rhs._type {return false} + if lhs._source != rhs._source {return false} + if lhs._sourceDevice != rhs._sourceDevice {return false} + if lhs._relay != rhs._relay {return false} + if lhs._timestamp != rhs._timestamp {return false} + if lhs._legacyMessage != rhs._legacyMessage {return false} + if lhs._content != rhs._content {return false} + if lhs._serverGuid != rhs._serverGuid {return false} + if lhs._serverTimestamp != rhs._serverTimestamp {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_Envelope.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "CIPHERTEXT"), + 2: .same(proto: "KEY_EXCHANGE"), + 3: .same(proto: "PREKEY_BUNDLE"), + 5: .same(proto: "RECEIPT"), + 6: .same(proto: "UNIDENTIFIED_SENDER"), + 7: .same(proto: "CLOSED_GROUP_CIPHERTEXT"), + 101: .same(proto: "FALLBACK_MESSAGE"), + ] +} + +extension SignalServiceProtos_TypingMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TypingMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "timestamp"), + 2: .same(proto: "action"), + 3: .same(proto: "groupId"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._timestamp) + case 2: try decoder.decodeSingularEnumField(value: &self._action) + case 3: try decoder.decodeSingularBytesField(value: &self._groupID) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._timestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + if let v = self._action { + try visitor.visitSingularEnumField(value: v, fieldNumber: 2) + } + if let v = self._groupID { + try visitor.visitSingularBytesField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_TypingMessage, rhs: SignalServiceProtos_TypingMessage) -> Bool { + if lhs._timestamp != rhs._timestamp {return false} + if lhs._action != rhs._action {return false} + if lhs._groupID != rhs._groupID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_TypingMessage.Action: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "STARTED"), + 1: .same(proto: "STOPPED"), + ] +} + +extension SignalServiceProtos_Content: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Content" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "dataMessage"), + 2: .same(proto: "syncMessage"), + 3: .same(proto: "callMessage"), + 4: .same(proto: "nullMessage"), + 5: .same(proto: "receiptMessage"), + 6: .same(proto: "typingMessage"), + 101: .same(proto: "prekeyBundleMessage"), + 103: .same(proto: "lokiDeviceLinkMessage"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularMessageField(value: &self._dataMessage) + case 2: try decoder.decodeSingularMessageField(value: &self._syncMessage) + case 3: try decoder.decodeSingularMessageField(value: &self._callMessage) + case 4: try decoder.decodeSingularMessageField(value: &self._nullMessage) + case 5: try decoder.decodeSingularMessageField(value: &self._receiptMessage) + case 6: try decoder.decodeSingularMessageField(value: &self._typingMessage) + case 101: try decoder.decodeSingularMessageField(value: &self._prekeyBundleMessage) + case 103: try decoder.decodeSingularMessageField(value: &self._lokiDeviceLinkMessage) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._dataMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } + if let v = self._syncMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } + if let v = self._callMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + if let v = self._nullMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } + if let v = self._receiptMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } + if let v = self._typingMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } + if let v = self._prekeyBundleMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 101) + } + if let v = self._lokiDeviceLinkMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 103) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_Content, rhs: SignalServiceProtos_Content) -> Bool { + if lhs._dataMessage != rhs._dataMessage {return false} + if lhs._syncMessage != rhs._syncMessage {return false} + if lhs._callMessage != rhs._callMessage {return false} + if lhs._nullMessage != rhs._nullMessage {return false} + if lhs._receiptMessage != rhs._receiptMessage {return false} + if lhs._typingMessage != rhs._typingMessage {return false} + if lhs._prekeyBundleMessage != rhs._prekeyBundleMessage {return false} + if lhs._lokiDeviceLinkMessage != rhs._lokiDeviceLinkMessage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_PrekeyBundleMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PrekeyBundleMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "identityKey"), + 2: .same(proto: "deviceID"), + 3: .same(proto: "prekeyID"), + 4: .same(proto: "signedKeyID"), + 5: .same(proto: "prekey"), + 6: .same(proto: "signedKey"), + 7: .same(proto: "signature"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._identityKey) + case 2: try decoder.decodeSingularUInt32Field(value: &self._deviceID) + case 3: try decoder.decodeSingularUInt32Field(value: &self._prekeyID) + case 4: try decoder.decodeSingularUInt32Field(value: &self._signedKeyID) + case 5: try decoder.decodeSingularBytesField(value: &self._prekey) + case 6: try decoder.decodeSingularBytesField(value: &self._signedKey) + case 7: try decoder.decodeSingularBytesField(value: &self._signature) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._identityKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._deviceID { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } + if let v = self._prekeyID { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3) + } + if let v = self._signedKeyID { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } + if let v = self._prekey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 5) + } + if let v = self._signedKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 6) + } + if let v = self._signature { + try visitor.visitSingularBytesField(value: v, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_PrekeyBundleMessage, rhs: SignalServiceProtos_PrekeyBundleMessage) -> Bool { + if lhs._identityKey != rhs._identityKey {return false} + if lhs._deviceID != rhs._deviceID {return false} + if lhs._prekeyID != rhs._prekeyID {return false} + if lhs._signedKeyID != rhs._signedKeyID {return false} + if lhs._prekey != rhs._prekey {return false} + if lhs._signedKey != rhs._signedKey {return false} + if lhs._signature != rhs._signature {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_LokiDeviceLinkMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LokiDeviceLinkMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "masterPublicKey"), + 2: .same(proto: "slavePublicKey"), + 3: .same(proto: "slaveSignature"), + 4: .same(proto: "masterSignature"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._masterPublicKey) + case 2: try decoder.decodeSingularStringField(value: &self._slavePublicKey) + case 3: try decoder.decodeSingularBytesField(value: &self._slaveSignature) + case 4: try decoder.decodeSingularBytesField(value: &self._masterSignature) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._masterPublicKey { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._slavePublicKey { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._slaveSignature { + try visitor.visitSingularBytesField(value: v, fieldNumber: 3) + } + if let v = self._masterSignature { + try visitor.visitSingularBytesField(value: v, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_LokiDeviceLinkMessage, rhs: SignalServiceProtos_LokiDeviceLinkMessage) -> Bool { + if lhs._masterPublicKey != rhs._masterPublicKey {return false} + if lhs._slavePublicKey != rhs._slavePublicKey {return false} + if lhs._slaveSignature != rhs._slaveSignature {return false} + if lhs._masterSignature != rhs._masterSignature {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_CallMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".CallMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "offer"), + 2: .same(proto: "answer"), + 3: .same(proto: "iceUpdate"), + 4: .same(proto: "hangup"), + 5: .same(proto: "busy"), + 6: .same(proto: "profileKey"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularMessageField(value: &self._offer) + case 2: try decoder.decodeSingularMessageField(value: &self._answer) + case 3: try decoder.decodeRepeatedMessageField(value: &self.iceUpdate) + case 4: try decoder.decodeSingularMessageField(value: &self._hangup) + case 5: try decoder.decodeSingularMessageField(value: &self._busy) + case 6: try decoder.decodeSingularBytesField(value: &self._profileKey) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._offer { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } + if let v = self._answer { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } + if !self.iceUpdate.isEmpty { + try visitor.visitRepeatedMessageField(value: self.iceUpdate, fieldNumber: 3) + } + if let v = self._hangup { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } + if let v = self._busy { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } + if let v = self._profileKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_CallMessage, rhs: SignalServiceProtos_CallMessage) -> Bool { + if lhs._offer != rhs._offer {return false} + if lhs._answer != rhs._answer {return false} + if lhs.iceUpdate != rhs.iceUpdate {return false} + if lhs._hangup != rhs._hangup {return false} + if lhs._busy != rhs._busy {return false} + if lhs._profileKey != rhs._profileKey {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_CallMessage.Offer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_CallMessage.protoMessageName + ".Offer" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "sessionDescription"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._id) + case 2: try decoder.decodeSingularStringField(value: &self._sessionDescription) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + if let v = self._sessionDescription { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_CallMessage.Offer, rhs: SignalServiceProtos_CallMessage.Offer) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._sessionDescription != rhs._sessionDescription {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_CallMessage.Answer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_CallMessage.protoMessageName + ".Answer" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "sessionDescription"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._id) + case 2: try decoder.decodeSingularStringField(value: &self._sessionDescription) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + if let v = self._sessionDescription { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_CallMessage.Answer, rhs: SignalServiceProtos_CallMessage.Answer) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._sessionDescription != rhs._sessionDescription {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_CallMessage.IceUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_CallMessage.protoMessageName + ".IceUpdate" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "sdpMid"), + 3: .same(proto: "sdpMLineIndex"), + 4: .same(proto: "sdp"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._id) + case 2: try decoder.decodeSingularStringField(value: &self._sdpMid) + case 3: try decoder.decodeSingularUInt32Field(value: &self._sdpMlineIndex) + case 4: try decoder.decodeSingularStringField(value: &self._sdp) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + if let v = self._sdpMid { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._sdpMlineIndex { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3) + } + if let v = self._sdp { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_CallMessage.IceUpdate, rhs: SignalServiceProtos_CallMessage.IceUpdate) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._sdpMid != rhs._sdpMid {return false} + if lhs._sdpMlineIndex != rhs._sdpMlineIndex {return false} + if lhs._sdp != rhs._sdp {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_CallMessage.Busy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_CallMessage.protoMessageName + ".Busy" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._id) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_CallMessage.Busy, rhs: SignalServiceProtos_CallMessage.Busy) -> Bool { + if lhs._id != rhs._id {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_CallMessage.Hangup: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_CallMessage.protoMessageName + ".Hangup" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._id) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_CallMessage.Hangup, rhs: SignalServiceProtos_CallMessage.Hangup) -> Bool { + if lhs._id != rhs._id {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_ClosedGroupCiphertextMessageWrapper: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClosedGroupCiphertextMessageWrapper" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "ciphertext"), + 2: .same(proto: "ephemeralPublicKey"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._ciphertext) + case 2: try decoder.decodeSingularBytesField(value: &self._ephemeralPublicKey) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._ciphertext { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._ephemeralPublicKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_ClosedGroupCiphertextMessageWrapper, rhs: SignalServiceProtos_ClosedGroupCiphertextMessageWrapper) -> Bool { + if lhs._ciphertext != rhs._ciphertext {return false} + if lhs._ephemeralPublicKey != rhs._ephemeralPublicKey {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DataMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "body"), + 2: .same(proto: "attachments"), + 3: .same(proto: "group"), + 4: .same(proto: "flags"), + 5: .same(proto: "expireTimer"), + 6: .same(proto: "profileKey"), + 7: .same(proto: "timestamp"), + 8: .same(proto: "quote"), + 9: .same(proto: "contact"), + 10: .same(proto: "preview"), + 101: .same(proto: "profile"), + 103: .same(proto: "closedGroupUpdate"), + 999: .same(proto: "publicChatInfo"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._body) + case 2: try decoder.decodeRepeatedMessageField(value: &self.attachments) + case 3: try decoder.decodeSingularMessageField(value: &self._group) + case 4: try decoder.decodeSingularUInt32Field(value: &self._flags) + case 5: try decoder.decodeSingularUInt32Field(value: &self._expireTimer) + case 6: try decoder.decodeSingularBytesField(value: &self._profileKey) + case 7: try decoder.decodeSingularUInt64Field(value: &self._timestamp) + case 8: try decoder.decodeSingularMessageField(value: &self._quote) + case 9: try decoder.decodeRepeatedMessageField(value: &self.contact) + case 10: try decoder.decodeRepeatedMessageField(value: &self.preview) + case 101: try decoder.decodeSingularMessageField(value: &self._profile) + case 103: try decoder.decodeSingularMessageField(value: &self._closedGroupUpdate) + case 999: try decoder.decodeSingularMessageField(value: &self._publicChatInfo) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._body { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if !self.attachments.isEmpty { + try visitor.visitRepeatedMessageField(value: self.attachments, fieldNumber: 2) + } + if let v = self._group { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + if let v = self._flags { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } + if let v = self._expireTimer { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 5) + } + if let v = self._profileKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 6) + } + if let v = self._timestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 7) + } + if let v = self._quote { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } + if !self.contact.isEmpty { + try visitor.visitRepeatedMessageField(value: self.contact, fieldNumber: 9) + } + if !self.preview.isEmpty { + try visitor.visitRepeatedMessageField(value: self.preview, fieldNumber: 10) + } + if let v = self._profile { + try visitor.visitSingularMessageField(value: v, fieldNumber: 101) + } + if let v = self._closedGroupUpdate { + try visitor.visitSingularMessageField(value: v, fieldNumber: 103) + } + if let v = self._publicChatInfo { + try visitor.visitSingularMessageField(value: v, fieldNumber: 999) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage, rhs: SignalServiceProtos_DataMessage) -> Bool { + if lhs._body != rhs._body {return false} + if lhs.attachments != rhs.attachments {return false} + if lhs._group != rhs._group {return false} + if lhs._flags != rhs._flags {return false} + if lhs._expireTimer != rhs._expireTimer {return false} + if lhs._profileKey != rhs._profileKey {return false} + if lhs._timestamp != rhs._timestamp {return false} + if lhs._quote != rhs._quote {return false} + if lhs.contact != rhs.contact {return false} + if lhs.preview != rhs.preview {return false} + if lhs._profile != rhs._profile {return false} + if lhs._closedGroupUpdate != rhs._closedGroupUpdate {return false} + if lhs._publicChatInfo != rhs._publicChatInfo {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Flags: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "END_SESSION"), + 2: .same(proto: "EXPIRATION_TIMER_UPDATE"), + 4: .same(proto: "PROFILE_KEY_UPDATE"), + 128: .same(proto: "UNLINK_DEVICE"), + ] +} + +extension SignalServiceProtos_DataMessage.Quote: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".Quote" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "author"), + 3: .same(proto: "text"), + 4: .same(proto: "attachments"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._id) + case 2: try decoder.decodeSingularStringField(value: &self._author) + case 3: try decoder.decodeSingularStringField(value: &self._text) + case 4: try decoder.decodeRepeatedMessageField(value: &self.attachments) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + if let v = self._author { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._text { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if !self.attachments.isEmpty { + try visitor.visitRepeatedMessageField(value: self.attachments, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Quote, rhs: SignalServiceProtos_DataMessage.Quote) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._author != rhs._author {return false} + if lhs._text != rhs._text {return false} + if lhs.attachments != rhs.attachments {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Quote.QuotedAttachment: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.Quote.protoMessageName + ".QuotedAttachment" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "contentType"), + 2: .same(proto: "fileName"), + 3: .same(proto: "thumbnail"), + 4: .same(proto: "flags"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._contentType) + case 2: try decoder.decodeSingularStringField(value: &self._fileName) + case 3: try decoder.decodeSingularMessageField(value: &self._thumbnail) + case 4: try decoder.decodeSingularUInt32Field(value: &self._flags) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._contentType { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._fileName { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._thumbnail { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + if let v = self._flags { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Quote.QuotedAttachment, rhs: SignalServiceProtos_DataMessage.Quote.QuotedAttachment) -> Bool { + if lhs._contentType != rhs._contentType {return false} + if lhs._fileName != rhs._fileName {return false} + if lhs._thumbnail != rhs._thumbnail {return false} + if lhs._flags != rhs._flags {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Quote.QuotedAttachment.Flags: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "VOICE_MESSAGE"), + ] +} + +extension SignalServiceProtos_DataMessage.Contact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".Contact" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 3: .same(proto: "number"), + 4: .same(proto: "email"), + 5: .same(proto: "address"), + 6: .same(proto: "avatar"), + 7: .same(proto: "organization"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularMessageField(value: &self._name) + case 3: try decoder.decodeRepeatedMessageField(value: &self.number) + case 4: try decoder.decodeRepeatedMessageField(value: &self.email) + case 5: try decoder.decodeRepeatedMessageField(value: &self.address) + case 6: try decoder.decodeSingularMessageField(value: &self._avatar) + case 7: try decoder.decodeSingularStringField(value: &self._organization) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._name { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } + if !self.number.isEmpty { + try visitor.visitRepeatedMessageField(value: self.number, fieldNumber: 3) + } + if !self.email.isEmpty { + try visitor.visitRepeatedMessageField(value: self.email, fieldNumber: 4) + } + if !self.address.isEmpty { + try visitor.visitRepeatedMessageField(value: self.address, fieldNumber: 5) + } + if let v = self._avatar { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } + if let v = self._organization { + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Contact, rhs: SignalServiceProtos_DataMessage.Contact) -> Bool { + if lhs._name != rhs._name {return false} + if lhs.number != rhs.number {return false} + if lhs.email != rhs.email {return false} + if lhs.address != rhs.address {return false} + if lhs._avatar != rhs._avatar {return false} + if lhs._organization != rhs._organization {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Contact.Name: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.Contact.protoMessageName + ".Name" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "givenName"), + 2: .same(proto: "familyName"), + 3: .same(proto: "prefix"), + 4: .same(proto: "suffix"), + 5: .same(proto: "middleName"), + 6: .same(proto: "displayName"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._givenName) + case 2: try decoder.decodeSingularStringField(value: &self._familyName) + case 3: try decoder.decodeSingularStringField(value: &self._prefix) + case 4: try decoder.decodeSingularStringField(value: &self._suffix) + case 5: try decoder.decodeSingularStringField(value: &self._middleName) + case 6: try decoder.decodeSingularStringField(value: &self._displayName) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._givenName { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._familyName { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._prefix { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if let v = self._suffix { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } + if let v = self._middleName { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } + if let v = self._displayName { + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Contact.Name, rhs: SignalServiceProtos_DataMessage.Contact.Name) -> Bool { + if lhs._givenName != rhs._givenName {return false} + if lhs._familyName != rhs._familyName {return false} + if lhs._prefix != rhs._prefix {return false} + if lhs._suffix != rhs._suffix {return false} + if lhs._middleName != rhs._middleName {return false} + if lhs._displayName != rhs._displayName {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Contact.Phone: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.Contact.protoMessageName + ".Phone" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + 2: .same(proto: "type"), + 3: .same(proto: "label"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._value) + case 2: try decoder.decodeSingularEnumField(value: &self._type) + case 3: try decoder.decodeSingularStringField(value: &self._label) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._value { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 2) + } + if let v = self._label { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Contact.Phone, rhs: SignalServiceProtos_DataMessage.Contact.Phone) -> Bool { + if lhs._value != rhs._value {return false} + if lhs._type != rhs._type {return false} + if lhs._label != rhs._label {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Contact.Phone.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "HOME"), + 2: .same(proto: "MOBILE"), + 3: .same(proto: "WORK"), + 4: .same(proto: "CUSTOM"), + ] +} + +extension SignalServiceProtos_DataMessage.Contact.Email: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.Contact.protoMessageName + ".Email" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + 2: .same(proto: "type"), + 3: .same(proto: "label"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._value) + case 2: try decoder.decodeSingularEnumField(value: &self._type) + case 3: try decoder.decodeSingularStringField(value: &self._label) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._value { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 2) + } + if let v = self._label { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Contact.Email, rhs: SignalServiceProtos_DataMessage.Contact.Email) -> Bool { + if lhs._value != rhs._value {return false} + if lhs._type != rhs._type {return false} + if lhs._label != rhs._label {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Contact.Email.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "HOME"), + 2: .same(proto: "MOBILE"), + 3: .same(proto: "WORK"), + 4: .same(proto: "CUSTOM"), + ] +} + +extension SignalServiceProtos_DataMessage.Contact.PostalAddress: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.Contact.protoMessageName + ".PostalAddress" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "label"), + 3: .same(proto: "street"), + 4: .same(proto: "pobox"), + 5: .same(proto: "neighborhood"), + 6: .same(proto: "city"), + 7: .same(proto: "region"), + 8: .same(proto: "postcode"), + 9: .same(proto: "country"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularEnumField(value: &self._type) + case 2: try decoder.decodeSingularStringField(value: &self._label) + case 3: try decoder.decodeSingularStringField(value: &self._street) + case 4: try decoder.decodeSingularStringField(value: &self._pobox) + case 5: try decoder.decodeSingularStringField(value: &self._neighborhood) + case 6: try decoder.decodeSingularStringField(value: &self._city) + case 7: try decoder.decodeSingularStringField(value: &self._region) + case 8: try decoder.decodeSingularStringField(value: &self._postcode) + case 9: try decoder.decodeSingularStringField(value: &self._country) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 1) + } + if let v = self._label { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._street { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if let v = self._pobox { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } + if let v = self._neighborhood { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } + if let v = self._city { + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + } + if let v = self._region { + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + } + if let v = self._postcode { + try visitor.visitSingularStringField(value: v, fieldNumber: 8) + } + if let v = self._country { + try visitor.visitSingularStringField(value: v, fieldNumber: 9) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Contact.PostalAddress, rhs: SignalServiceProtos_DataMessage.Contact.PostalAddress) -> Bool { + if lhs._type != rhs._type {return false} + if lhs._label != rhs._label {return false} + if lhs._street != rhs._street {return false} + if lhs._pobox != rhs._pobox {return false} + if lhs._neighborhood != rhs._neighborhood {return false} + if lhs._city != rhs._city {return false} + if lhs._region != rhs._region {return false} + if lhs._postcode != rhs._postcode {return false} + if lhs._country != rhs._country {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "HOME"), + 2: .same(proto: "WORK"), + 3: .same(proto: "CUSTOM"), + ] +} + +extension SignalServiceProtos_DataMessage.Contact.Avatar: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.Contact.protoMessageName + ".Avatar" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "avatar"), + 2: .same(proto: "isProfile"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularMessageField(value: &self._avatar) + case 2: try decoder.decodeSingularBoolField(value: &self._isProfile) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._avatar { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } + if let v = self._isProfile { + try visitor.visitSingularBoolField(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Contact.Avatar, rhs: SignalServiceProtos_DataMessage.Contact.Avatar) -> Bool { + if lhs._avatar != rhs._avatar {return false} + if lhs._isProfile != rhs._isProfile {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.Preview: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".Preview" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "url"), + 2: .same(proto: "title"), + 3: .same(proto: "image"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._url) + case 2: try decoder.decodeSingularStringField(value: &self._title) + case 3: try decoder.decodeSingularMessageField(value: &self._image) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._url { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._title { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._image { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.Preview, rhs: SignalServiceProtos_DataMessage.Preview) -> Bool { + if lhs._url != rhs._url {return false} + if lhs._title != rhs._title {return false} + if lhs._image != rhs._image {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.LokiProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".LokiProfile" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "displayName"), + 2: .same(proto: "profilePicture"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._displayName) + case 2: try decoder.decodeSingularStringField(value: &self._profilePicture) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._displayName { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._profilePicture { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.LokiProfile, rhs: SignalServiceProtos_DataMessage.LokiProfile) -> Bool { + if lhs._displayName != rhs._displayName {return false} + if lhs._profilePicture != rhs._profilePicture {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.ClosedGroupUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".ClosedGroupUpdate" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 2: .same(proto: "groupPublicKey"), + 3: .same(proto: "groupPrivateKey"), + 4: .same(proto: "senderKeys"), + 5: .same(proto: "members"), + 6: .same(proto: "admins"), + 7: .same(proto: "type"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._name) + case 2: try decoder.decodeSingularBytesField(value: &self._groupPublicKey) + case 3: try decoder.decodeSingularBytesField(value: &self._groupPrivateKey) + case 4: try decoder.decodeRepeatedMessageField(value: &self.senderKeys) + case 5: try decoder.decodeRepeatedBytesField(value: &self.members) + case 6: try decoder.decodeRepeatedBytesField(value: &self.admins) + case 7: try decoder.decodeSingularEnumField(value: &self._type) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._name { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._groupPublicKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + if let v = self._groupPrivateKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 3) + } + if !self.senderKeys.isEmpty { + try visitor.visitRepeatedMessageField(value: self.senderKeys, fieldNumber: 4) + } + if !self.members.isEmpty { + try visitor.visitRepeatedBytesField(value: self.members, fieldNumber: 5) + } + if !self.admins.isEmpty { + try visitor.visitRepeatedBytesField(value: self.admins, fieldNumber: 6) + } + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate, rhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate) -> Bool { + if lhs._name != rhs._name {return false} + if lhs._groupPublicKey != rhs._groupPublicKey {return false} + if lhs._groupPrivateKey != rhs._groupPrivateKey {return false} + if lhs.senderKeys != rhs.senderKeys {return false} + if lhs.members != rhs.members {return false} + if lhs.admins != rhs.admins {return false} + if lhs._type != rhs._type {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "NEW"), + 1: .same(proto: "INFO"), + 2: .same(proto: "SENDER_KEY_REQUEST"), + 3: .same(proto: "SENDER_KEY"), + ] +} + +extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_DataMessage.ClosedGroupUpdate.protoMessageName + ".SenderKey" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "chainKey"), + 2: .same(proto: "keyIndex"), + 3: .same(proto: "publicKey"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._chainKey) + case 2: try decoder.decodeSingularUInt32Field(value: &self._keyIndex) + case 3: try decoder.decodeSingularBytesField(value: &self._publicKey) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._chainKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._keyIndex { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } + if let v = self._publicKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey, rhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate.SenderKey) -> Bool { + if lhs._chainKey != rhs._chainKey {return false} + if lhs._keyIndex != rhs._keyIndex {return false} + if lhs._publicKey != rhs._publicKey {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_NullMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".NullMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "padding"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._padding) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._padding { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_NullMessage, rhs: SignalServiceProtos_NullMessage) -> Bool { + if lhs._padding != rhs._padding {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_ReceiptMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ReceiptMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "timestamp"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularEnumField(value: &self._type) + case 2: try decoder.decodeRepeatedUInt64Field(value: &self.timestamp) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 1) + } + if !self.timestamp.isEmpty { + try visitor.visitRepeatedUInt64Field(value: self.timestamp, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_ReceiptMessage, rhs: SignalServiceProtos_ReceiptMessage) -> Bool { + if lhs._type != rhs._type {return false} + if lhs.timestamp != rhs.timestamp {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_ReceiptMessage.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "DELIVERY"), + 1: .same(proto: "READ"), + ] +} + +extension SignalServiceProtos_Verified: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Verified" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "destination"), + 2: .same(proto: "identityKey"), + 3: .same(proto: "state"), + 4: .same(proto: "nullMessage"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._destination) + case 2: try decoder.decodeSingularBytesField(value: &self._identityKey) + case 3: try decoder.decodeSingularEnumField(value: &self._state) + case 4: try decoder.decodeSingularBytesField(value: &self._nullMessage) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._destination { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._identityKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + } + if let v = self._state { + try visitor.visitSingularEnumField(value: v, fieldNumber: 3) + } + if let v = self._nullMessage { + try visitor.visitSingularBytesField(value: v, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_Verified, rhs: SignalServiceProtos_Verified) -> Bool { + if lhs._destination != rhs._destination {return false} + if lhs._identityKey != rhs._identityKey {return false} + if lhs._state != rhs._state {return false} + if lhs._nullMessage != rhs._nullMessage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_Verified.State: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "DEFAULT"), + 1: .same(proto: "VERIFIED"), + 2: .same(proto: "UNVERIFIED"), + ] +} + +extension SignalServiceProtos_SyncMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SyncMessage" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "sent"), + 2: .same(proto: "contacts"), + 3: .same(proto: "groups"), + 4: .same(proto: "request"), + 5: .same(proto: "read"), + 6: .same(proto: "blocked"), + 7: .same(proto: "verified"), + 9: .same(proto: "configuration"), + 8: .same(proto: "padding"), + 100: .same(proto: "openGroups"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularMessageField(value: &self._sent) + case 2: try decoder.decodeSingularMessageField(value: &self._contacts) + case 3: try decoder.decodeSingularMessageField(value: &self._groups) + case 4: try decoder.decodeSingularMessageField(value: &self._request) + case 5: try decoder.decodeRepeatedMessageField(value: &self.read) + case 6: try decoder.decodeSingularMessageField(value: &self._blocked) + case 7: try decoder.decodeSingularMessageField(value: &self._verified) + case 8: try decoder.decodeSingularBytesField(value: &self._padding) + case 9: try decoder.decodeSingularMessageField(value: &self._configuration) + case 100: try decoder.decodeRepeatedMessageField(value: &self.openGroups) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._sent { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } + if let v = self._contacts { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } + if let v = self._groups { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + if let v = self._request { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } + if !self.read.isEmpty { + try visitor.visitRepeatedMessageField(value: self.read, fieldNumber: 5) + } + if let v = self._blocked { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } + if let v = self._verified { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } + if let v = self._padding { + try visitor.visitSingularBytesField(value: v, fieldNumber: 8) + } + if let v = self._configuration { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } + if !self.openGroups.isEmpty { + try visitor.visitRepeatedMessageField(value: self.openGroups, fieldNumber: 100) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage, rhs: SignalServiceProtos_SyncMessage) -> Bool { + if lhs._sent != rhs._sent {return false} + if lhs._contacts != rhs._contacts {return false} + if lhs._groups != rhs._groups {return false} + if lhs._request != rhs._request {return false} + if lhs.read != rhs.read {return false} + if lhs._blocked != rhs._blocked {return false} + if lhs._verified != rhs._verified {return false} + if lhs._configuration != rhs._configuration {return false} + if lhs._padding != rhs._padding {return false} + if lhs.openGroups != rhs.openGroups {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Sent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".Sent" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "destination"), + 2: .same(proto: "timestamp"), + 3: .same(proto: "message"), + 4: .same(proto: "expirationStartTimestamp"), + 5: .same(proto: "unidentifiedStatus"), + 6: .same(proto: "isRecipientUpdate"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._destination) + case 2: try decoder.decodeSingularUInt64Field(value: &self._timestamp) + case 3: try decoder.decodeSingularMessageField(value: &self._message) + case 4: try decoder.decodeSingularUInt64Field(value: &self._expirationStartTimestamp) + case 5: try decoder.decodeRepeatedMessageField(value: &self.unidentifiedStatus) + case 6: try decoder.decodeSingularBoolField(value: &self._isRecipientUpdate) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._destination { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._timestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2) + } + if let v = self._message { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + if let v = self._expirationStartTimestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4) + } + if !self.unidentifiedStatus.isEmpty { + try visitor.visitRepeatedMessageField(value: self.unidentifiedStatus, fieldNumber: 5) + } + if let v = self._isRecipientUpdate { + try visitor.visitSingularBoolField(value: v, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Sent, rhs: SignalServiceProtos_SyncMessage.Sent) -> Bool { + if lhs._destination != rhs._destination {return false} + if lhs._timestamp != rhs._timestamp {return false} + if lhs._message != rhs._message {return false} + if lhs._expirationStartTimestamp != rhs._expirationStartTimestamp {return false} + if lhs.unidentifiedStatus != rhs.unidentifiedStatus {return false} + if lhs._isRecipientUpdate != rhs._isRecipientUpdate {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.Sent.protoMessageName + ".UnidentifiedDeliveryStatus" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "destination"), + 2: .same(proto: "unidentified"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._destination) + case 2: try decoder.decodeSingularBoolField(value: &self._unidentified) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._destination { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._unidentified { + try visitor.visitSingularBoolField(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus, rhs: SignalServiceProtos_SyncMessage.Sent.UnidentifiedDeliveryStatus) -> Bool { + if lhs._destination != rhs._destination {return false} + if lhs._unidentified != rhs._unidentified {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Contacts: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".Contacts" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "blob"), + 2: .same(proto: "isComplete"), + 101: .same(proto: "data"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularMessageField(value: &self._blob) + case 2: try decoder.decodeSingularBoolField(value: &self._isComplete) + case 101: try decoder.decodeSingularBytesField(value: &self._data) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._blob { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } + if let v = self._isComplete { + try visitor.visitSingularBoolField(value: v, fieldNumber: 2) + } + if let v = self._data { + try visitor.visitSingularBytesField(value: v, fieldNumber: 101) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Contacts, rhs: SignalServiceProtos_SyncMessage.Contacts) -> Bool { + if lhs._blob != rhs._blob {return false} + if lhs._isComplete != rhs._isComplete {return false} + if lhs._data != rhs._data {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Groups: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".Groups" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "blob"), + 101: .same(proto: "data"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularMessageField(value: &self._blob) + case 101: try decoder.decodeSingularBytesField(value: &self._data) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._blob { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } + if let v = self._data { + try visitor.visitSingularBytesField(value: v, fieldNumber: 101) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Groups, rhs: SignalServiceProtos_SyncMessage.Groups) -> Bool { + if lhs._blob != rhs._blob {return false} + if lhs._data != rhs._data {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.OpenGroupDetails: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".OpenGroupDetails" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "url"), + 2: .same(proto: "channelID"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._url) + case 2: try decoder.decodeSingularUInt64Field(value: &self._channelID) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._url { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._channelID { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.OpenGroupDetails, rhs: SignalServiceProtos_SyncMessage.OpenGroupDetails) -> Bool { + if lhs._url != rhs._url {return false} + if lhs._channelID != rhs._channelID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Blocked: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".Blocked" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "numbers"), + 2: .same(proto: "groupIds"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeRepeatedStringField(value: &self.numbers) + case 2: try decoder.decodeRepeatedBytesField(value: &self.groupIds) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.numbers.isEmpty { + try visitor.visitRepeatedStringField(value: self.numbers, fieldNumber: 1) + } + if !self.groupIds.isEmpty { + try visitor.visitRepeatedBytesField(value: self.groupIds, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Blocked, rhs: SignalServiceProtos_SyncMessage.Blocked) -> Bool { + if lhs.numbers != rhs.numbers {return false} + if lhs.groupIds != rhs.groupIds {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Request: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".Request" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularEnumField(value: &self._type) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Request, rhs: SignalServiceProtos_SyncMessage.Request) -> Bool { + if lhs._type != rhs._type {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Request.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "CONTACTS"), + 2: .same(proto: "GROUPS"), + 3: .same(proto: "BLOCKED"), + 4: .same(proto: "CONFIGURATION"), + ] +} + +extension SignalServiceProtos_SyncMessage.Read: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".Read" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "sender"), + 2: .same(proto: "timestamp"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._sender) + case 2: try decoder.decodeSingularUInt64Field(value: &self._timestamp) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._sender { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._timestamp { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Read, rhs: SignalServiceProtos_SyncMessage.Read) -> Bool { + if lhs._sender != rhs._sender {return false} + if lhs._timestamp != rhs._timestamp {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_SyncMessage.Configuration: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".Configuration" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "readReceipts"), + 2: .same(proto: "unidentifiedDeliveryIndicators"), + 3: .same(proto: "typingIndicators"), + 4: .same(proto: "linkPreviews"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBoolField(value: &self._readReceipts) + case 2: try decoder.decodeSingularBoolField(value: &self._unidentifiedDeliveryIndicators) + case 3: try decoder.decodeSingularBoolField(value: &self._typingIndicators) + case 4: try decoder.decodeSingularBoolField(value: &self._linkPreviews) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._readReceipts { + try visitor.visitSingularBoolField(value: v, fieldNumber: 1) + } + if let v = self._unidentifiedDeliveryIndicators { + try visitor.visitSingularBoolField(value: v, fieldNumber: 2) + } + if let v = self._typingIndicators { + try visitor.visitSingularBoolField(value: v, fieldNumber: 3) + } + if let v = self._linkPreviews { + try visitor.visitSingularBoolField(value: v, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_SyncMessage.Configuration, rhs: SignalServiceProtos_SyncMessage.Configuration) -> Bool { + if lhs._readReceipts != rhs._readReceipts {return false} + if lhs._unidentifiedDeliveryIndicators != rhs._unidentifiedDeliveryIndicators {return false} + if lhs._typingIndicators != rhs._typingIndicators {return false} + if lhs._linkPreviews != rhs._linkPreviews {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_AttachmentPointer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".AttachmentPointer" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "contentType"), + 3: .same(proto: "key"), + 4: .same(proto: "size"), + 5: .same(proto: "thumbnail"), + 6: .same(proto: "digest"), + 7: .same(proto: "fileName"), + 8: .same(proto: "flags"), + 9: .same(proto: "width"), + 10: .same(proto: "height"), + 11: .same(proto: "caption"), + 101: .same(proto: "url"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularFixed64Field(value: &self._id) + case 2: try decoder.decodeSingularStringField(value: &self._contentType) + case 3: try decoder.decodeSingularBytesField(value: &self._key) + case 4: try decoder.decodeSingularUInt32Field(value: &self._size) + case 5: try decoder.decodeSingularBytesField(value: &self._thumbnail) + case 6: try decoder.decodeSingularBytesField(value: &self._digest) + case 7: try decoder.decodeSingularStringField(value: &self._fileName) + case 8: try decoder.decodeSingularUInt32Field(value: &self._flags) + case 9: try decoder.decodeSingularUInt32Field(value: &self._width) + case 10: try decoder.decodeSingularUInt32Field(value: &self._height) + case 11: try decoder.decodeSingularStringField(value: &self._caption) + case 101: try decoder.decodeSingularStringField(value: &self._url) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularFixed64Field(value: v, fieldNumber: 1) + } + if let v = self._contentType { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._key { + try visitor.visitSingularBytesField(value: v, fieldNumber: 3) + } + if let v = self._size { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 4) + } + if let v = self._thumbnail { + try visitor.visitSingularBytesField(value: v, fieldNumber: 5) + } + if let v = self._digest { + try visitor.visitSingularBytesField(value: v, fieldNumber: 6) + } + if let v = self._fileName { + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + } + if let v = self._flags { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8) + } + if let v = self._width { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 9) + } + if let v = self._height { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 10) + } + if let v = self._caption { + try visitor.visitSingularStringField(value: v, fieldNumber: 11) + } + if let v = self._url { + try visitor.visitSingularStringField(value: v, fieldNumber: 101) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_AttachmentPointer, rhs: SignalServiceProtos_AttachmentPointer) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._contentType != rhs._contentType {return false} + if lhs._key != rhs._key {return false} + if lhs._size != rhs._size {return false} + if lhs._thumbnail != rhs._thumbnail {return false} + if lhs._digest != rhs._digest {return false} + if lhs._fileName != rhs._fileName {return false} + if lhs._flags != rhs._flags {return false} + if lhs._width != rhs._width {return false} + if lhs._height != rhs._height {return false} + if lhs._caption != rhs._caption {return false} + if lhs._url != rhs._url {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_AttachmentPointer.Flags: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "VOICE_MESSAGE"), + ] +} + +extension SignalServiceProtos_GroupContext: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".GroupContext" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "type"), + 3: .same(proto: "name"), + 4: .same(proto: "members"), + 5: .same(proto: "avatar"), + 6: .same(proto: "admins"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._id) + case 2: try decoder.decodeSingularEnumField(value: &self._type) + case 3: try decoder.decodeSingularStringField(value: &self._name) + case 4: try decoder.decodeRepeatedStringField(value: &self.members) + case 5: try decoder.decodeSingularMessageField(value: &self._avatar) + case 6: try decoder.decodeRepeatedStringField(value: &self.admins) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._type { + try visitor.visitSingularEnumField(value: v, fieldNumber: 2) + } + if let v = self._name { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } + if !self.members.isEmpty { + try visitor.visitRepeatedStringField(value: self.members, fieldNumber: 4) + } + if let v = self._avatar { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } + if !self.admins.isEmpty { + try visitor.visitRepeatedStringField(value: self.admins, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_GroupContext, rhs: SignalServiceProtos_GroupContext) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._type != rhs._type {return false} + if lhs._name != rhs._name {return false} + if lhs.members != rhs.members {return false} + if lhs._avatar != rhs._avatar {return false} + if lhs.admins != rhs.admins {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_GroupContext.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "UPDATE"), + 2: .same(proto: "DELIVER"), + 3: .same(proto: "QUIT"), + 4: .same(proto: "REQUEST_INFO"), + ] +} + +extension SignalServiceProtos_ContactDetails: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ContactDetails" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "number"), + 2: .same(proto: "name"), + 3: .same(proto: "avatar"), + 4: .same(proto: "color"), + 5: .same(proto: "verified"), + 6: .same(proto: "profileKey"), + 7: .same(proto: "blocked"), + 8: .same(proto: "expireTimer"), + 101: .same(proto: "nickname"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._number) + case 2: try decoder.decodeSingularStringField(value: &self._name) + case 3: try decoder.decodeSingularMessageField(value: &self._avatar) + case 4: try decoder.decodeSingularStringField(value: &self._color) + case 5: try decoder.decodeSingularMessageField(value: &self._verified) + case 6: try decoder.decodeSingularBytesField(value: &self._profileKey) + case 7: try decoder.decodeSingularBoolField(value: &self._blocked) + case 8: try decoder.decodeSingularUInt32Field(value: &self._expireTimer) + case 101: try decoder.decodeSingularStringField(value: &self._nickname) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._number { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._name { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if let v = self._avatar { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } + if let v = self._color { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } + if let v = self._verified { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } + if let v = self._profileKey { + try visitor.visitSingularBytesField(value: v, fieldNumber: 6) + } + if let v = self._blocked { + try visitor.visitSingularBoolField(value: v, fieldNumber: 7) + } + if let v = self._expireTimer { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 8) + } + if let v = self._nickname { + try visitor.visitSingularStringField(value: v, fieldNumber: 101) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_ContactDetails, rhs: SignalServiceProtos_ContactDetails) -> Bool { + if lhs._number != rhs._number {return false} + if lhs._name != rhs._name {return false} + if lhs._avatar != rhs._avatar {return false} + if lhs._color != rhs._color {return false} + if lhs._verified != rhs._verified {return false} + if lhs._profileKey != rhs._profileKey {return false} + if lhs._blocked != rhs._blocked {return false} + if lhs._expireTimer != rhs._expireTimer {return false} + if lhs._nickname != rhs._nickname {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_ContactDetails.Avatar: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_ContactDetails.protoMessageName + ".Avatar" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "contentType"), + 2: .same(proto: "length"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._contentType) + case 2: try decoder.decodeSingularUInt32Field(value: &self._length) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._contentType { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._length { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_ContactDetails.Avatar, rhs: SignalServiceProtos_ContactDetails.Avatar) -> Bool { + if lhs._contentType != rhs._contentType {return false} + if lhs._length != rhs._length {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_GroupDetails: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".GroupDetails" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "name"), + 3: .same(proto: "members"), + 4: .same(proto: "avatar"), + 5: .same(proto: "active"), + 6: .same(proto: "expireTimer"), + 7: .same(proto: "color"), + 8: .same(proto: "blocked"), + 9: .same(proto: "admins"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularBytesField(value: &self._id) + case 2: try decoder.decodeSingularStringField(value: &self._name) + case 3: try decoder.decodeRepeatedStringField(value: &self.members) + case 4: try decoder.decodeSingularMessageField(value: &self._avatar) + case 5: try decoder.decodeSingularBoolField(value: &self._active) + case 6: try decoder.decodeSingularUInt32Field(value: &self._expireTimer) + case 7: try decoder.decodeSingularStringField(value: &self._color) + case 8: try decoder.decodeSingularBoolField(value: &self._blocked) + case 9: try decoder.decodeRepeatedStringField(value: &self.admins) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._id { + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + } + if let v = self._name { + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + } + if !self.members.isEmpty { + try visitor.visitRepeatedStringField(value: self.members, fieldNumber: 3) + } + if let v = self._avatar { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } + if let v = self._active { + try visitor.visitSingularBoolField(value: v, fieldNumber: 5) + } + if let v = self._expireTimer { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 6) + } + if let v = self._color { + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + } + if let v = self._blocked { + try visitor.visitSingularBoolField(value: v, fieldNumber: 8) + } + if !self.admins.isEmpty { + try visitor.visitRepeatedStringField(value: self.admins, fieldNumber: 9) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_GroupDetails, rhs: SignalServiceProtos_GroupDetails) -> Bool { + if lhs._id != rhs._id {return false} + if lhs._name != rhs._name {return false} + if lhs.members != rhs.members {return false} + if lhs._avatar != rhs._avatar {return false} + if lhs._active != rhs._active {return false} + if lhs._expireTimer != rhs._expireTimer {return false} + if lhs._color != rhs._color {return false} + if lhs._blocked != rhs._blocked {return false} + if lhs.admins != rhs.admins {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_GroupDetails.Avatar: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = SignalServiceProtos_GroupDetails.protoMessageName + ".Avatar" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "contentType"), + 2: .same(proto: "length"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self._contentType) + case 2: try decoder.decodeSingularUInt32Field(value: &self._length) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._contentType { + try visitor.visitSingularStringField(value: v, fieldNumber: 1) + } + if let v = self._length { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_GroupDetails.Avatar, rhs: SignalServiceProtos_GroupDetails.Avatar) -> Bool { + if lhs._contentType != rhs._contentType {return false} + if lhs._length != rhs._length {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension SignalServiceProtos_PublicChatInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PublicChatInfo" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "serverID"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularUInt64Field(value: &self._serverID) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if let v = self._serverID { + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: SignalServiceProtos_PublicChatInfo, rhs: SignalServiceProtos_PublicChatInfo) -> Bool { + if lhs._serverID != rhs._serverID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/SignalUtilitiesKit/SignalServiceClient.swift b/SignalUtilitiesKit/SignalServiceClient.swift new file mode 100644 index 000000000..b7bd546b4 --- /dev/null +++ b/SignalUtilitiesKit/SignalServiceClient.swift @@ -0,0 +1,94 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + + +public typealias RecipientIdentifier = String + +@objc +public protocol SignalServiceClientObjC { + @objc func updateAccountAttributesObjC() -> AnyPromise +} + +public protocol SignalServiceClient: SignalServiceClientObjC { + func getAvailablePreKeys() -> Promise + func registerPreKeys(identityKey: IdentityKey, signedPreKeyRecord: SignedPreKeyRecord, preKeyRecords: [PreKeyRecord]) -> Promise + func setCurrentSignedPreKey(_ signedPreKey: SignedPreKeyRecord) -> Promise + func requestUDSenderCertificate() -> Promise + func updateAccountAttributes() -> Promise +} + +/// Based on libsignal-service-java's PushServiceSocket class +@objc +public class SignalServiceRestClient: NSObject, SignalServiceClient { + + var networkManager: TSNetworkManager { + return TSNetworkManager.shared() + } + + private var udManager: OWSUDManager { + return SSKEnvironment.shared.udManager + } + + func unexpectedServerResponseError() -> Error { + return OWSErrorMakeUnableToProcessServerResponseError() + } + + public func getAvailablePreKeys() -> Promise { + Logger.debug("") + + let request = OWSRequestFactory.availablePreKeysCountRequest() + return firstly { + networkManager.makePromise(request: request) + }.map { _, responseObject in + Logger.debug("got response") + guard let params = ParamParser(responseObject: responseObject) else { + throw self.unexpectedServerResponseError() + } + + let count: Int = try params.required(key: "count") + + return count + } + } + + public func registerPreKeys(identityKey: IdentityKey, signedPreKeyRecord: SignedPreKeyRecord, preKeyRecords: [PreKeyRecord]) -> Promise { + Logger.debug("") + + let request = OWSRequestFactory.registerPrekeysRequest(withPrekeyArray: preKeyRecords, identityKey: identityKey, signedPreKey: signedPreKeyRecord) + return networkManager.makePromise(request: request).asVoid() + } + + public func setCurrentSignedPreKey(_ signedPreKey: SignedPreKeyRecord) -> Promise { + Logger.debug("") + + let request = OWSRequestFactory.registerSignedPrekeyRequest(with: signedPreKey) + return networkManager.makePromise(request: request).asVoid() + } + + public func requestUDSenderCertificate() -> Promise { + let request = OWSRequestFactory.udSenderCertificateRequest() + return firstly { + self.networkManager.makePromise(request: request) + }.map { _, responseObject in + guard let parser = ParamParser(responseObject: responseObject) else { + throw OWSUDError.invalidData(description: "Invalid sender certificate response") + } + + return try parser.requiredBase64EncodedData(key: "certificate") + } + } + + @objc + public func updateAccountAttributesObjC() -> AnyPromise { + return AnyPromise(updateAccountAttributes()) + } + + public func updateAccountAttributes() -> Promise { + let request = OWSRequestFactory.updateAttributesRequest() + return networkManager.makePromise(request: request).asVoid() + } +} diff --git a/SignalUtilitiesKit/SignalServiceProfile.swift b/SignalUtilitiesKit/SignalServiceProfile.swift new file mode 100644 index 000000000..e1175884f --- /dev/null +++ b/SignalUtilitiesKit/SignalServiceProfile.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class SignalServiceProfile: NSObject { + + public enum ValidationError: Error { + case invalid(description: String) + case invalidIdentityKey(description: String) + case invalidProfileName(description: String) + } + + public let recipientId: String + public let identityKey: Data + public let profileNameEncrypted: Data? + public let avatarUrlPath: String? + public let unidentifiedAccessVerifier: Data? + public let hasUnrestrictedUnidentifiedAccess: Bool + + public init(recipientId: String, responseObject: Any?) throws { + self.recipientId = recipientId + + guard let params = ParamParser(responseObject: responseObject) else { + throw ValidationError.invalid(description: "invalid response: \(String(describing: responseObject))") + } + + let identityKeyWithType = try params.requiredBase64EncodedData(key: "identityKey") + let kIdentityKeyLength = 33 + guard identityKeyWithType.count == kIdentityKeyLength else { + throw ValidationError.invalidIdentityKey(description: "malformed identity key \(identityKeyWithType.hexadecimalString) with decoded length: \(identityKeyWithType.count)") + } + do { + // `removeKeyType` is an objc category method only on NSData, so temporarily cast. + self.identityKey = try (identityKeyWithType as NSData).removeKeyType() as Data + } catch { + // `removeKeyType` throws an SCKExceptionWrapperError, which, typically should + // be unwrapped by any objc code calling this method. + owsFailDebug("identify key had unexpected format") + throw ValidationError.invalidIdentityKey(description: "malformed identity key \(identityKeyWithType.hexadecimalString) with data: \(identityKeyWithType)") + } + + self.profileNameEncrypted = try params.optionalBase64EncodedData(key: "name") + + let avatarUrlPath: String? = try params.optional(key: "avatar") + self.avatarUrlPath = avatarUrlPath + + self.unidentifiedAccessVerifier = try params.optionalBase64EncodedData(key: "unidentifiedAccess") + + self.hasUnrestrictedUnidentifiedAccess = try params.optional(key: "unrestrictedUnidentifiedAccess") ?? false + } +} diff --git a/SignalUtilitiesKit/Storage+ClosedGroups.swift b/SignalUtilitiesKit/Storage+ClosedGroups.swift new file mode 100644 index 000000000..f4e031dd3 --- /dev/null +++ b/SignalUtilitiesKit/Storage+ClosedGroups.swift @@ -0,0 +1,83 @@ + +public extension Storage { + + internal enum ClosedGroupRatchetCollectionType { + case old, current + } + + // MARK: Ratchets + internal static func getClosedGroupRatchetCollection(_ collection: ClosedGroupRatchetCollectionType, for groupPublicKey: String) -> String { + switch collection { + case .old: return "LokiOldClosedGroupRatchetCollection.\(groupPublicKey)" + case .current: return "LokiClosedGroupRatchetCollection.\(groupPublicKey)" + } + } + + internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> ClosedGroupRatchet? { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) + var result: ClosedGroupRatchet? + read { transaction in + result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet + } + return result + } + + internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, in collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) + transaction.setObject(ratchet, forKey: senderPublicKey, inCollection: collection) + } + + internal static func getAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) + var result: [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] = [] + read { transaction in + transaction.enumerateRows(inCollection: collection) { key, object, _, _ in + guard let senderPublicKey = key as? String, let ratchet = object as? ClosedGroupRatchet else { return } + result.append((senderPublicKey: senderPublicKey, ratchet: ratchet)) + } + } + return result + } + + internal static func getAllClosedGroupSenderKeys(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> Set { + return Set(getAllClosedGroupRatchets(for: groupPublicKey, from: collection).map { senderPublicKey, ratchet in + ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: senderPublicKey)) + }) + } + + internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) + transaction.removeAllObjects(inCollection: collection) + } +} + +@objc public extension Storage { + + // MARK: Private Keys + internal static let closedGroupPrivateKeyCollection = "LokiClosedGroupPrivateKeyCollection" + + public static func getUserClosedGroupPublicKeys() -> Set { + var result: Set = [] + read { transaction in + result = Set(transaction.allKeys(inCollection: closedGroupPrivateKeyCollection)) + } + return result + } + + @objc(getPrivateKeyForClosedGroupWithPublicKey:) + internal static func getClosedGroupPrivateKey(for publicKey: String) -> String? { + var result: String? + read { transaction in + result = transaction.object(forKey: publicKey, inCollection: closedGroupPrivateKeyCollection) as? String + } + return result + } + + internal static func setClosedGroupPrivateKey(_ privateKey: String, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(privateKey, forKey: publicKey, inCollection: closedGroupPrivateKeyCollection) + } + + internal static func removeClosedGroupPrivateKey(for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.removeObject(forKey: publicKey, inCollection: closedGroupPrivateKeyCollection) + } +} diff --git a/SignalUtilitiesKit/Storage+Collections.swift b/SignalUtilitiesKit/Storage+Collections.swift new file mode 100644 index 000000000..d017cf89d --- /dev/null +++ b/SignalUtilitiesKit/Storage+Collections.swift @@ -0,0 +1,21 @@ + +// TODO: Create an extension for each category, e.g. Storage+OpenGroups, Storage+SnodePool, etc. + +@objc public extension Storage { + + // TODO: Add remaining collections + + @objc func getDeviceLinkCollection(for masterPublicKey: String) -> String { + return "LokiDeviceLinkCollection-\(masterPublicKey)" + } + + @objc public static func getSwarmCollection(for publicKey: String) -> String { + return "LokiSwarmCollection-\(publicKey)" + } + + @objc public static let openGroupCollection = "LokiPublicChatCollection" + @objc public static let openGroupProfilePictureURLCollection = "LokiPublicChatAvatarURLCollection" + @objc public static let openGroupUserCountCollection = "LokiPublicChatUserCountCollection" + @objc public static let sessionRequestTimestampCollection = "LokiSessionRequestTimestampCollection" + @objc public static let snodePoolCollection = "LokiSnodePoolCollection" +} diff --git a/SignalUtilitiesKit/Storage+OnionRequests.swift b/SignalUtilitiesKit/Storage+OnionRequests.swift new file mode 100644 index 000000000..3a1e417ba --- /dev/null +++ b/SignalUtilitiesKit/Storage+OnionRequests.swift @@ -0,0 +1,48 @@ + +public extension Storage { + + // MARK: Onion Request Paths + internal static let onionRequestPathCollection = "LokiOnionRequestPathCollection" + + internal static func setOnionRequestPaths(_ paths: [OnionRequestAPI.Path], using transaction: YapDatabaseReadWriteTransaction) { + let collection = onionRequestPathCollection + // FIXME: This approach assumes either 1 or 2 paths of length 3 each. We should do better than this. + clearOnionRequestPaths(using: transaction) + guard paths.count >= 1 else { return } + let path0 = paths[0] + guard path0.count == 3 else { return } + transaction.setObject(path0[0], forKey: "0-0", inCollection: collection) + transaction.setObject(path0[1], forKey: "0-1", inCollection: collection) + transaction.setObject(path0[2], forKey: "0-2", inCollection: collection) + guard paths.count >= 2 else { return } + let path1 = paths[1] + guard path1.count == 3 else { return } + transaction.setObject(path1[0], forKey: "1-0", inCollection: collection) + transaction.setObject(path1[1], forKey: "1-1", inCollection: collection) + transaction.setObject(path1[2], forKey: "1-2", inCollection: collection) + } + + public static func getOnionRequestPaths() -> [OnionRequestAPI.Path] { + let collection = onionRequestPathCollection + var result: [OnionRequestAPI.Path] = [] + read { transaction in + if + let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? Snode, + let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? Snode, + let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? Snode { + result.append([ path0Snode0, path0Snode1, path0Snode2 ]) + if + let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? Snode, + let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? Snode, + let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? Snode { + result.append([ path1Snode0, path1Snode1, path1Snode2 ]) + } + } + } + return result + } + + internal static func clearOnionRequestPaths(using transaction: YapDatabaseReadWriteTransaction) { + transaction.removeAllObjects(inCollection: onionRequestPathCollection) + } +} diff --git a/SignalUtilitiesKit/Storage+PublicChats.swift b/SignalUtilitiesKit/Storage+PublicChats.swift new file mode 100644 index 000000000..8cabffc42 --- /dev/null +++ b/SignalUtilitiesKit/Storage+PublicChats.swift @@ -0,0 +1,38 @@ + +public extension Storage { + + // MARK: Open Group Public Keys + internal static let openGroupPublicKeyCollection = "LokiOpenGroupPublicKeyCollection" + internal static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection" + internal static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection" + + internal static func getOpenGroupPublicKey(for server: String) -> String? { + var result: String? = nil + read { transaction in + result = transaction.object(forKey: server, inCollection: openGroupPublicKeyCollection) as? String + } + return result + } + + internal static func setOpenGroupPublicKey(for server: String, to publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(publicKey, forKey: server, inCollection: openGroupPublicKeyCollection) + } + + internal static func removeOpenGroupPublicKey(for server: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.removeObject(forKey: server, inCollection: openGroupPublicKeyCollection) + } + + private static func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection) + } + + private static func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection) + } + + internal static func clearAllData(for group: UInt64, on server: String, using transaction: YapDatabaseReadWriteTransaction) { + removeLastMessageServerID(for: group, on: server, using: transaction) + removeLastDeletionServerID(for: group, on: server, using: transaction) + Storage.removeOpenGroupPublicKey(for: server, using: transaction) + } +} diff --git a/SignalUtilitiesKit/Storage+SessionManagement.swift b/SignalUtilitiesKit/Storage+SessionManagement.swift new file mode 100644 index 000000000..7284a9601 --- /dev/null +++ b/SignalUtilitiesKit/Storage+SessionManagement.swift @@ -0,0 +1,31 @@ + +public extension Storage { + + // MARK: Session Request Timestamps + internal static let sessionRequestSentTimestampCollection = "LokiSessionRequestSentTimestampCollection" + internal static let sessionRequestProcessedTimestampCollection = "LokiSessionRequestProcessedTimestampCollection" + + internal static func getSessionRequestSentTimestamp(for publicKey: String) -> UInt64 { + var result: UInt64? + read { transaction in + result = transaction.object(forKey: publicKey, inCollection: sessionRequestSentTimestampCollection) as? UInt64 + } + return result ?? 0 + } + + internal static func setSessionRequestSentTimestamp(for publicKey: String, to timestamp: UInt64, using transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(timestamp, forKey: publicKey, inCollection: sessionRequestSentTimestampCollection) + } + + internal static func getSessionRequestProcessedTimestamp(for publicKey: String) -> UInt64 { + var result: UInt64? + read { transaction in + result = transaction.object(forKey: publicKey, inCollection: sessionRequestProcessedTimestampCollection) as? UInt64 + } + return result ?? 0 + } + + internal static func setSessionRequestProcessedTimestamp(for publicKey: String, to timestamp: UInt64, using transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(timestamp, forKey: publicKey, inCollection: sessionRequestProcessedTimestampCollection) + } +} diff --git a/SignalUtilitiesKit/Storage+SnodeAPI.swift b/SignalUtilitiesKit/Storage+SnodeAPI.swift new file mode 100644 index 000000000..b7e6aa29e --- /dev/null +++ b/SignalUtilitiesKit/Storage+SnodeAPI.swift @@ -0,0 +1,58 @@ + +internal extension Storage { + + // MARK: Last Message Hash + private static let lastMessageHashCollection = "LokiLastMessageHashCollection" + + internal static func getLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String) -> JSON? { + let key = "\(snode.address):\(snode.port).\(publicKey)" + var result: JSON? + read { transaction in + result = transaction.object(forKey: key, inCollection: lastMessageHashCollection) as? JSON + } + if let result = result { + guard result["hash"] as? String != nil else { return nil } + guard result["expirationDate"] as? NSNumber != nil else { return nil } + } + return result + } + + internal static func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, associatedWith: publicKey), + let hash = lastMessageHashInfo["hash"] as? String, let expirationDate = (lastMessageHashInfo["expirationDate"] as? NSNumber)?.uint64Value else { return } + let now = NSDate.ows_millisecondTimeStamp() + if now >= expirationDate { + removeLastMessageHashInfo(for: snode, associatedWith: publicKey, using: transaction) + } + } + + internal static func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? { + return getLastMessageHashInfo(for: snode, associatedWith: publicKey)?["hash"] as? String + } + + internal static func removeLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + let key = "\(snode.address):\(snode.port).\(publicKey)" + transaction.removeObject(forKey: key, inCollection: lastMessageHashCollection) + } + + internal static func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: YapDatabaseReadWriteTransaction) { + let key = "\(snode.address):\(snode.port).\(publicKey)" + guard lastMessageHashInfo.count == 2 && lastMessageHashInfo["hash"] as? String != nil && lastMessageHashInfo["expirationDate"] as? NSNumber != nil else { return } + transaction.setObject(lastMessageHashInfo, forKey: key, inCollection: lastMessageHashCollection) + } + + // MARK: Received Messages + private static let receivedMessagesCollection = "LokiReceivedMessagesCollection" + + internal static func getReceivedMessages(for publicKey: String) -> Set? { + var result: Set? + read { transaction in + result = transaction.object(forKey: publicKey, inCollection: receivedMessagesCollection) as? Set + } + return result + } + + internal static func setReceivedMessages(to receivedMessages: Set, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) { + transaction.setObject(receivedMessages, forKey: publicKey, inCollection: receivedMessagesCollection) + } +} diff --git a/SignalUtilitiesKit/Storage.swift b/SignalUtilitiesKit/Storage.swift new file mode 100644 index 000000000..add543757 --- /dev/null +++ b/SignalUtilitiesKit/Storage.swift @@ -0,0 +1,76 @@ +import PromiseKit + +// Some important notes about YapDatabase: +// +// • Connections are thread-safe. +// • Executing a write transaction from within a write transaction is NOT allowed. + +@objc(LKStorage) +public final class Storage : NSObject { + public static let serialQueue = DispatchQueue(label: "Storage.serialQueue", qos: .userInitiated) + + private static var owsStorage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + + // MARK: Reading + + // Some important points regarding reading from the database: + // + // • Background threads should use `OWSPrimaryStorage`'s `dbReadConnection`, whereas the main thread should use `OWSPrimaryStorage`'s `uiDatabaseConnection` (see the `YapDatabaseConnectionPool` documentation for more information). + // • Multiple read transactions can safely be executed at the same time. + + @objc(readWithBlock:) + public static func read(with block: @escaping (YapDatabaseReadTransaction) -> Void) { + // FIXME: For some reason the code below appears to be causing crashes even though it *should* + // be in line with the YapDatabase docs + /* + let isMainThread = Thread.current.isMainThread + let connection = isMainThread ? owsStorage.uiDatabaseConnection : owsStorage.dbReadConnection + connection.read(block) + */ + owsStorage.dbReadConnection.read(block) + } + + // MARK: Writing + + // Some important points regarding writing to the database: + // + // • There can only be a single write transaction per database at any one time, so all write transactions must use `OWSPrimaryStorage`'s `dbReadWriteConnection`. + // • Executing a write transaction from within a write transaction causes a deadlock and must be avoided. + + @discardableResult + @objc(writeWithBlock:) + public static func objc_write(with block: @escaping (YapDatabaseReadWriteTransaction) -> Void) -> AnyPromise { + return AnyPromise.from(write(with: block) { }) + } + + @discardableResult + public static func write(with block: @escaping (YapDatabaseReadWriteTransaction) -> Void) -> Promise { + return write(with: block) { } + } + + @discardableResult + @objc(writeWithBlock:completion:) + public static func objc_write(with block: @escaping (YapDatabaseReadWriteTransaction) -> Void, completion: @escaping () -> Void) -> AnyPromise { + return AnyPromise.from(write(with: block, completion: completion)) + } + + @discardableResult + public static func write(with block: @escaping (YapDatabaseReadWriteTransaction) -> Void, completion: @escaping () -> Void) -> Promise { + let (promise, seal) = Promise.pending() + serialQueue.async { + owsStorage.dbReadWriteConnection.readWrite { transaction in + transaction.addCompletionQueue(DispatchQueue.main, completionBlock: completion) + block(transaction) + } + seal.fulfill(()) + } + return promise + } + + /// Blocks the calling thread until the write has finished. + @discardableResult + @objc(writeSyncWithBlock:) + public static func writeSync(with block: @escaping (YapDatabaseReadWriteTransaction) -> Void) { + try! write(with: block, completion: { }).wait() // The promise returned by write(with:completion:) never rejects + } +} diff --git a/SignalUtilitiesKit/String+SSK.swift b/SignalUtilitiesKit/String+SSK.swift new file mode 100644 index 000000000..cdfdaee57 --- /dev/null +++ b/SignalUtilitiesKit/String+SSK.swift @@ -0,0 +1,86 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public extension String { + public var digitsOnly: String { + return (self as NSString).digitsOnly() + } + + func rtlSafeAppend(_ string: String) -> String { + return (self as NSString).rtlSafeAppend(string) + } + + public func substring(from index: Int) -> String { + return String(self[self.index(self.startIndex, offsetBy: index)...]) + } + + public func substring(to index: Int) -> String { + return String(prefix(index)) + } + + enum StringError: Error { + case invalidCharacterShift + } +} + +// MARK: - Selector Encoding + +private let selectorOffset: UInt32 = 17 + +public extension String { + + public func caesar(shift: UInt32) throws -> String { + let shiftedScalars: [UnicodeScalar] = try unicodeScalars.map { c in + guard let shiftedScalar = UnicodeScalar((c.value + shift) % 127) else { + owsFailDebug("invalidCharacterShift") + throw StringError.invalidCharacterShift + } + return shiftedScalar + } + return String(String.UnicodeScalarView(shiftedScalars)) + } + + public var encodedForSelector: String? { + guard let shifted = try? self.caesar(shift: selectorOffset) else { + owsFailDebug("shifted was unexpectedly nil") + return nil + } + + guard let data = shifted.data(using: .utf8) else { + owsFailDebug("data was unexpectedly nil") + return nil + } + + return data.base64EncodedString() + } + + public var decodedForSelector: String? { + guard let data = Data(base64Encoded: self) else { + owsFailDebug("data was unexpectedly nil") + return nil + } + + guard let shifted = String(data: data, encoding: .utf8) else { + owsFailDebug("shifted was unexpectedly nil") + return nil + } + + return try? shifted.caesar(shift: 127 - selectorOffset) + } +} + +public extension NSString { + + @objc + public var encodedForSelector: String? { + return (self as String).encodedForSelector + } + + @objc + public var decodedForSelector: String? { + return (self as String).decodedForSelector + } +} diff --git a/SignalUtilitiesKit/String+Trimming.swift b/SignalUtilitiesKit/String+Trimming.swift new file mode 100644 index 000000000..221d66b8c --- /dev/null +++ b/SignalUtilitiesKit/String+Trimming.swift @@ -0,0 +1,9 @@ + +@objc extension NSString { + + @objc public func removing05PrefixIfNeeded() -> NSString { + var result = self as String + if result.count == 66 && result.hasPrefix("05") { result.removeFirst(2) } + return result as NSString + } +} diff --git a/SignalUtilitiesKit/SwiftSingletons.swift b/SignalUtilitiesKit/SwiftSingletons.swift new file mode 100644 index 000000000..346069bf1 --- /dev/null +++ b/SignalUtilitiesKit/SwiftSingletons.swift @@ -0,0 +1,35 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public class SwiftSingletons: NSObject { + public static let shared = SwiftSingletons() + + private var classSet = Set() + + private override init() { + super.init() + } + + public func register(_ singleton: AnyObject) { + guard !CurrentAppContext().isRunningTests else { + return + } + guard _isDebugAssertConfiguration() else { + return + } + let singletonClassName = String(describing: type(of: singleton)) + guard !classSet.contains(singletonClassName) else { + owsFailDebug("Duplicate singleton: \(singletonClassName).") + return + } + Logger.verbose("Registering singleton: \(singletonClassName).") + classSet.insert(singletonClassName) + } + + public static func register(_ singleton: AnyObject) { + shared.register(singleton) + } +} diff --git a/SignalUtilitiesKit/SyncMessagesProtocol.swift b/SignalUtilitiesKit/SyncMessagesProtocol.swift new file mode 100644 index 000000000..ba159ef62 --- /dev/null +++ b/SignalUtilitiesKit/SyncMessagesProtocol.swift @@ -0,0 +1,297 @@ +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(LKSyncMessagesProtocol) +public final class SyncMessagesProtocol : NSObject { + + /// Only ever modified from the message processing queue (`OWSBatchMessageProcessor.processingQueue`). + private static var syncMessageTimestamps: [String:Set] = [:] + + internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + + // MARK: - Error + + @objc(LKSyncMessagesProtocolError) + public class SyncMessagesProtocolError : NSError { // Not called `Error` for Obj-C interoperablity + + @objc public static let privateKeyMissing = SyncMessagesProtocolError(domain: "SyncMessagesProtocolErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Couldn't get private key for SSK based closed group." ]) + } + + // MARK: - Sending + + @objc public static func syncProfile() { + Storage.writeSync { transaction in + let userPublicKey = getUserHexEncodedPublicKey() + let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction) + for device in userLinkedDevices { + guard device != userPublicKey else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: device, transaction: transaction) + thread.save(with: transaction) + let syncMessage = OWSOutgoingSyncMessage(in: thread, messageBody: "", attachmentId: nil) + syncMessage.save(with: transaction) + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + messageSenderJobQueue.add(message: syncMessage, transaction: transaction) + } + } + } + + @objc(syncContactWithPublicKey:) + public static func syncContact(_ publicKey: String) -> AnyPromise { + let syncManager = SSKEnvironment.shared.syncManager + return syncManager.syncContacts(for: [ SignalAccount(recipientId: publicKey) ]) + } + + private static func getContactsToSync(using transaction: YapDatabaseReadTransaction) -> Set { + return Set(TSContactThread.allObjectsInCollection().compactMap { $0 as? TSContactThread } + .filter { $0.shouldThreadBeVisible } + .map { $0.contactIdentifier() } + .filter { ECKeyPair.isValidHexEncodedPublicKey(candidate: $0) } + .filter { storage.getMasterHexEncodedPublicKey(for: $0, in: transaction) == nil } // Exclude secondary devices + .filter { !LokiDatabaseUtilities.isUserLinkedDevice($0, transaction: transaction) }) + } + + @objc public static func syncAllContacts() -> AnyPromise { + var publicKeys: [String] = [] + storage.dbReadConnection.read { transaction in + publicKeys = [String](getContactsToSync(using: transaction)) + } + let accounts = Set(publicKeys).map { SignalAccount(recipientId: $0) } + let syncManager = SSKEnvironment.shared.syncManager + let promises = accounts.chunked(by: 3).map { accounts -> Promise in // TODO: Does this always fit? + return Promise(syncManager.syncContacts(for: accounts)).map2 { _ in } + } + return AnyPromise.from(when(fulfilled: promises)) + } + + @objc(syncClosedGroup:transaction:) + public static func syncClosedGroup(_ thread: TSGroupThread, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { + // Prepare + let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue + let group = thread.groupModel + let groupPublicKey = LKGroupUtilities.getDecodedGroupID(group.groupId) + let name = group.groupName! + let members = group.groupMemberIds.map { Data(hex: $0) } + let admins = group.groupAdminIds.map { Data(hex: $0) } + guard let groupPrivateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey) else { + print("[Loki] Couldn't get private key for SSK based closed group.") + return AnyPromise.from(Promise(error: SyncMessagesProtocolError.privateKeyMissing)) + } + // Generate ratchets for the user's linked devices + let userPublicKey = getUserHexEncodedPublicKey() + let masterPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey + let deviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction) + let linkedDevices = deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }.filter { $0 != userPublicKey } + let senderKeys: [ClosedGroupSenderKey] = linkedDevices.map { publicKey in + let ratchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) + return ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) + } + // Send a closed group update message to the existing members with the linked devices' ratchets (this message is aimed at the group) + func sendMessageToGroup() { + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: senderKeys, + members: members, admins: admins) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) + } + sendMessageToGroup() + // Send closed group update messages to the linked devices using established channels + func sendMessageToLinkedDevices() { + var allSenderKeys = Storage.getAllClosedGroupSenderKeys(for: groupPublicKey) + allSenderKeys.formUnion(senderKeys) + let thread = TSContactThread.getOrCreateThread(withContactId: masterPublicKey, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, + groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: [ClosedGroupSenderKey](allSenderKeys), members: members, admins: admins) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction) // This internally takes care of multi device + } + sendMessageToLinkedDevices() + // Return a dummy promise + return AnyPromise.from(Promise { $0.fulfill(()) }) + } + + @objc public static func syncAllClosedGroups() -> AnyPromise { + var closedGroups: [TSGroupThread] = [] + TSGroupThread.enumerateCollectionObjects { object, _ in + guard let closedGroup = object as? TSGroupThread, closedGroup.groupModel.groupType == .closedGroup, + closedGroup.shouldThreadBeVisible else { return } + closedGroups.append(closedGroup) + } + let syncManager = SSKEnvironment.shared.syncManager + let promises = closedGroups.map { group -> Promise in + return Promise(syncManager.syncGroup(for: group)).map2 { _ in } + } + return AnyPromise.from(when(fulfilled: promises)) + } + + @objc public static func syncAllOpenGroups() -> AnyPromise { + let openGroupSyncMessage = SyncOpenGroupsMessage() + let (promise, seal) = Promise.pending() + let messageSender = SSKEnvironment.shared.messageSender + messageSender.send(openGroupSyncMessage, success: { + seal.fulfill(()) + }, failure: { error in + seal.reject(error) + }) + return AnyPromise.from(promise) + } + + // MARK: - Receiving + + @objc(isValidSyncMessage:transaction:) + public static func isValidSyncMessage(_ envelope: SSKProtoEnvelope, transaction: YapDatabaseReadTransaction) -> Bool { + let publicKey = envelope.source! // Set during UD decryption + return LokiDatabaseUtilities.isUserLinkedDevice(publicKey, transaction: transaction) + } + + public static func dropFromSyncMessageTimestampCache(_ timestamp: UInt64, for publicKey: String) { + var timestamps: Set = syncMessageTimestamps[publicKey] ?? [] + if timestamps.contains(timestamp) { timestamps.remove(timestamp) } + syncMessageTimestamps[publicKey] = timestamps + } + + @objc(isDuplicateSyncMessage:fromPublicKey:) + public static func isDuplicateSyncMessage(_ protoContent: SSKProtoContent, from publicKey: String) -> Bool { + guard let syncMessage = protoContent.syncMessage?.sent else { return false } + var timestamps: Set = syncMessageTimestamps[publicKey] ?? [] + let hasTimestamp = syncMessage.timestamp != 0 + guard hasTimestamp else { return false } + let result = timestamps.contains(syncMessage.timestamp) + timestamps.insert(syncMessage.timestamp) + syncMessageTimestamps[publicKey] = timestamps + return result + } + + @objc(updateProfileFromSyncMessageIfNeeded:wrappedIn:transaction:) + public static func updateProfileFromSyncMessageIfNeeded(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) { + let publicKey = envelope.source! // Set during UD decryption + guard let userMasterPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return } + let wasSentByMasterDevice = (userMasterPublicKey == publicKey) + guard wasSentByMasterDevice else { return } + SessionMetaProtocol.updateDisplayNameIfNeeded(for: userMasterPublicKey, using: dataMessage, in: transaction) + SessionMetaProtocol.updateProfileKeyIfNeeded(for: userMasterPublicKey, using: dataMessage) + } + + /// - Note: Deprecated. + @objc(handleClosedGroupUpdateSyncMessageIfNeeded:wrappedIn:transaction:) + public static func handleClosedGroupUpdateSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) { + // Check preconditions + let publicKey = envelope.source! // Set during UD decryption + let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction) + let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey) + guard wasSentByLinkedDevice, let group = transcript.dataMessage.group, let name = group.name else { return } + // Create or update the group + let id = group.id + let members = group.members + let newGroupThread = TSGroupThread.getOrCreateThread(withGroupId: id, groupType: .closedGroup, transaction: transaction) + let newGroupModel = TSGroupModel(title: name, memberIds: members, image: nil, groupId: id, groupType: .closedGroup, adminIds: group.admins) + newGroupThread.save(with: transaction) + newGroupThread.setGroupModel(newGroupModel, with: transaction) + OWSDisappearingMessagesJob.shared().becomeConsistent(withDisappearingDuration: transcript.dataMessage.expireTimer, thread: newGroupThread, createdByRemoteRecipientId: nil, createdInExistingGroup: true, transaction: transaction) + // Try to establish sessions with all members for which none exists yet when a group is created or updated + ClosedGroupsProtocol.establishSessionsIfNeeded(with: members, using: transaction) + // Notify the user + let contactsManager = SSKEnvironment.shared.contactsManager + let infoMessageText = newGroupThread.groupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: contactsManager) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: newGroupThread, messageType: .typeGroupUpdate, customMessage: infoMessageText) + infoMessage.save(with: transaction) + } + + /// - Note: Deprecated. + @objc(handleClosedGroupQuitSyncMessageIfNeeded:wrappedIn:transaction:) + public static func handleClosedGroupQuitSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) { + // Check preconditions + let publicKey = envelope.source! // Set during UD decryption + let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction) + let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey) + guard wasSentByLinkedDevice, let group = transcript.dataMessage.group else { return } + // Leave the group + let groupThread = TSGroupThread.getOrCreateThread(withGroupId: group.id, groupType: .closedGroup, transaction: transaction) + groupThread.save(with: transaction) + groupThread.leaveGroup(with: transaction) + // Notify the user + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: groupThread, messageType: .typeGroupQuit, customMessage: NSLocalizedString("GROUP_YOU_LEFT", comment: "")) + infoMessage.save(with: transaction) + } + + @objc(handleContactSyncMessageIfNeeded:wrappedIn:transaction:) + public static func handleContactSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) { + let publicKey = envelope.source! // Set during UD decryption + let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction) + let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey) + guard wasSentByLinkedDevice, let contacts = syncMessage.contacts, let contactsAsData = contacts.data, !contactsAsData.isEmpty else { return } + print("[Loki] Contact sync message received.") + handleContactSyncMessageData(contactsAsData, using: transaction) + } + + public static func handleContactSyncMessageData(_ data: Data, using transaction: YapDatabaseReadWriteTransaction) { + let parser = ContactParser(data: data) + let tuples = parser.parse() + let blockedPublicKeys = tuples.filter { $0.isBlocked }.map { $0.publicKey } + let userPublicKey = getUserHexEncodedPublicKey() + let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction) + // Try to establish sessions + for (publicKey, isBlocked) in tuples { + guard !userLinkedDevices.contains(publicKey) else { continue } // Skip self and linked devices + let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction) + thread.shouldThreadBeVisible = true + thread.save(with: transaction) + if !isBlocked { + SessionManagementProtocol.sendSessionRequestIfNeeded(to: publicKey, using: transaction) + } + } + // Update the blocked contacts list + transaction.addCompletionQueue(DispatchQueue.main) { + SSKEnvironment.shared.blockingManager.setBlockedPhoneNumbers(blockedPublicKeys, sendSyncMessage: false) + NotificationCenter.default.post(name: .blockedContactsUpdated, object: nil) + } + } + + /// - Note: Deprecated. + @objc(handleClosedGroupSyncMessageIfNeeded:wrappedIn:transaction:) + public static func handleClosedGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) { + let publicKey = envelope.source! // Set during UD decryption + let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction) + let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey) + guard wasSentByLinkedDevice, let groups = syncMessage.groups, let groupsAsData = groups.data, !groupsAsData.isEmpty else { return } + print("[Loki] Closed group sync message received.") + let parser = ClosedGroupParser(data: groupsAsData) + let closedGroups = parser.parseGroupModels() + for closedGroup in closedGroups { + var thread: TSGroupThread! = TSGroupThread(groupId: closedGroup.groupId, transaction: transaction) + if thread == nil { + thread = TSGroupThread.getOrCreateThread(with: closedGroup, transaction: transaction) + thread.shouldThreadBeVisible = true + thread.save(with: transaction) + } + ClosedGroupsProtocol.establishSessionsIfNeeded(with: closedGroup.groupMemberIds, using: transaction) + } + } + + @objc(handleOpenGroupSyncMessageIfNeeded:wrappedIn:transaction:) + public static func handleOpenGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) { + let publicKey = envelope.source! // Set during UD decryption + let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction) + let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey) + guard wasSentByLinkedDevice else { return } + let openGroups = syncMessage.openGroups + guard !openGroups.isEmpty else { return } + print("[Loki] Open group sync message received.") + let openGroupManager = PublicChatManager.shared + let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() + let userDisplayName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: userPublicKey, transaction: transaction) + for openGroup in openGroups { + guard openGroupManager.getChat(server: openGroup.url, channel: openGroup.channelID) == nil else { return } + openGroupManager.addChat(server: openGroup.url, channel: openGroup.channelID) + OpenGroupAPI.setDisplayName(to: userDisplayName, on: openGroup.url) + // TODO: Should we also set the profile picture here? + } + } +} diff --git a/SignalUtilitiesKit/TSAccountManager.h b/SignalUtilitiesKit/TSAccountManager.h new file mode 100644 index 000000000..c8dd602cc --- /dev/null +++ b/SignalUtilitiesKit/TSAccountManager.h @@ -0,0 +1,171 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const TSRegistrationErrorDomain; +extern NSString *const TSRegistrationErrorUserInfoHTTPStatus; +extern NSString *const RegistrationStateDidChangeNotification; +extern NSString *const kNSNotificationName_LocalNumberDidChange; + +@class AnyPromise; +@class OWSPrimaryStorage; +@class TSNetworkManager; +@class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; + +typedef NS_ENUM(NSUInteger, OWSRegistrationState) { + OWSRegistrationState_Unregistered, + OWSRegistrationState_PendingBackupRestore, + OWSRegistrationState_Registered, + OWSRegistrationState_Deregistered, + OWSRegistrationState_Reregistering, +}; + +@interface TSAccountManager : NSObject + +@property (nonatomic, nullable) NSString *phoneNumberAwaitingVerification; + +#pragma mark - Initializers + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER; + ++ (instancetype)sharedInstance; + +- (OWSRegistrationState)registrationState; + +/** + * Returns if a user is registered or not + * + * @return registered or not + */ +- (BOOL)isRegistered; +- (BOOL)isRegisteredAndReady; + +/** + * Returns current phone number for this device, which may not yet have been registered. + * + * @return E164 formatted phone number + */ ++ (nullable NSString *)localNumber; +- (nullable NSString *)localNumber; + +// A variant of localNumber that never opens a "sneaky" transaction. +- (nullable NSString *)storedOrCachedLocalNumber:(YapDatabaseReadTransaction *)transaction; + +/** + * Symmetric key that's used to encrypt message payloads from the server, + * + * @return signaling key + */ ++ (nullable NSString *)signalingKey; +- (nullable NSString *)signalingKey; + +/** + * The server auth token allows the Signal client to connect to the Signal server + * + * @return server authentication token + */ ++ (nullable NSString *)serverAuthToken; +- (nullable NSString *)serverAuthToken; + +/** + * The registration ID is unique to an installation of TextSecure, it allows to know if the app was reinstalled + * + * @return registrationID; + */ + ++ (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction; +- (uint32_t)getOrGenerateRegistrationId; +- (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark - Register with phone number + +- (void)registerWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock + smsVerification:(BOOL)isSMS; + +- (void)rerequestSMSWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock; + +- (void)rerequestVoiceWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock; + +- (void)verifyAccountWithCode:(NSString *)verificationCode + pin:(nullable NSString *)pin + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock; + +// Called once registration is complete - meaning the following have succeeded: +// - obtained signal server credentials +// - uploaded pre-keys +// - uploaded push tokens +- (void)didRegister; + +#if TARGET_OS_IPHONE + +/** + * Register's the device's push notification token with the server + * + * @param pushToken Apple's Push Token + */ +- (void)registerForPushNotificationsWithPushToken:(NSString *)pushToken + voipToken:(NSString *)voipToken + isForcedUpdate:(BOOL)isForcedUpdate + success:(void (^)(void))successHandler + failure:(void (^)(NSError *error))failureHandler + NS_SWIFT_NAME(registerForPushNotifications(pushToken:voipToken:isForcedUpdate:success:failure:)); + +#endif + ++ (void)unregisterTextSecureWithSuccess:(void (^)(void))success failure:(void (^)(NSError *error))failureBlock; + +#pragma mark - De-Registration + +// De-registration reflects whether or not the "last known contact" +// with the service was: +// +// * A 403 from the service, indicating de-registration. +// * A successful auth'd request _or_ websocket connection indicating +// valid registration. +- (BOOL)isDeregistered; +- (void)setIsDeregistered:(BOOL)isDeregistered; + +- (BOOL)hasPendingBackupRestoreDecision; +- (void)setHasPendingBackupRestoreDecision:(BOOL)value; + +#pragma mark - Re-registration + +// Re-registration is the process of re-registering _with the same phone number_. + +// Returns YES on success. +- (BOOL)resetForReregistration; +- (nullable NSString *)reregisterationPhoneNumber; +- (BOOL)isReregistering; + +#pragma mark - Manual Message Fetch + +- (BOOL)isManualMessageFetchEnabled; +- (AnyPromise *)setIsManualMessageFetchEnabled:(BOOL)value __attribute__((warn_unused_result)); + +#ifdef DEBUG +- (void)registerForTestsWithLocalNumber:(NSString *)localNumber; +#endif + +- (AnyPromise *)updateAccountAttributes __attribute__((warn_unused_result)); + +// This should only be used during the registration process. +- (AnyPromise *)performUpdateAccountAttributes __attribute__((warn_unused_result)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSAccountManager.m b/SignalUtilitiesKit/TSAccountManager.m new file mode 100644 index 000000000..c8f512e18 --- /dev/null +++ b/SignalUtilitiesKit/TSAccountManager.m @@ -0,0 +1,765 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSAccountManager.h" +#import "AppContext.h" +#import "AppReadiness.h" +#import "NSNotificationCenter+OWS.h" +#import "NSURLSessionDataTask+StatusCode.h" +#import "OWSError.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSRequestFactory.h" +#import "ProfileManagerProtocol.h" +#import "SSKEnvironment.h" +#import "TSNetworkManager.h" +#import "TSPreKeyManager.h" +#import "YapDatabaseConnection+OWS.h" +#import "YapDatabaseTransaction+OWS.h" +#import +#import +#import +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const TSRegistrationErrorDomain = @"TSRegistrationErrorDomain"; +NSString *const TSRegistrationErrorUserInfoHTTPStatus = @"TSHTTPStatus"; +NSString *const RegistrationStateDidChangeNotification = @"RegistrationStateDidChangeNotification"; +NSString *const kNSNotificationName_LocalNumberDidChange = @"kNSNotificationName_LocalNumberDidChange"; + +NSString *const TSAccountManager_RegisteredNumberKey = @"TSStorageRegisteredNumberKey"; +NSString *const TSAccountManager_IsDeregisteredKey = @"TSAccountManager_IsDeregisteredKey"; +NSString *const TSAccountManager_ReregisteringPhoneNumberKey = @"TSAccountManager_ReregisteringPhoneNumberKey"; +NSString *const TSAccountManager_LocalRegistrationIdKey = @"TSStorageLocalRegistrationId"; +NSString *const TSAccountManager_HasPendingRestoreDecisionKey = @"TSAccountManager_HasPendingRestoreDecisionKey"; + +NSString *const TSAccountManager_UserAccountCollection = @"TSStorageUserAccountCollection"; +NSString *const TSAccountManager_ServerAuthToken = @"TSStorageServerAuthToken"; +NSString *const TSAccountManager_ServerSignalingKey = @"TSStorageServerSignalingKey"; +NSString *const TSAccountManager_ManualMessageFetchKey = @"TSAccountManager_ManualMessageFetchKey"; +NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountManager_NeedsAccountAttributesUpdateKey"; + +@interface TSAccountManager () + +@property (atomic, readonly) BOOL isRegistered; + +@property (nonatomic, nullable) NSString *cachedLocalNumber; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@property (nonatomic, nullable) NSNumber *cachedIsDeregistered; + +@property (nonatomic) Reachability *reachability; + +@end + +#pragma mark - + +@implementation TSAccountManager + +@synthesize isRegistered = _isRegistered; + +- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage +{ + self = [super init]; + if (!self) { + return self; + } + + _dbConnection = [primaryStorage newDatabaseConnection]; + self.reachability = [Reachability reachabilityForInternetConnection]; + + OWSSingletonAssert(); + + if (!CurrentAppContext().isMainApp) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(yapDatabaseModifiedExternally:) + name:YapDatabaseModifiedExternallyNotification + object:nil]; + } + + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [[self updateAccountAttributesIfNecessary] retainUntilComplete]; + }]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reachabilityChanged) + name:kReachabilityChangedNotification + object:nil]; + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + ++ (instancetype)sharedInstance +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +#pragma mark - Dependencies + +- (TSNetworkManager *)networkManager +{ + OWSAssertDebug(SSKEnvironment.shared.networkManager); + + return SSKEnvironment.shared.networkManager; +} + +- (id)profileManager { + OWSAssertDebug(SSKEnvironment.shared.profileManager); + + return SSKEnvironment.shared.profileManager; +} + +#pragma mark - + +- (void)setPhoneNumberAwaitingVerification:(NSString *_Nullable)phoneNumberAwaitingVerification +{ + _phoneNumberAwaitingVerification = phoneNumberAwaitingVerification; + + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_LocalNumberDidChange + object:nil + userInfo:nil]; +} + +- (OWSRegistrationState)registrationState +{ + if (!self.isRegistered) { + return OWSRegistrationState_Unregistered; + } else if (self.isDeregistered) { + if (self.isReregistering) { + return OWSRegistrationState_Reregistering; + } else { + return OWSRegistrationState_Deregistered; + } + } else if (self.isDeregistered) { + return OWSRegistrationState_PendingBackupRestore; + } else { + return OWSRegistrationState_Registered; + } +} + +- (BOOL)isRegistered +{ + @synchronized (self) { + if (_isRegistered) { + return YES; + } else { + // Cache this once it's true since it's called alot, involves a dbLookup, and once set - it doesn't change. + _isRegistered = [self storedLocalNumber] != nil; + } + return _isRegistered; + } +} + +- (BOOL)isRegisteredAndReady +{ + return self.registrationState == OWSRegistrationState_Registered; +} + +- (void)didRegister +{ + OWSLogInfo(@"didRegister"); + NSString *phoneNumber = self.phoneNumberAwaitingVerification; + + if (!phoneNumber) { + OWSFail(@"phoneNumber was unexpectedly nil"); + } + + [self storeLocalNumber:phoneNumber]; + + // Warm these cached values. + [self isRegistered]; + [self localNumber]; + [self isDeregistered]; + + [self postRegistrationStateDidChangeNotification]; +} + ++ (nullable NSString *)localNumber +{ + return [[self sharedInstance] localNumber]; +} + +- (nullable NSString *)localNumber +{ + NSString *awaitingVerif = self.phoneNumberAwaitingVerification; + if (awaitingVerif) { + return awaitingVerif; + } + + // Cache this since we access this a lot, and once set it will not change. + @synchronized(self) + { + if (self.cachedLocalNumber == nil) { + self.cachedLocalNumber = self.storedLocalNumber; + } + } + + return self.cachedLocalNumber; +} + +- (nullable NSString *)storedLocalNumber +{ + @synchronized (self) { + return [self.dbConnection stringForKey:TSAccountManager_RegisteredNumberKey + inCollection:TSAccountManager_UserAccountCollection]; + } +} + +- (nullable NSString *)storedOrCachedLocalNumber:(YapDatabaseReadTransaction *)transaction +{ + @synchronized(self) { + if (self.cachedLocalNumber) { + return self.cachedLocalNumber; + } + } + + return [transaction stringForKey:TSAccountManager_RegisteredNumberKey + inCollection:TSAccountManager_UserAccountCollection]; +} + +- (void)storeLocalNumber:(NSString *)localNumber +{ + @synchronized (self) { + [self.dbConnection setObject:localNumber + forKey:TSAccountManager_RegisteredNumberKey + inCollection:TSAccountManager_UserAccountCollection]; + + [self.dbConnection removeObjectForKey:TSAccountManager_ReregisteringPhoneNumberKey + inCollection:TSAccountManager_UserAccountCollection]; + + self.phoneNumberAwaitingVerification = nil; + + self.cachedLocalNumber = localNumber; + } +} + ++ (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction +{ + return [[self sharedInstance] getOrGenerateRegistrationId:transaction]; +} + +- (uint32_t)getOrGenerateRegistrationId +{ + __block uint32_t result; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + result = [self getOrGenerateRegistrationId:transaction]; + }]; + return result; +} + +- (uint32_t)getOrGenerateRegistrationId:(YapDatabaseReadWriteTransaction *)transaction +{ + // Unlike other methods in this class, there's no need for a `@synchronized` block + // here, since we're already in a write transaction, and all writes occur on a serial queue. + // + // Since other code in this class which uses @synchronized(self) also needs to open write + // transaction, using @synchronized(self) here, inside of a WriteTransaction risks deadlock. + uint32_t registrationID = [[transaction objectForKey:TSAccountManager_LocalRegistrationIdKey + inCollection:TSAccountManager_UserAccountCollection] unsignedIntValue]; + + if (registrationID == 0) { + registrationID = (uint32_t)arc4random_uniform(16380) + 1; + OWSLogWarn(@"Generated a new registrationID: %u", registrationID); + + [transaction setObject:[NSNumber numberWithUnsignedInteger:registrationID] + forKey:TSAccountManager_LocalRegistrationIdKey + inCollection:TSAccountManager_UserAccountCollection]; + } + return registrationID; +} + +- (void)registerForPushNotificationsWithPushToken:(NSString *)pushToken + voipToken:(NSString *)voipToken + isForcedUpdate:(BOOL)isForcedUpdate + success:(void (^)(void))successHandler + failure:(void (^)(NSError *))failureHandler +{ + [self registerForPushNotificationsWithPushToken:pushToken + voipToken:voipToken + isForcedUpdate:isForcedUpdate + success:successHandler + failure:failureHandler + remainingRetries:3]; +} + +- (void)registerForPushNotificationsWithPushToken:(NSString *)pushToken + voipToken:(NSString *)voipToken + isForcedUpdate:(BOOL)isForcedUpdate + success:(void (^)(void))successHandler + failure:(void (^)(NSError *))failureHandler + remainingRetries:(int)remainingRetries +{ + BOOL isUsingFullAPNs = [NSUserDefaults.standardUserDefaults boolForKey:@"isUsingFullAPNs"]; + NSData *pushTokenAsData = [NSData dataFromHexString:pushToken]; + AnyPromise *promise = isUsingFullAPNs ? [LKPushNotificationManager registerWithToken:pushTokenAsData hexEncodedPublicKey:self.localNumber isForcedUpdate:isForcedUpdate] + : [LKPushNotificationManager unregisterWithToken:pushTokenAsData isForcedUpdate:isForcedUpdate]; + promise + .then(^() { + successHandler(); + }) + .catch(^(NSError *error) { + if (remainingRetries > 0) { + [self registerForPushNotificationsWithPushToken:pushToken voipToken:voipToken isForcedUpdate:isForcedUpdate success:successHandler failure:failureHandler + remainingRetries:remainingRetries - 1]; + } else { + failureHandler(error); + } + }); +} + +- (void)registerWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock + smsVerification:(BOOL)isSMS + +{ + if ([self isRegistered]) { + failureBlock([NSError errorWithDomain:@"tsaccountmanager.verify" code:4000 userInfo:nil]); + return; + } + + // The country code of TSAccountManager.phoneNumberAwaitingVerification is used to + // determine whether or not to use domain fronting, so it needs to be set _before_ + // we make our verification code request. + self.phoneNumberAwaitingVerification = phoneNumber; + + TSRequest *request = + [OWSRequestFactory requestVerificationCodeRequestWithPhoneNumber:phoneNumber + captchaToken:captchaToken + transport:(isSMS ? TSVerificationTransportSMS + : TSVerificationTransportVoice)]; + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSLogInfo(@"Successfully requested verification code request for number: %@ method:%@", + phoneNumber, + isSMS ? @"SMS" : @"Voice"); + successBlock(); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSLogError(@"Failed to request verification code request with error:%@", error); + failureBlock(error); + }]; +} + +- (void)rerequestSMSWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock +{ + // TODO: Can we remove phoneNumberAwaitingVerification? + NSString *number = self.phoneNumberAwaitingVerification; + OWSAssertDebug(number); + + [self registerWithPhoneNumber:number + captchaToken:captchaToken + success:successBlock + failure:failureBlock + smsVerification:YES]; +} + +- (void)rerequestVoiceWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock +{ + NSString *number = self.phoneNumberAwaitingVerification; + OWSAssertDebug(number); + + [self registerWithPhoneNumber:number + captchaToken:captchaToken + success:successBlock + failure:failureBlock + smsVerification:NO]; +} + +- (void)verifyAccountWithCode:(NSString *)verificationCode + pin:(nullable NSString *)pin + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock +{ + NSString *authToken = [[self class] generateNewAccountAuthenticationToken]; + NSString *phoneNumber = self.phoneNumberAwaitingVerification; + + OWSAssertDebug(authToken); + OWSAssertDebug(phoneNumber); + + TSRequest *request = [OWSRequestFactory verifyCodeRequestWithVerificationCode:verificationCode + forNumber:phoneNumber + pin:pin + authKey:authToken]; + + [self.networkManager makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response; + long statuscode = response.statusCode; + + switch (statuscode) { + case 200: + case 204: { + OWSLogInfo(@"Verification code accepted."); + + [self storeServerAuthToken:authToken]; + + [[[SignalServiceRestClient new] updateAccountAttributesObjC] + .thenInBackground(^{ + return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { + [TSPreKeyManager + createPreKeysWithSuccess:^{ + resolve(@(1)); + } + failure:^(NSError *error) { + resolve(error); + }]; + }]; + }) + .then(^{ + [self.profileManager fetchLocalUsersProfile]; + }) + .then(^{ + successBlock(); + }) + .catchInBackground(^(NSError *error) { + OWSLogError(@"Error: %@", error); + failureBlock(error); + }) retainUntilComplete]; + + break; + } + default: { + OWSLogError(@"Unexpected status while verifying code: %ld", statuscode); + NSError *error = OWSErrorMakeUnableToProcessServerResponseError(); + failureBlock(error); + break; + } + } + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSAssertDebug([error.domain isEqualToString:TSNetworkManagerErrorDomain]); + + OWSLogWarn(@"Error verifying code: %@", error.debugDescription); + + switch (error.code) { + case 403: { + NSError *userError = OWSErrorWithCodeDescription(OWSErrorCodeUserError, + NSLocalizedString(@"REGISTRATION_VERIFICATION_FAILED_WRONG_CODE_DESCRIPTION", + "Error message indicating that registration failed due to a missing or incorrect " + "verification code.")); + failureBlock(userError); + break; + } + case 413: { + // In the case of the "rate limiting" error, we want to show the + // "recovery suggestion", not the error's "description." + NSError *userError + = OWSErrorWithCodeDescription(OWSErrorCodeUserError, error.localizedRecoverySuggestion); + failureBlock(userError); + break; + } + case 423: { + NSString *localizedMessage = NSLocalizedString(@"REGISTRATION_VERIFICATION_FAILED_WRONG_PIN", + "Error message indicating that registration failed due to a missing or incorrect 2FA PIN."); + OWSLogError(@"2FA PIN required: %ld", (long)error.code); + NSError *error + = OWSErrorWithCodeDescription(OWSErrorCodeRegistrationMissing2FAPIN, localizedMessage); + failureBlock(error); + break; + } + default: { + OWSLogError(@"verifying code failed with unknown error: %@", error); + failureBlock(error); + break; + } + } + }]; +} + +#pragma mark Server keying material + ++ (NSString *)generateNewAccountAuthenticationToken { + NSData *authToken = [Randomness generateRandomBytes:16]; + NSString *authTokenPrint = [[NSData dataWithData:authToken] hexadecimalString]; + return authTokenPrint; +} + ++ (nullable NSString *)signalingKey +{ + return [[self sharedInstance] signalingKey]; +} + +- (nullable NSString *)signalingKey +{ + return [self.dbConnection stringForKey:TSAccountManager_ServerSignalingKey + inCollection:TSAccountManager_UserAccountCollection]; +} + ++ (nullable NSString *)serverAuthToken +{ + return [[self sharedInstance] serverAuthToken]; +} + +- (nullable NSString *)serverAuthToken +{ + return [self.dbConnection stringForKey:TSAccountManager_ServerAuthToken + inCollection:TSAccountManager_UserAccountCollection]; +} + +- (void)storeServerAuthToken:(NSString *)authToken +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction setObject:authToken + forKey:TSAccountManager_ServerAuthToken + inCollection:TSAccountManager_UserAccountCollection]; + }]; +} + ++ (void)unregisterTextSecureWithSuccess:(void (^)(void))success failure:(void (^)(NSError *error))failureBlock +{ + TSRequest *request = [OWSRequestFactory unregisterAccountRequest]; + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + OWSLogInfo(@"Successfully unregistered"); + success(); + + // This is called from `[AppSettingsViewController proceedToUnregistration]` whose + // success handler calls `[Environment resetAppData]`. + // This method, after calling that success handler, fires + // `RegistrationStateDidChangeNotification` which is only safe to fire after + // the data store is reset. + + [self.sharedInstance postRegistrationStateDidChangeNotification]; + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + OWSLogError(@"Failed to unregister with error: %@", error); + failureBlock(error); + }]; +} + +- (void)yapDatabaseModifiedExternally:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + OWSLogVerbose(@""); + + // Any database write by the main app might reflect a deregistration, + // so clear the cached "is registered" state. This will significantly + // erode the value of this cache in the SAE. + @synchronized(self) + { + _isRegistered = NO; + } +} + +#pragma mark - De-Registration + +- (BOOL)isDeregistered +{ + // Cache this since we access this a lot, and once set it will not change. + @synchronized(self) { + if (self.cachedIsDeregistered == nil) { + self.cachedIsDeregistered = @([self.dbConnection boolForKey:TSAccountManager_IsDeregisteredKey + inCollection:TSAccountManager_UserAccountCollection + defaultValue:NO]); + } + + OWSAssertDebug(self.cachedIsDeregistered); + return self.cachedIsDeregistered.boolValue; + } +} + +- (void)setIsDeregistered:(BOOL)isDeregistered +{ + @synchronized(self) { + if (self.cachedIsDeregistered && self.cachedIsDeregistered.boolValue == isDeregistered) { + return; + } + + OWSLogWarn(@"isDeregistered: %d", isDeregistered); + + self.cachedIsDeregistered = @(isDeregistered); + } + + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction setObject:@(isDeregistered) + forKey:TSAccountManager_IsDeregisteredKey + inCollection:TSAccountManager_UserAccountCollection]; + }]; + + [self postRegistrationStateDidChangeNotification]; +} + +#pragma mark - Re-registration + +- (BOOL)resetForReregistration +{ + @synchronized(self) { + NSString *_Nullable localNumber = self.localNumber; + if (!localNumber) { + OWSFailDebug(@"can't re-register without valid local number."); + return NO; + } + + _isRegistered = NO; + _cachedLocalNumber = nil; + _phoneNumberAwaitingVerification = nil; + _cachedIsDeregistered = nil; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction removeAllObjectsInCollection:TSAccountManager_UserAccountCollection]; + + [[OWSPrimaryStorage sharedManager] resetSessionStore:transaction]; + + [transaction setObject:localNumber + forKey:TSAccountManager_ReregisteringPhoneNumberKey + inCollection:TSAccountManager_UserAccountCollection]; + }]; + + [self postRegistrationStateDidChangeNotification]; + + return YES; + } +} + +- (nullable NSString *)reregisterationPhoneNumber +{ + OWSAssertDebug([self isReregistering]); + + NSString *_Nullable result = [self.dbConnection stringForKey:TSAccountManager_ReregisteringPhoneNumberKey + inCollection:TSAccountManager_UserAccountCollection]; + OWSAssertDebug(result); + return result; +} + +- (BOOL)isReregistering +{ + return nil != + [self.dbConnection stringForKey:TSAccountManager_ReregisteringPhoneNumberKey + inCollection:TSAccountManager_UserAccountCollection]; +} + +- (BOOL)hasPendingBackupRestoreDecision +{ + return [self.dbConnection boolForKey:TSAccountManager_HasPendingRestoreDecisionKey + inCollection:TSAccountManager_UserAccountCollection + defaultValue:NO]; +} + +- (void)setHasPendingBackupRestoreDecision:(BOOL)value +{ + OWSLogInfo(@"%d", value); + + [self.dbConnection setBool:value + forKey:TSAccountManager_HasPendingRestoreDecisionKey + inCollection:TSAccountManager_UserAccountCollection]; + + [self postRegistrationStateDidChangeNotification]; +} + +- (BOOL)isManualMessageFetchEnabled +{ + return [self.dbConnection boolForKey:TSAccountManager_ManualMessageFetchKey + inCollection:TSAccountManager_UserAccountCollection + defaultValue:NO]; +} + +- (AnyPromise *)setIsManualMessageFetchEnabled:(BOOL)value { + [self.dbConnection setBool:value + forKey:TSAccountManager_ManualMessageFetchKey + inCollection:TSAccountManager_UserAccountCollection]; + + // Try to update the account attributes to reflect this change. + return [self updateAccountAttributes]; +} + +- (void)registerForTestsWithLocalNumber:(NSString *)localNumber +{ + OWSAssertDebug(localNumber.length > 0); + + [self storeLocalNumber:localNumber]; +} + +#pragma mark - Account Attributes + +- (AnyPromise *)updateAccountAttributes { + // Enqueue a "account attribute update", recording the "request time". + [self.dbConnection setObject:[NSDate new] + forKey:TSAccountManager_NeedsAccountAttributesUpdateKey + inCollection:TSAccountManager_UserAccountCollection]; + + return [self updateAccountAttributesIfNecessary]; +} + +- (AnyPromise *)updateAccountAttributesIfNecessary { + if (!self.isRegistered) { + return [AnyPromise promiseWithValue:@(1)]; + } + + return [AnyPromise promiseWithValue:@(1)]; + + NSDate *_Nullable updateRequestDate = + [self.dbConnection objectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey + inCollection:TSAccountManager_UserAccountCollection]; + if (!updateRequestDate) { + return [AnyPromise promiseWithValue:@(1)]; + } + AnyPromise *promise = [self performUpdateAccountAttributes]; + promise = promise.then(^(id value) { + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + // Clear the update request unless a new update has been requested + // while this update was in flight. + NSDate *_Nullable latestUpdateRequestDate = + [transaction objectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey + inCollection:TSAccountManager_UserAccountCollection]; + if (latestUpdateRequestDate && [latestUpdateRequestDate isEqual:updateRequestDate]) { + [transaction removeObjectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey + inCollection:TSAccountManager_UserAccountCollection]; + } + }]; + }); + return promise; +} + +- (AnyPromise *)performUpdateAccountAttributes +{ + AnyPromise *promise = [[SignalServiceRestClient new] updateAccountAttributesObjC]; + promise = promise.then(^(id value) { + // Fetch the local profile, as we may have changed its + // account attributes. Specifically, we need to determine + // if all devices for our account now support UD for sync + // messages. + [self.profileManager fetchLocalUsersProfile]; + }); + [promise retainUntilComplete]; + return promise; +} + +- (void)reachabilityChanged { + OWSAssertIsOnMainThread(); + + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [[self updateAccountAttributesIfNecessary] retainUntilComplete]; + }]; +} + +#pragma mark - Notifications + +- (void)postRegistrationStateDidChangeNotification +{ + OWSAssertIsOnMainThread(); + + [[NSNotificationCenter defaultCenter] postNotificationNameAsync:RegistrationStateDidChangeNotification + object:nil + userInfo:nil]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSAttachment.h b/SignalUtilitiesKit/TSAttachment.h new file mode 100644 index 000000000..e4b15002c --- /dev/null +++ b/SignalUtilitiesKit/TSAttachment.h @@ -0,0 +1,103 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSAttachmentPointer; +@class TSMessage; + +typedef NS_ENUM(NSUInteger, TSAttachmentType) { + TSAttachmentTypeDefault = 0, + TSAttachmentTypeVoiceMessage = 1, +}; + +@interface TSAttachment : TSYapDatabaseObject { + +@protected + NSString *_contentType; +} + +// TSAttachment is a base class for TSAttachmentPointer (a yet-to-be-downloaded +// incoming attachment) and TSAttachmentStream (an outgoing or already-downloaded +// incoming attachment). +// +// The attachmentSchemaVersion and serverId properties only apply to +// TSAttachmentPointer, which can be distinguished by the isDownloaded +// property. +@property (atomic, readwrite) UInt64 serverId; +@property (atomic, readwrite, nullable) NSData *encryptionKey; +@property (nonatomic, readonly) NSString *contentType; +@property (atomic, readwrite) BOOL isDownloaded; +@property (nonatomic) TSAttachmentType attachmentType; +@property (nonatomic) NSString *downloadURL; + +// Though now required, may incorrectly be 0 on legacy attachments. +@property (nonatomic, readonly) UInt32 byteCount; + +// Represents the "source" filename sent or received in the protos, +// not the filename on disk. +@property (nonatomic, readonly, nullable) NSString *sourceFilename; + +#pragma mark - Media Album + +@property (nonatomic, readonly, nullable) NSString *caption; +@property (nonatomic, nullable) NSString *albumMessageId; +- (nullable TSMessage *)fetchAlbumMessageWithTransaction:(YapDatabaseReadTransaction *)transaction; + +// `migrateAlbumMessageId` is only used in the migration to the new multi-attachment message scheme, +// and shouldn't be used as a general purpose setter. Instead, `albumMessageId` should be passed as +// an initializer param. +- (void)migrateAlbumMessageId:(NSString *)albumMesssageId; + +#pragma mark - + +// This constructor is used for new instances of TSAttachmentPointer, +// i.e. undownloaded incoming attachments. +- (instancetype)initWithServerId:(UInt64)serverId + encryptionKey:(nullable NSData *)encryptionKey + byteCount:(UInt32)byteCount + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId; + +// This constructor is used for new instances of TSAttachmentPointer, +// i.e. undownloaded restoring attachments. +- (instancetype)initForRestoreWithUniqueId:(NSString *)uniqueId + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId; + +// This constructor is used for new instances of TSAttachmentStream +// that represent new, un-uploaded outgoing attachments. +- (instancetype)initWithContentType:(NSString *)contentType + byteCount:(UInt32)byteCount + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId; + +// This constructor is used for new instances of TSAttachmentStream +// that represent downloaded incoming attachments. +- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder; + +- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion; + +@property (nonatomic, readonly) BOOL isAnimated; +@property (nonatomic, readonly) BOOL isImage; +@property (nonatomic, readonly) BOOL isVideo; +@property (nonatomic, readonly) BOOL isAudio; +@property (nonatomic, readonly) BOOL isVoiceMessage; +@property (nonatomic, readonly) BOOL isVisualMedia; +@property (nonatomic, readonly) BOOL isOversizeText; + ++ (NSString *)emojiForMimeType:(NSString *)contentType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSAttachment.m b/SignalUtilitiesKit/TSAttachment.m new file mode 100644 index 000000000..fcb08527f --- /dev/null +++ b/SignalUtilitiesKit/TSAttachment.m @@ -0,0 +1,302 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSAttachment.h" +#import "MIMETypeUtil.h" +#import "NSString+SSK.h" +#import "TSAttachmentPointer.h" +#import "TSMessage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSUInteger const TSAttachmentSchemaVersion = 4; + +@interface TSAttachment () + +@property (nonatomic, readonly) NSUInteger attachmentSchemaVersion; + +@property (nonatomic, nullable) NSString *sourceFilename; + +@property (nonatomic) NSString *contentType; + +@end + +@implementation TSAttachment + +// This constructor is used for new instances of TSAttachmentPointer, +// i.e. undownloaded incoming attachments. +- (instancetype)initWithServerId:(UInt64)serverId + encryptionKey:(nullable NSData *)encryptionKey + byteCount:(UInt32)byteCount + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId +{ + OWSAssertDebug(serverId > 0); + if (byteCount <= 0) { + // This will fail with legacy iOS clients which don't upload attachment size. + OWSLogWarn(@"Missing byteCount for attachment with serverId: %lld", serverId); + } + if (contentType.length < 1) { + OWSLogWarn(@"incoming attachment has invalid content type"); + + contentType = OWSMimeTypeApplicationOctetStream; + } + OWSAssertDebug(contentType.length > 0); + + self = [super init]; + if (!self) { + return self; + } + + _serverId = serverId; + _encryptionKey = encryptionKey; + _byteCount = byteCount; + _contentType = contentType; + _sourceFilename = sourceFilename; + _caption = caption; + _albumMessageId = albumMessageId; + + _attachmentSchemaVersion = TSAttachmentSchemaVersion; + + return self; +} + +// This constructor is used for new instances of TSAttachmentPointer, +// i.e. undownloaded restoring attachments. +- (instancetype)initForRestoreWithUniqueId:(NSString *)uniqueId + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId +{ + OWSAssertDebug(uniqueId.length > 0); + if (contentType.length < 1) { + OWSLogWarn(@"incoming attachment has invalid content type"); + + contentType = OWSMimeTypeApplicationOctetStream; + } + OWSAssertDebug(contentType.length > 0); + + // If saved, this AttachmentPointer would replace the AttachmentStream in the attachments collection. + // However we only use this AttachmentPointer should only be used during the export process so it + // won't be saved until we restore the backup (when there will be no AttachmentStream to replace). + self = [super initWithUniqueId:uniqueId]; + if (!self) { + return self; + } + + _contentType = contentType; + _sourceFilename = sourceFilename; + _caption = caption; + _albumMessageId = albumMessageId; + + _attachmentSchemaVersion = TSAttachmentSchemaVersion; + + return self; +} + +// This constructor is used for new instances of TSAttachmentStream +// that represent new, un-uploaded outgoing attachments. +- (instancetype)initWithContentType:(NSString *)contentType + byteCount:(UInt32)byteCount + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId +{ + if (contentType.length < 1) { + OWSLogWarn(@"outgoing attachment has invalid content type"); + + contentType = OWSMimeTypeApplicationOctetStream; + } + OWSAssertDebug(contentType.length > 0); + + self = [super init]; + if (!self) { + return self; + } + OWSLogVerbose(@"init attachment with uniqueId: %@", self.uniqueId); + + _contentType = contentType; + _byteCount = byteCount; + _sourceFilename = sourceFilename; + _caption = caption; + _albumMessageId = albumMessageId; + + _attachmentSchemaVersion = TSAttachmentSchemaVersion; + + return self; +} + +// This constructor is used for new instances of TSAttachmentStream +// that represent downloaded incoming attachments. +- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer +{ + if (!pointer.lazyRestoreFragment) { + OWSAssertDebug(pointer.serverId > 0); + if (pointer.byteCount <= 0) { + // This will fail with legacy iOS clients which don't upload attachment size. + OWSLogWarn(@"Missing pointer.byteCount for attachment with serverId: %lld", pointer.serverId); + } + } + OWSAssertDebug(pointer.contentType.length > 0); + + // Once saved, this AttachmentStream will replace the AttachmentPointer in the attachments collection. + self = [super initWithUniqueId:pointer.uniqueId]; + if (!self) { + return self; + } + + _serverId = pointer.serverId; + _encryptionKey = pointer.encryptionKey; + _byteCount = pointer.byteCount; + _sourceFilename = pointer.sourceFilename; + NSString *contentType = pointer.contentType; + if (contentType.length < 1) { + OWSLogWarn(@"incoming attachment has invalid content type"); + + contentType = OWSMimeTypeApplicationOctetStream; + } + _contentType = contentType; + _caption = pointer.caption; + _albumMessageId = pointer.albumMessageId; + + _attachmentSchemaVersion = TSAttachmentSchemaVersion; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (_attachmentSchemaVersion < TSAttachmentSchemaVersion) { + [self upgradeFromAttachmentSchemaVersion:_attachmentSchemaVersion]; + _attachmentSchemaVersion = TSAttachmentSchemaVersion; + } + + if (!_sourceFilename) { + // renamed _filename to _sourceFilename + _sourceFilename = [coder decodeObjectForKey:@"filename"]; + OWSAssertDebug(!_sourceFilename || [_sourceFilename isKindOfClass:[NSString class]]); + } + + if (_contentType.length < 1) { + OWSLogWarn(@"legacy attachment has invalid content type"); + + _contentType = OWSMimeTypeApplicationOctetStream; + } + + return self; +} + +- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion +{ + // This method is overridden by the base classes TSAttachmentPointer and + // TSAttachmentStream. +} + ++ (NSString *)collection { + return @"TSAttachements"; +} + +- (NSString *)description { + NSString *attachmentString = NSLocalizedString(@"ATTACHMENT", nil); + + if ([MIMETypeUtil isAudio:self.contentType]) { + // a missing filename is the legacy way to determine if an audio attachment is + // a voice note vs. other arbitrary audio attachments. + if (self.isVoiceMessage || !self.sourceFilename || self.sourceFilename.length == 0) { + attachmentString = NSLocalizedString(@"ATTACHMENT_TYPE_VOICE_MESSAGE", + @"Short text label for a voice message attachment, used for thread preview and on the lock screen"); + return [NSString stringWithFormat:@"🎤 %@", attachmentString]; + } + } + + return [NSString stringWithFormat:@"%@ %@", [TSAttachment emojiForMimeType:self.contentType], attachmentString]; +} + ++ (NSString *)emojiForMimeType:(NSString *)contentType +{ + if ([MIMETypeUtil isImage:contentType]) { + return @"📷"; + } else if ([MIMETypeUtil isVideo:contentType]) { + return @"🎥"; + } else if ([MIMETypeUtil isAudio:contentType]) { + return @"🎧"; + } else if ([MIMETypeUtil isAnimated:contentType]) { + return @"🎡"; + } else { + return @"📎"; + } +} + +- (BOOL)isImage +{ + return [MIMETypeUtil isImage:self.contentType]; +} + +- (BOOL)isVideo +{ + return [MIMETypeUtil isVideo:self.contentType]; +} + +- (BOOL)isAudio +{ + return [MIMETypeUtil isAudio:self.contentType]; +} + +- (BOOL)isAnimated +{ + return [MIMETypeUtil isAnimated:self.contentType]; +} + +- (BOOL)isVoiceMessage +{ + return self.attachmentType == TSAttachmentTypeVoiceMessage; +} + +- (BOOL)isVisualMedia +{ + return [MIMETypeUtil isVisualMedia:self.contentType]; +} + +- (BOOL)isOversizeText +{ + return [self.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]; +} + +- (nullable NSString *)sourceFilename +{ + return _sourceFilename.filterFilename; +} + +- (NSString *)contentType +{ + return _contentType.filterFilename; +} + +#pragma mark - Relationships + +- (nullable TSMessage *)fetchAlbumMessageWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + if (self.albumMessageId == nil) { + return nil; + } + return [TSMessage fetchObjectWithUniqueID:self.albumMessageId transaction:transaction]; +} + +- (void)migrateAlbumMessageId:(NSString *)albumMesssageId +{ + _albumMessageId = albumMesssageId; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSAttachmentPointer.h b/SignalUtilitiesKit/TSAttachmentPointer.h new file mode 100644 index 000000000..879a01fac --- /dev/null +++ b/SignalUtilitiesKit/TSAttachmentPointer.h @@ -0,0 +1,75 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSAttachment.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSBackupFragment; +@class SSKProtoAttachmentPointer; +@class TSAttachmentStream; +@class TSMessage; + +typedef NS_ENUM(NSUInteger, TSAttachmentPointerType) { + TSAttachmentPointerTypeUnknown = 0, + TSAttachmentPointerTypeIncoming = 1, + TSAttachmentPointerTypeRestoring = 2, +}; + +typedef NS_ENUM(NSUInteger, TSAttachmentPointerState) { + TSAttachmentPointerStateEnqueued = 0, + TSAttachmentPointerStateDownloading = 1, + TSAttachmentPointerStateFailed = 2, +}; + +/** + * A TSAttachmentPointer is a yet-to-be-downloaded attachment. + */ +@interface TSAttachmentPointer : TSAttachment + +@property (nonatomic) TSAttachmentPointerType pointerType; +@property (atomic) TSAttachmentPointerState state; +@property (nullable, atomic) NSString *mostRecentFailureLocalizedText; + +// Though now required, `digest` may be null for pre-existing records or from +// messages received from other clients +@property (nullable, nonatomic, readonly) NSData *digest; + +@property (nonatomic, readonly) CGSize mediaSize; + +// Non-nil for attachments which need "lazy backup restore." +- (nullable OWSBackupFragment *)lazyRestoreFragment; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithServerId:(UInt64)serverId + key:(nullable NSData *)key + digest:(nullable NSData *)digest + byteCount:(UInt32)byteCount + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId + attachmentType:(TSAttachmentType)attachmentType + mediaSize:(CGSize)mediaSize NS_DESIGNATED_INITIALIZER; + +- (instancetype)initForRestoreWithAttachmentStream:(TSAttachmentStream *)attachmentStream NS_DESIGNATED_INITIALIZER; + ++ (nullable TSAttachmentPointer *)attachmentPointerFromProto:(SSKProtoAttachmentPointer *)attachmentProto + albumMessage:(nullable TSMessage *)message; + ++ (NSArray *)attachmentPointersFromProtos: + (NSArray *)attachmentProtos + albumMessage:(TSMessage *)message; + +#pragma mark - Update With... Methods + +// Marks attachment as needing "lazy backup restore." +- (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSAttachmentPointer.m b/SignalUtilitiesKit/TSAttachmentPointer.m new file mode 100644 index 000000000..7cbcb181d --- /dev/null +++ b/SignalUtilitiesKit/TSAttachmentPointer.m @@ -0,0 +1,265 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSAttachmentPointer.h" +#import "OWSBackupFragment.h" +#import "TSAttachmentStream.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TSAttachmentStream (TSAttachmentPointer) + +- (CGSize)cachedMediaSize; + +@end + +#pragma mark - + + +@interface TSAttachmentPointer () + +// Optional property. Only set for attachments which need "lazy backup restore." +@property (nonatomic, nullable) NSString *lazyRestoreFragmentId; + +@end + +#pragma mark - + +@implementation TSAttachmentPointer + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + // A TSAttachmentPointer is a yet-to-be-downloaded attachment. + // If this is an old TSAttachmentPointer from another session, + // we know that it failed to complete before the session completed. + if (![coder containsValueForKey:@"state"]) { + _state = TSAttachmentPointerStateFailed; + } + + if (_pointerType == TSAttachmentPointerTypeUnknown) { + _pointerType = TSAttachmentPointerTypeIncoming; + } + + return self; +} + +- (instancetype)initWithServerId:(UInt64)serverId + key:(nullable NSData *)key + digest:(nullable NSData *)digest + byteCount:(UInt32)byteCount + contentType:(NSString *)contentType + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId + attachmentType:(TSAttachmentType)attachmentType + mediaSize:(CGSize)mediaSize +{ + self = [super initWithServerId:serverId + encryptionKey:key + byteCount:byteCount + contentType:contentType + sourceFilename:sourceFilename + caption:caption + albumMessageId:albumMessageId]; + if (!self) { + return self; + } + + _digest = digest; + _state = TSAttachmentPointerStateEnqueued; + self.attachmentType = attachmentType; + _pointerType = TSAttachmentPointerTypeIncoming; + _mediaSize = mediaSize; + + return self; +} + +- (instancetype)initForRestoreWithAttachmentStream:(TSAttachmentStream *)attachmentStream +{ + OWSAssertDebug(attachmentStream); + + self = [super initForRestoreWithUniqueId:attachmentStream.uniqueId + contentType:attachmentStream.contentType + sourceFilename:attachmentStream.sourceFilename + caption:attachmentStream.caption + albumMessageId:attachmentStream.albumMessageId]; + if (!self) { + return self; + } + + _state = TSAttachmentPointerStateEnqueued; + self.attachmentType = attachmentStream.attachmentType; + _pointerType = TSAttachmentPointerTypeRestoring; + _mediaSize = (attachmentStream.shouldHaveImageSize ? attachmentStream.cachedMediaSize : CGSizeZero); + + return self; +} + ++ (nullable TSAttachmentPointer *)attachmentPointerFromProto:(SSKProtoAttachmentPointer *)attachmentProto + albumMessage:(nullable TSMessage *)albumMessage +{ + if (attachmentProto.id < 1) { + OWSFailDebug(@"Invalid attachment id."); + return nil; + } + /* + if (attachmentProto.key.length < 1) { + OWSFailDebug(@"Invalid attachment key."); + return nil; + } + */ + NSString *_Nullable fileName = attachmentProto.fileName; + NSString *_Nullable contentType = attachmentProto.contentType; + if (contentType.length < 1) { + // Content type might not set if the sending client can't + // infer a MIME type from the file extension. + OWSLogWarn(@"Invalid attachment content type."); + NSString *_Nullable fileExtension = [fileName pathExtension].lowercaseString; + if (fileExtension.length > 0) { + contentType = [MIMETypeUtil mimeTypeForFileExtension:fileExtension]; + } + if (contentType.length < 1) { + contentType = OWSMimeTypeApplicationOctetStream; + } + } + + // digest will be empty for old clients. + NSData *_Nullable digest = attachmentProto.hasDigest ? attachmentProto.digest : nil; + + TSAttachmentType attachmentType = TSAttachmentTypeDefault; + if ([attachmentProto hasFlags]) { + UInt32 flags = attachmentProto.flags; + if ((flags & (UInt32)SSKProtoAttachmentPointerFlagsVoiceMessage) > 0) { + attachmentType = TSAttachmentTypeVoiceMessage; + } + } + NSString *_Nullable caption; + if (attachmentProto.hasCaption) { + caption = attachmentProto.caption; + } + + NSString *_Nullable albumMessageId; + if (albumMessage != nil) { + albumMessageId = albumMessage.uniqueId; + } + + CGSize mediaSize = CGSizeZero; + if (attachmentProto.hasWidth && attachmentProto.hasHeight && attachmentProto.width > 0 + && attachmentProto.height > 0) { + mediaSize = CGSizeMake(attachmentProto.width, attachmentProto.height); + } + + TSAttachmentPointer *pointer = [[TSAttachmentPointer alloc] initWithServerId:attachmentProto.id + key:attachmentProto.key + digest:digest + byteCount:attachmentProto.size + contentType:contentType + sourceFilename:fileName + caption:caption + albumMessageId:albumMessageId + attachmentType:attachmentType + mediaSize:mediaSize]; + + pointer.downloadURL = attachmentProto.url; // Loki + + return pointer; +} + ++ (NSArray *)attachmentPointersFromProtos: + (NSArray *)attachmentProtos + albumMessage:(TSMessage *)albumMessage +{ + OWSAssertDebug(attachmentProtos); + OWSAssertDebug(albumMessage); + + NSMutableArray *attachmentPointers = [NSMutableArray new]; + for (SSKProtoAttachmentPointer *attachmentProto in attachmentProtos) { + TSAttachmentPointer *_Nullable attachmentPointer = + [self attachmentPointerFromProto:attachmentProto albumMessage:albumMessage]; + if (attachmentPointer) { + [attachmentPointers addObject:attachmentPointer]; + } + } + return [attachmentPointers copy]; +} + +- (BOOL)isDecimalNumberText:(NSString *)text +{ + return [text componentsSeparatedByCharactersInSet:[NSCharacterSet decimalDigitCharacterSet]].count == 1; +} + +- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion +{ + // Legacy instances of TSAttachmentPointer apparently used the serverId as their + // uniqueId. + if (attachmentSchemaVersion < 2 && self.serverId == 0) { + OWSAssertDebug([self isDecimalNumberText:self.uniqueId]); + if ([self isDecimalNumberText:self.uniqueId]) { + // For legacy instances, try to parse the serverId from the uniqueId. + self.serverId = [self.uniqueId integerValue]; + } else { + OWSLogError(@"invalid legacy attachment uniqueId: %@.", self.uniqueId); + } + } +} + +- (nullable OWSBackupFragment *)lazyRestoreFragment +{ + if (!self.lazyRestoreFragmentId) { + return nil; + } + OWSBackupFragment *_Nullable backupFragment = + [OWSBackupFragment fetchObjectWithUniqueID:self.lazyRestoreFragmentId]; + OWSAssertDebug(backupFragment); + return backupFragment; +} + +#pragma mark - Update With... Methods + +- (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(lazyRestoreFragment); + OWSAssertDebug(transaction); + + if (!lazyRestoreFragment.uniqueId) { + // If metadata hasn't been saved yet, save now. + [lazyRestoreFragment saveWithTransaction:transaction]; + + OWSAssertDebug(lazyRestoreFragment.uniqueId); + } + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSAttachmentPointer *attachment) { + [attachment setLazyRestoreFragmentId:lazyRestoreFragment.uniqueId]; + }]; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ +#ifdef DEBUG + if (self.uniqueId.length > 0) { + id _Nullable oldObject = [transaction objectForKey:self.uniqueId inCollection:TSAttachment.collection]; + if ([oldObject isKindOfClass:[TSAttachmentStream class]]) { + OWSFailDebug(@"We should never overwrite a TSAttachmentStream with a TSAttachmentPointer."); + } + } else { + OWSFailDebug(@"Missing uniqueId."); + } +#endif + + [super saveWithTransaction:transaction]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSAttachmentStream.h b/SignalUtilitiesKit/TSAttachmentStream.h new file mode 100644 index 000000000..3c071d152 --- /dev/null +++ b/SignalUtilitiesKit/TSAttachmentStream.h @@ -0,0 +1,108 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "DataSource.h" +#import "TSAttachment.h" + +#if TARGET_OS_IPHONE +#import + +#endif + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoAttachmentPointer; +@class TSAttachmentPointer; +@class YapDatabaseReadWriteTransaction; + +typedef void (^OWSThumbnailSuccess)(UIImage *image); +typedef void (^OWSThumbnailFailure)(void); + +@interface TSAttachmentStream : TSAttachment + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithContentType:(NSString *)contentType + byteCount:(UInt32)byteCount + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +// Though now required, `digest` may be null for pre-existing records or from +// messages received from other clients +@property (nullable, nonatomic) NSData *digest; + +// This only applies for attachments being uploaded. +@property (atomic) BOOL isUploaded; + +@property (nonatomic, readonly) NSDate *creationTimestamp; + +#if TARGET_OS_IPHONE +- (nullable NSData *)validStillImageData; +#endif + +@property (nonatomic, readonly, nullable) UIImage *originalImage; +@property (nonatomic, readonly, nullable) NSString *originalFilePath; +@property (nonatomic, readonly, nullable) NSURL *originalMediaURL; + +- (NSArray *)allThumbnailPaths; + ++ (BOOL)hasThumbnailForMimeType:(NSString *)contentType; + +- (nullable NSData *)readDataFromFileWithError:(NSError **)error; +- (BOOL)writeData:(NSData *)data error:(NSError **)error; +- (BOOL)writeDataSource:(DataSource *)dataSource; + ++ (void)deleteAttachments; + ++ (NSString *)attachmentsFolder; ++ (NSString *)legacyAttachmentsDirPath; ++ (NSString *)sharedDataAttachmentsDirPath; + +- (BOOL)shouldHaveImageSize; +- (CGSize)imageSize; + +- (CGFloat)audioDurationSeconds; + ++ (nullable NSError *)migrateToSharedData; + +#pragma mark - Thumbnails + +// On cache hit, the thumbnail will be returned synchronously and completion will never be invoked. +// On cache miss, nil will be returned and success will be invoked if thumbnail can be generated; +// otherwise failure will be invoked. +// +// success and failure are invoked async on main. +- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint + success:(OWSThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageSmallWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageMediumWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageLargeWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure; +- (nullable UIImage *)thumbnailImageSmallSync; + +// This method should only be invoked by OWSThumbnailService. +- (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints; + +#pragma mark - Validation + +@property (nonatomic, readonly) BOOL isValidImage; +@property (nonatomic, readonly) BOOL isValidVideo; +@property (nonatomic, readonly) BOOL isValidVisualMedia; + +#pragma mark - Update With... Methods + +- (nullable TSAttachmentStream *)cloneAsThumbnail; + +#pragma mark - Protobuf + ++ (nullable SSKProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId; + +- (nullable SSKProtoAttachmentPointer *)buildProto; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSAttachmentStream.m b/SignalUtilitiesKit/TSAttachmentStream.m new file mode 100644 index 000000000..f3c797b74 --- /dev/null +++ b/SignalUtilitiesKit/TSAttachmentStream.m @@ -0,0 +1,887 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSAttachmentStream.h" +#import "MIMETypeUtil.h" +#import "NSData+Image.h" +#import "OWSFileSystem.h" +#import "TSAttachmentPointer.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +const NSUInteger kThumbnailDimensionPointsSmall = 200; +const NSUInteger kThumbnailDimensionPointsMedium = 450; +// This size is large enough to render full screen. +const NSUInteger ThumbnailDimensionPointsLarge() +{ + CGSize screenSizePoints = UIScreen.mainScreen.bounds.size; + const CGFloat kMinZoomFactor = 2.f; + return MAX(screenSizePoints.width, screenSizePoints.height) * kMinZoomFactor; +} + +typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); + +@interface TSAttachmentStream () + +// We only want to generate the file path for this attachment once, so that +// changes in the file path generation logic don't break existing attachments. +@property (nullable, nonatomic) NSString *localRelativeFilePath; + +// These properties should only be accessed while synchronized on self. +@property (nullable, nonatomic) NSNumber *cachedImageWidth; +@property (nullable, nonatomic) NSNumber *cachedImageHeight; + +// This property should only be accessed on the main thread. +@property (nullable, nonatomic) NSNumber *cachedAudioDurationSeconds; + +@property (atomic, nullable) NSNumber *isValidImageCached; +@property (atomic, nullable) NSNumber *isValidVideoCached; + +@end + +#pragma mark - + +@implementation TSAttachmentStream + +- (instancetype)initWithContentType:(NSString *)contentType + byteCount:(UInt32)byteCount + sourceFilename:(nullable NSString *)sourceFilename + caption:(nullable NSString *)caption + albumMessageId:(nullable NSString *)albumMessageId +{ + self = [super initWithContentType:contentType + byteCount:byteCount + sourceFilename:sourceFilename + caption:caption + albumMessageId:albumMessageId]; + if (!self) { + return self; + } + + self.isDownloaded = YES; + // TSAttachmentStream doesn't have any "incoming vs. outgoing" + // state, but this constructor is used only for new outgoing + // attachments which haven't been uploaded yet. + _isUploaded = NO; + _creationTimestamp = [NSDate new]; + + [self ensureFilePath]; + + return self; +} + +- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer +{ + // Once saved, this AttachmentStream will replace the AttachmentPointer in the attachments collection. + self = [super initWithPointer:pointer]; + if (!self) { + return self; + } + + _contentType = pointer.contentType; + self.isDownloaded = YES; + // TSAttachmentStream doesn't have any "incoming vs. outgoing" + // state, but this constructor is used only for new incoming + // attachments which don't need to be uploaded. + _isUploaded = YES; + self.attachmentType = pointer.attachmentType; + _creationTimestamp = [NSDate new]; + + [self ensureFilePath]; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + // OWS105AttachmentFilePaths will ensure the file path is saved if necessary. + [self ensureFilePath]; + + // OWS105AttachmentFilePaths will ensure the creation timestamp is saved if necessary. + if (!_creationTimestamp) { + _creationTimestamp = [NSDate new]; + } + + return self; +} + +- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion +{ + [super upgradeFromAttachmentSchemaVersion:attachmentSchemaVersion]; + + if (attachmentSchemaVersion < 3) { + // We want to treat any legacy TSAttachmentStream as though + // they have already been uploaded. If it needs to be reuploaded, + // the OWSUploadingService will update this progress when the + // upload begins. + self.isUploaded = YES; + } + + if (attachmentSchemaVersion < 4) { + // Legacy image sizes don't correctly reflect image orientation. + @synchronized(self) + { + self.cachedImageWidth = nil; + self.cachedImageHeight = nil; + } + } +} + +- (void)ensureFilePath +{ + if (self.localRelativeFilePath) { + return; + } + + NSString *attachmentsFolder = [[self class] attachmentsFolder]; + NSString *filePath = [MIMETypeUtil filePathForAttachment:self.uniqueId + ofMIMEType:self.contentType + sourceFilename:self.sourceFilename + inFolder:attachmentsFolder]; + if (!filePath) { + OWSFailDebug(@"Could not generate path for attachment."); + return; + } + if (![filePath hasPrefix:attachmentsFolder]) { + OWSFailDebug(@"Attachment paths should all be in the attachments folder."); + return; + } + NSString *localRelativeFilePath = [filePath substringFromIndex:attachmentsFolder.length]; + if (localRelativeFilePath.length < 1) { + OWSFailDebug(@"Empty local relative attachment paths."); + return; + } + + self.localRelativeFilePath = localRelativeFilePath; + OWSAssertDebug(self.originalFilePath); +} + +#pragma mark - File Management + +- (nullable NSData *)readDataFromFileWithError:(NSError **)error +{ + *error = nil; + NSString *_Nullable filePath = self.originalFilePath; + if (!filePath) { + OWSFailDebug(@"Missing path for attachment."); + return nil; + } + return [NSData dataWithContentsOfFile:filePath options:0 error:error]; +} + +- (BOOL)writeData:(NSData *)data error:(NSError **)error +{ + OWSAssertDebug(data); + + *error = nil; + NSString *_Nullable filePath = self.originalFilePath; + if (!filePath) { + OWSFailDebug(@"Missing path for attachment."); + return NO; + } + OWSLogDebug(@"Writing attachment to file: %@", filePath); + return [data writeToFile:filePath options:0 error:error]; +} + +- (BOOL)writeDataSource:(DataSource *)dataSource +{ + OWSAssertDebug(dataSource); + + NSString *_Nullable filePath = self.originalFilePath; + if (!filePath) { + OWSFailDebug(@"Missing path for attachment."); + return NO; + } + OWSLogDebug(@"Writing attachment to file: %@", filePath); + return [dataSource writeToPath:filePath]; +} + ++ (NSString *)legacyAttachmentsDirPath +{ + return [[OWSFileSystem appDocumentDirectoryPath] stringByAppendingPathComponent:@"Attachments"]; +} + ++ (NSString *)sharedDataAttachmentsDirPath +{ + return [[OWSFileSystem appSharedDataDirectoryPath] stringByAppendingPathComponent:@"Attachments"]; +} + ++ (nullable NSError *)migrateToSharedData +{ + OWSLogInfo(@""); + + return [OWSFileSystem moveAppFilePath:self.legacyAttachmentsDirPath + sharedDataFilePath:self.sharedDataAttachmentsDirPath]; +} + ++ (NSString *)attachmentsFolder +{ + static NSString *attachmentsFolder = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + attachmentsFolder = TSAttachmentStream.sharedDataAttachmentsDirPath; + + [OWSFileSystem ensureDirectoryExists:attachmentsFolder]; + }); + return attachmentsFolder; +} + +- (nullable NSString *)originalFilePath +{ + if (!self.localRelativeFilePath) { + OWSFailDebug(@"Attachment missing local file path."); + return nil; + } + + return [[[self class] attachmentsFolder] stringByAppendingPathComponent:self.localRelativeFilePath]; +} + +- (nullable NSString *)legacyThumbnailPath +{ + NSString *filePath = self.originalFilePath; + if (!filePath) { + OWSFailDebug(@"Attachment missing local file path."); + return nil; + } + + if (!self.isImage && !self.isVideo && !self.isAnimated) { + return nil; + } + + NSString *filename = filePath.lastPathComponent.stringByDeletingPathExtension; + NSString *containingDir = filePath.stringByDeletingLastPathComponent; + NSString *newFilename = [filename stringByAppendingString:@"-signal-ios-thumbnail"]; + + return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"]; +} + +- (NSString *)thumbnailsDirPath +{ + if (!self.localRelativeFilePath) { + OWSFailDebug(@"Attachment missing local file path."); + return nil; + } + + // Thumbnails are written to the caches directory, so that iOS can + // remove them if necessary. + NSString *dirName = [NSString stringWithFormat:@"%@-thumbnails", self.uniqueId]; + return [OWSFileSystem.cachesDirectoryPath stringByAppendingPathComponent:dirName]; +} + +- (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints +{ + NSString *filename = [NSString stringWithFormat:@"thumbnail-%lu.jpg", (unsigned long)thumbnailDimensionPoints]; + return [self.thumbnailsDirPath stringByAppendingPathComponent:filename]; +} + +- (nullable NSURL *)originalMediaURL +{ + NSString *_Nullable filePath = self.originalFilePath; + if (!filePath) { + OWSFailDebug(@"Missing path for attachment."); + return nil; + } + return [NSURL fileURLWithPath:filePath]; +} + +- (void)removeFileWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + NSError *error; + + NSString *thumbnailsDirPath = self.thumbnailsDirPath; + if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) { + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailsDirPath error:&error]; + if (error || !success) { + OWSLogError(@"remove thumbnails dir failed with: %@", error); + } + } + + NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath; + if (legacyThumbnailPath) { + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyThumbnailPath error:&error]; + + if (error || !success) { + OWSLogError(@"remove legacy thumbnail failed with: %@", error); + } + } + + NSString *_Nullable filePath = self.originalFilePath; + if (!filePath) { + OWSFailDebug(@"Missing path for attachment."); + return; + } + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + if (error || !success) { + OWSLogError(@"remove file failed with: %@", error); + } +} + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [super removeWithTransaction:transaction]; + [self removeFileWithTransaction:transaction]; +} + +- (BOOL)isValidVisualMedia +{ + if (self.isImage && self.isValidImage) { + return YES; + } + + if (self.isVideo && self.isValidVideo) { + return YES; + } + + if (self.isAnimated && self.isValidImage) { + return YES; + } + + return NO; +} + +#pragma mark - Image Validation + +- (BOOL)isValidImage +{ + OWSAssertDebug(self.isImage || self.isAnimated); + + BOOL result; + BOOL didUpdateCache = NO; + @synchronized(self) { + if (!self.isValidImageCached) { + OWSLogVerbose(@"Updating isValidImageCached."); + self.isValidImageCached = @([NSData ows_isValidImageAtPath:self.originalFilePath + mimeType:self.contentType]); + didUpdateCache = YES; + } + result = self.isValidImageCached.boolValue; + } + + if (didUpdateCache) { + [self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) { + latestInstance.isValidImageCached = @(result); + }]; + } + + return result; +} + +- (BOOL)isValidVideo +{ + OWSAssertDebug(self.isVideo); + + BOOL result; + BOOL didUpdateCache = NO; + @synchronized(self) { + if (!self.isValidVideoCached) { + OWSLogVerbose(@"Updating isValidVideoCached."); + self.isValidVideoCached = @([OWSMediaUtils isValidVideoWithPath:self.originalFilePath]); + didUpdateCache = YES; + } + result = self.isValidVideoCached.boolValue; + } + + if (didUpdateCache) { + [self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) { + latestInstance.isValidVideoCached = @(result); + }]; + } + + return result; +} + +#pragma mark - + +- (nullable UIImage *)originalImage +{ + if ([self isVideo]) { + return [self videoStillImage]; + } else if ([self isImage] || [self isAnimated]) { + NSURL *_Nullable mediaUrl = self.originalMediaURL; + if (!mediaUrl) { + return nil; + } + if (![self isValidImage]) { + return nil; + } + return [[UIImage alloc] initWithContentsOfFile:self.originalFilePath]; + } else { + return nil; + } +} + +- (nullable NSData *)validStillImageData +{ + if ([self isVideo]) { + OWSFailDebug(@"isVideo was unexpectedly true"); + return nil; + } + if ([self isAnimated]) { + OWSFailDebug(@"isAnimated was unexpectedly true"); + return nil; + } + + if (![NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]) { + OWSFailDebug(@"skipping invalid image"); + return nil; + } + + return [NSData dataWithContentsOfFile:self.originalFilePath]; +} + ++ (BOOL)hasThumbnailForMimeType:(NSString *)contentType +{ + return ([MIMETypeUtil isVideo:contentType] || [MIMETypeUtil isImage:contentType] || + [MIMETypeUtil isAnimated:contentType]); +} + +- (nullable UIImage *)videoStillImage +{ + NSError *error; + UIImage *_Nullable image = [OWSMediaUtils thumbnailForVideoAtPath:self.originalFilePath + maxDimension:ThumbnailDimensionPointsLarge() + error:&error]; + if (error || !image) { + OWSLogError(@"Could not create video still: %@.", error); + return nil; + } + return image; +} + ++ (void)deleteAttachments +{ + NSError *error; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSURL *fileURL = [NSURL fileURLWithPath:self.attachmentsFolder]; + NSArray *contents = + [fileManager contentsOfDirectoryAtURL:fileURL includingPropertiesForKeys:nil options:0 error:&error]; + + if (error) { + OWSFailDebug(@"failed to get contents of attachments folder: %@ with error: %@", self.attachmentsFolder, error); + return; + } + + for (NSURL *url in contents) { + [fileManager removeItemAtURL:url error:&error]; + if (error) { + OWSFailDebug(@"failed to remove item at path: %@ with error: %@", url, error); + } + } +} + +- (CGSize)calculateImageSize +{ + if ([self isVideo]) { + if (![self isValidVideo]) { + return CGSizeZero; + } + return [self videoStillImage].size; + } else if ([self isImage] || [self isAnimated]) { + // imageSizeForFilePath checks validity. + return [NSData imageSizeForFilePath:self.originalFilePath mimeType:self.contentType]; + } else { + return CGSizeZero; + } +} + +- (BOOL)shouldHaveImageSize +{ + return ([self isVideo] || [self isImage] || [self isAnimated]); +} + +- (CGSize)imageSize +{ + // Avoid crash in dev mode + // OWSAssertDebug(self.shouldHaveImageSize); + + @synchronized(self) + { + if (self.cachedImageWidth && self.cachedImageHeight) { + return CGSizeMake(self.cachedImageWidth.floatValue, self.cachedImageHeight.floatValue); + } + + CGSize imageSize = [self calculateImageSize]; + if (imageSize.width <= 0 || imageSize.height <= 0) { + return CGSizeZero; + } + self.cachedImageWidth = @(imageSize.width); + self.cachedImageHeight = @(imageSize.height); + + [self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) { + latestInstance.cachedImageWidth = @(imageSize.width); + latestInstance.cachedImageHeight = @(imageSize.height); + }]; + + return imageSize; + } +} + +- (CGSize)cachedMediaSize +{ + OWSAssertDebug(self.shouldHaveImageSize); + + @synchronized(self) { + if (self.cachedImageWidth && self.cachedImageHeight) { + return CGSizeMake(self.cachedImageWidth.floatValue, self.cachedImageHeight.floatValue); + } else { + return CGSizeZero; + } + } +} + +#pragma mark - Update With... + +- (void)applyChangeAsyncToLatestCopyWithChangeBlock:(void (^)(TSAttachmentStream *))changeBlock +{ + OWSAssertDebug(changeBlock); + + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + NSString *collection = [TSAttachmentStream collection]; + TSAttachmentStream *latestInstance = [transaction objectForKey:self.uniqueId inCollection:collection]; + if (!latestInstance) { + // This attachment has either not yet been saved or has been deleted; do nothing. + // This isn't an error per se, but these race conditions should be + // _very_ rare. + // + // An exception is incoming group avatar updates which we don't ever save. + OWSLogWarn(@"Attachment not yet saved."); + } else if (![latestInstance isKindOfClass:[TSAttachmentStream class]]) { + OWSFailDebug(@"Attachment has unexpected type: %@", latestInstance.class); + } else { + changeBlock(latestInstance); + + [latestInstance saveWithTransaction:transaction]; + } + }]; +} + +#pragma mark - + +- (CGFloat)calculateAudioDurationSeconds +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug([self isAudio]); + + NSError *error; + AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.originalMediaURL error:&error]; + if (error && [error.domain isEqualToString:NSOSStatusErrorDomain] + && (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) { + // Ignore "invalid audio file" errors. + return 0.f; + } + if (!error) { + return (CGFloat)[audioPlayer duration]; + } else { + OWSLogError(@"Could not find audio duration: %@", self.originalMediaURL); + return 0; + } +} + +- (CGFloat)audioDurationSeconds +{ + OWSAssertIsOnMainThread(); + + if (self.cachedAudioDurationSeconds) { + return self.cachedAudioDurationSeconds.floatValue; + } + + CGFloat audioDurationSeconds = [self calculateAudioDurationSeconds]; + self.cachedAudioDurationSeconds = @(audioDurationSeconds); + + [self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) { + latestInstance.cachedAudioDurationSeconds = @(audioDurationSeconds); + }]; + + return audioDurationSeconds; +} + +#pragma mark - Thumbnails + +- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint + success:(OWSThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure +{ + CGFloat maxDimensionHint = MAX(sizeHint.width, sizeHint.height); + NSUInteger thumbnailDimensionPoints; + if (maxDimensionHint <= kThumbnailDimensionPointsSmall) { + thumbnailDimensionPoints = kThumbnailDimensionPointsSmall; + } else if (maxDimensionHint <= kThumbnailDimensionPointsMedium) { + thumbnailDimensionPoints = kThumbnailDimensionPointsMedium; + } else { + thumbnailDimensionPoints = ThumbnailDimensionPointsLarge(); + } + + return [self thumbnailImageWithThumbnailDimensionPoints:thumbnailDimensionPoints success:success failure:failure]; +} + +- (nullable UIImage *)thumbnailImageSmallWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure +{ + return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall + success:success + failure:failure]; +} + +- (nullable UIImage *)thumbnailImageMediumWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure +{ + return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsMedium + success:success + failure:failure]; +} + +- (nullable UIImage *)thumbnailImageLargeWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure +{ + return [self thumbnailImageWithThumbnailDimensionPoints:ThumbnailDimensionPointsLarge() + success:success + failure:failure]; +} + +- (nullable UIImage *)thumbnailImageWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints + success:(OWSThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure +{ + OWSLoadedThumbnail *_Nullable loadedThumbnail; + loadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:thumbnailDimensionPoints + success:^(OWSLoadedThumbnail *thumbnail) { + DispatchMainThreadSafe(^{ + success(thumbnail.image); + }); + } + failure:^{ + DispatchMainThreadSafe(^{ + failure(); + }); + }]; + return loadedThumbnail.image; +} + +- (nullable OWSLoadedThumbnail *)loadedThumbnailWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints + success:(OWSLoadedThumbnailSuccess)success + failure:(OWSThumbnailFailure)failure +{ + CGSize originalSize = self.imageSize; + if (originalSize.width < 1 || originalSize.height < 1) { + // Any time we return nil from this method we have to call the failure handler + // or else the caller waits for an async thumbnail + failure(); + return nil; + } + if (originalSize.width <= thumbnailDimensionPoints || originalSize.height <= thumbnailDimensionPoints) { + // There's no point in generating a thumbnail if the original is smaller than the + // thumbnail size. + return [[OWSLoadedThumbnail alloc] initWithImage:self.originalImage filePath:self.originalFilePath]; + } + + NSString *thumbnailPath = [self pathForThumbnailDimensionPoints:thumbnailDimensionPoints]; + if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) { + UIImage *_Nullable image = [UIImage imageWithContentsOfFile:thumbnailPath]; + if (!image) { + OWSFailDebug(@"couldn't load image."); + // Any time we return nil from this method we have to call the failure handler + // or else the caller waits for an async thumbnail + failure(); + return nil; + } + return [[OWSLoadedThumbnail alloc] initWithImage:image filePath:thumbnailPath]; + } + + [OWSThumbnailService.shared ensureThumbnailForAttachment:self + thumbnailDimensionPoints:thumbnailDimensionPoints + success:success + failure:^(NSError *error) { + OWSLogError(@"Failed to create thumbnail: %@", error); + failure(); + }]; + return nil; +} + +- (nullable OWSLoadedThumbnail *)loadedThumbnailSmallSync +{ + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + __block OWSLoadedThumbnail *_Nullable asyncLoadedThumbnail = nil; + OWSLoadedThumbnail *_Nullable syncLoadedThumbnail = nil; + syncLoadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall + success:^(OWSLoadedThumbnail *thumbnail) { + @synchronized(self) { + asyncLoadedThumbnail = thumbnail; + } + dispatch_semaphore_signal(semaphore); + } + failure:^{ + dispatch_semaphore_signal(semaphore); + }]; + + if (syncLoadedThumbnail) { + return syncLoadedThumbnail; + } + + // Wait up to N seconds. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC))); + @synchronized(self) { + return asyncLoadedThumbnail; + } +} + +- (nullable UIImage *)thumbnailImageSmallSync +{ + OWSLoadedThumbnail *_Nullable loadedThumbnail = [self loadedThumbnailSmallSync]; + if (!loadedThumbnail) { + OWSLogInfo(@"Couldn't load small thumbnail sync."); + return nil; + } + return loadedThumbnail.image; +} + +- (nullable NSData *)thumbnailDataSmallSync +{ + OWSLoadedThumbnail *_Nullable loadedThumbnail = [self loadedThumbnailSmallSync]; + if (!loadedThumbnail) { + OWSLogInfo(@"Couldn't load small thumbnail sync."); + return nil; + } + NSError *error; + NSData *_Nullable data = [loadedThumbnail dataAndReturnError:&error]; + if (error || !data) { + OWSFailDebug(@"Couldn't load thumbnail data: %@", error); + return nil; + } + return data; +} + +- (NSArray *)allThumbnailPaths +{ + NSMutableArray *result = [NSMutableArray new]; + + NSString *thumbnailsDirPath = self.thumbnailsDirPath; + if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) { + NSError *error; + NSArray *_Nullable fileNames = + [[NSFileManager defaultManager] contentsOfDirectoryAtPath:thumbnailsDirPath error:&error]; + if (error || !fileNames) { + OWSFailDebug(@"contentsOfDirectoryAtPath failed with error: %@", error); + } else { + for (NSString *fileName in fileNames) { + NSString *filePath = [thumbnailsDirPath stringByAppendingPathComponent:fileName]; + [result addObject:filePath]; + } + } + } + + NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath; + if (legacyThumbnailPath && [[NSFileManager defaultManager] fileExistsAtPath:legacyThumbnailPath]) { + [result addObject:legacyThumbnailPath]; + } + + return result; +} + +#pragma mark - Update With... Methods + +- (nullable TSAttachmentStream *)cloneAsThumbnail +{ + if (!self.isValidVisualMedia) { + return nil; + } + + NSData *_Nullable thumbnailData = self.thumbnailDataSmallSync; + // Only some media types have thumbnails + if (!thumbnailData) { + return nil; + } + + // Copy the thumbnail to a new attachment. + NSString *thumbnailName = [NSString stringWithFormat:@"quoted-thumbnail-%@", self.sourceFilename]; + TSAttachmentStream *thumbnailAttachment = + [[TSAttachmentStream alloc] initWithContentType:OWSMimeTypeImageJpeg + byteCount:(uint32_t)thumbnailData.length + sourceFilename:thumbnailName + caption:nil + albumMessageId:nil]; + + NSError *error; + BOOL success = [thumbnailAttachment writeData:thumbnailData error:&error]; + if (!success || error) { + OWSLogError(@"Couldn't copy attachment data for message sent to self: %@.", error); + return nil; + } + + return thumbnailAttachment; +} + +// MARK: Protobuf serialization + ++ (nullable SSKProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId +{ + OWSAssertDebug(attachmentId.length > 0); + + // TODO we should past in a transaction, rather than sneakily generate one in `fetch...` to make sure we're + // getting a consistent view in the message sending process. A brief glance shows it touches quite a bit of code, + // but should be straight forward. + TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId]; + if (![attachment isKindOfClass:[TSAttachmentStream class]]) { + OWSLogError(@"Unexpected type for attachment builder: %@", attachment); + return nil; + } + + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + return [attachmentStream buildProto]; +} + + +- (nullable SSKProtoAttachmentPointer *)buildProto +{ + SSKProtoAttachmentPointerBuilder *builder = [SSKProtoAttachmentPointer builderWithId:self.serverId]; + + OWSAssertDebug(self.contentType.length > 0); + builder.contentType = self.contentType; + + OWSLogVerbose(@"Sending attachment with filename: '%@'", self.sourceFilename); + if (self.sourceFilename.length > 0) { + builder.fileName = self.sourceFilename; + } + if (self.caption.length > 0) { + builder.caption = self.caption; + } + + builder.size = self.byteCount; + builder.key = self.encryptionKey; + builder.digest = self.digest; + builder.flags = self.isVoiceMessage ? SSKProtoAttachmentPointerFlagsVoiceMessage : 0; + + if (self.shouldHaveImageSize) { + CGSize imageSize = self.imageSize; + if (imageSize.width < NSIntegerMax && imageSize.height < NSIntegerMax) { + NSInteger imageWidth = (NSInteger)round(imageSize.width); + NSInteger imageHeight = (NSInteger)round(imageSize.height); + if (imageWidth > 0 && imageHeight > 0) { + builder.width = (UInt32)imageWidth; + builder.height = (UInt32)imageHeight; + } + } + } + + builder.url = self.downloadURL; + + NSError *error; + SSKProtoAttachmentPointer *_Nullable attachmentProto = [builder buildAndReturnError:&error]; + if (error || !attachmentProto) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + return attachmentProto; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSCall.h b/SignalUtilitiesKit/TSCall.h new file mode 100644 index 000000000..b7c26b366 --- /dev/null +++ b/SignalUtilitiesKit/TSCall.h @@ -0,0 +1,44 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSReadTracking.h" +#import "TSInteraction.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSContactThread; + +typedef enum { + RPRecentCallTypeIncoming = 1, + RPRecentCallTypeOutgoing, + RPRecentCallTypeIncomingMissed, + // These call types are used until the call connects. + RPRecentCallTypeOutgoingIncomplete, + RPRecentCallTypeIncomingIncomplete, + RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity, + RPRecentCallTypeIncomingDeclined, + RPRecentCallTypeOutgoingMissed, +} RPRecentCallType; + +NSString *NSStringFromCallType(RPRecentCallType callType); + +@interface TSCall : TSInteraction + +@property (nonatomic, readonly) RPRecentCallType callType; + +- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + withCallNumber:(NSString *)contactNumber + callType:(RPRecentCallType)callType + inThread:(TSContactThread *)thread NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (void)updateCallType:(RPRecentCallType)callType; +- (void)updateCallType:(RPRecentCallType)callType transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSCall.m b/SignalUtilitiesKit/TSCall.m new file mode 100644 index 000000000..c2ccf0590 --- /dev/null +++ b/SignalUtilitiesKit/TSCall.m @@ -0,0 +1,178 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSCall.h" +#import "TSContactThread.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *NSStringFromCallType(RPRecentCallType callType) +{ + switch (callType) { + case RPRecentCallTypeIncoming: + return @"RPRecentCallTypeIncoming"; + case RPRecentCallTypeOutgoing: + return @"RPRecentCallTypeOutgoing"; + case RPRecentCallTypeIncomingMissed: + return @"RPRecentCallTypeIncomingMissed"; + case RPRecentCallTypeOutgoingIncomplete: + return @"RPRecentCallTypeOutgoingIncomplete"; + case RPRecentCallTypeIncomingIncomplete: + return @"RPRecentCallTypeIncomingIncomplete"; + case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity: + return @"RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity"; + case RPRecentCallTypeIncomingDeclined: + return @"RPRecentCallTypeIncomingDeclined"; + case RPRecentCallTypeOutgoingMissed: + return @"RPRecentCallTypeOutgoingMissed"; + } +} + +NSUInteger TSCallCurrentSchemaVersion = 1; + +@interface TSCall () + +@property (nonatomic, getter=wasRead) BOOL read; + +@property (nonatomic, readonly) NSUInteger callSchemaVersion; + +@end + +#pragma mark - + +@implementation TSCall + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + withCallNumber:(NSString *)contactNumber + callType:(RPRecentCallType)callType + inThread:(TSContactThread *)thread +{ + self = [super initInteractionWithTimestamp:timestamp inThread:thread]; + + if (!self) { + return self; + } + + _callSchemaVersion = TSCallCurrentSchemaVersion; + _callType = callType; + + // Ensure users are notified of missed calls. + BOOL isIncomingMissed = (_callType == RPRecentCallTypeIncomingMissed + || _callType == RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity); + if (isIncomingMissed) { + _read = NO; + } else { + _read = YES; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (self.callSchemaVersion < 1) { + // Assume user has already seen any call that predate read-tracking + _read = YES; + } + + _callSchemaVersion = TSCallCurrentSchemaVersion; + + return self; +} + +- (OWSInteractionType)interactionType +{ + return OWSInteractionType_Call; +} + +- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + // We don't actually use the `transaction` but other sibling classes do. + switch (_callType) { + case RPRecentCallTypeIncoming: + return NSLocalizedString(@"INCOMING_CALL", @"info message text in conversation view"); + case RPRecentCallTypeOutgoing: + return NSLocalizedString(@"OUTGOING_CALL", @"info message text in conversation view"); + case RPRecentCallTypeIncomingMissed: + return NSLocalizedString(@"MISSED_CALL", @"info message text in conversation view"); + case RPRecentCallTypeOutgoingIncomplete: + return NSLocalizedString(@"OUTGOING_INCOMPLETE_CALL", @"info message text in conversation view"); + case RPRecentCallTypeIncomingIncomplete: + return NSLocalizedString(@"INCOMING_INCOMPLETE_CALL", @"info message text in conversation view"); + case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity: + return NSLocalizedString(@"INFO_MESSAGE_MISSED_CALL_DUE_TO_CHANGED_IDENITY", @"info message text shown in conversation view"); + case RPRecentCallTypeIncomingDeclined: + return NSLocalizedString(@"INCOMING_DECLINED_CALL", + @"info message recorded in conversation history when local user declined a call"); + case RPRecentCallTypeOutgoingMissed: + return NSLocalizedString(@"OUTGOING_MISSED_CALL", + @"info message recorded in conversation history when local user tries and fails to call another user."); + } +} + +#pragma mark - OWSReadTracking + +- (uint64_t)expireStartedAt +{ + return 0; +} + +- (BOOL)shouldAffectUnreadCounts +{ + return YES; +} + +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + + OWSAssertDebug(transaction); + + if (_read) { + return; + } + + OWSLogDebug(@"marking as read uniqueId: %@ which has timestamp: %llu", self.uniqueId, self.timestamp); + _read = YES; + [self saveWithTransaction:transaction]; + + // Ignore sendReadReceipt - it doesn't apply to calls. +} + +#pragma mark - Methods + +- (void)updateCallType:(RPRecentCallType)callType +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self updateCallType:callType transaction:transaction]; + }]; +} + +- (void)updateCallType:(RPRecentCallType)callType transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + OWSLogInfo(@"updating call type of call: %@ -> %@ with uniqueId: %@ which has timestamp: %llu", + NSStringFromCallType(_callType), + NSStringFromCallType(callType), + self.uniqueId, + self.timestamp); + + _callType = callType; + + [self saveWithTransaction:transaction]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSConstants.h b/SignalUtilitiesKit/TSConstants.h new file mode 100644 index 000000000..26962c8cc --- /dev/null +++ b/SignalUtilitiesKit/TSConstants.h @@ -0,0 +1,76 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#ifndef TextSecureKit_Constants_h +#define TextSecureKit_Constants_h + +typedef NS_ENUM(NSInteger, TSWhisperMessageType) { + TSUnknownMessageType = 0, + TSEncryptedWhisperMessageType = 1, + TSIgnoreOnIOSWhisperMessageType = 2, // on droid this is the prekey bundle message irrelevant for us + TSPreKeyWhisperMessageType = 3, + TSUnencryptedWhisperMessageType = 4, + TSUnidentifiedSenderMessageType = 6, + TSClosedGroupCiphertextMessageType = 7, + TSFallbackMessageType = 101 // Loki: Encrypted using the fallback session cipher. Contains a pre key bundle if it's a session request. +}; + +#pragma mark Server Address + +#define textSecureHTTPTimeOut 10 + +#define kLegalTermsUrlString @"https://getsession.org/privacy-policy/" + +//#ifndef DEBUG + +// Production +#define textSecureWebSocketAPI @"wss://textsecure-service.whispersystems.org/v1/websocket/" +#define textSecureCDNServerURL @"https://cdn.signal.org" +// Use same reflector for service and CDN +#define textSecureServiceReflectorHost @"europe-west1-signal-cdn-reflector.cloudfunctions.net" +#define textSecureCDNReflectorHost @"europe-west1-signal-cdn-reflector.cloudfunctions.net" +#define contactDiscoveryURL @"https://api.directory.signal.org" +#define kUDTrustRoot @"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF" +#define USING_PRODUCTION_SERVICE + +//#else + +// Staging +//#define textSecureWebSocketAPI @"wss://textsecure-service-staging.whispersystems.org/v1/websocket/" +//#define textSecureServerURL @"https://textsecure-service-staging.whispersystems.org/" +//#define textSecureCDNServerURL @"https://cdn-staging.signal.org" +//#define textSecureServiceReflectorHost @"meek-signal-service-staging.appspot.com"; +//#define textSecureCDNReflectorHost @"meek-signal-cdn-staging.appspot.com"; +//#define contactDiscoveryURL @"https://api-staging.directory.signal.org" +//#define kUDTrustRoot @"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx" + +//#endif + +BOOL IsUsingProductionService(void); + +#define textSecureAccountsAPI @"v1/accounts" +#define textSecureAttributesAPI @"/attributes/" + +#define textSecureMessagesAPI @"v1/messages/" +#define textSecureKeysAPI @"v2/keys" +#define textSecureSignedKeysAPI @"v2/keys/signed" +#define textSecureDirectoryAPI @"v1/directory" +#define textSecureAttachmentsAPI @"v1/attachments" +#define textSecureDeviceProvisioningCodeAPI @"v1/devices/provisioning/code" +#define textSecureDeviceProvisioningAPIFormat @"v1/provisioning/%@" +#define textSecureDevicesAPIFormat @"v1/devices/%@" +#define textSecureProfileAPIFormat @"v1/profile/%@" +#define textSecureSetProfileNameAPIFormat @"v1/profile/name/%@" +#define textSecureProfileAvatarFormAPI @"v1/profile/form/avatar" +#define textSecure2FAAPI @"/v1/accounts/pin" + +#define SignalApplicationGroup @"group.com.loki-project.loki-messenger" + +#endif + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSConstants.m b/SignalUtilitiesKit/TSConstants.m new file mode 100644 index 000000000..b82b9e0e7 --- /dev/null +++ b/SignalUtilitiesKit/TSConstants.m @@ -0,0 +1,18 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +BOOL IsUsingProductionService() +{ +#ifdef USING_PRODUCTION_SERVICE + return YES; +#else + return NO; +#endif +} + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSContactThread.h b/SignalUtilitiesKit/TSContactThread.h new file mode 100644 index 000000000..1671c9fbb --- /dev/null +++ b/SignalUtilitiesKit/TSContactThread.h @@ -0,0 +1,49 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSThread.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const TSContactThreadPrefix; + +typedef NS_ENUM(NSInteger, SNSessionRestorationStatus); + +@interface TSContactThread : TSThread + +@property (atomic) SNSessionRestorationStatus sessionResetStatus; +@property (atomic, readonly) NSArray *sessionRestoreDevices; + +@property (nonatomic) BOOL hasDismissedOffers; + +- (instancetype)initWithContactId:(NSString *)contactId; + ++ (instancetype)getOrCreateThreadWithContactId:(NSString *)contactId NS_SWIFT_NAME(getOrCreateThread(contactId:)); + ++ (instancetype)getOrCreateThreadWithContactId:(NSString *)contactId + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// Unlike getOrCreateThreadWithContactId, this will _NOT_ create a thread if one does not already exist. ++ (nullable instancetype)getThreadWithContactId:(NSString *)contactId transaction:(YapDatabaseReadTransaction *)transaction; + +- (NSString *)contactIdentifier; + ++ (NSString *)contactIdFromThreadId:(NSString *)threadId; + ++ (NSString *)threadIdFromContactId:(NSString *)contactId; + +// This method can be used to get the conversation color for a given +// recipient without using a read/write transaction to create a +// contact thread. ++ (NSString *)conversationColorNameForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadTransaction *)transaction; + +#pragma mark - Loki Session Restore + +- (void)addSessionRestoreDevice:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction; +- (void)removeAllSessionRestoreDevicesWithTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSContactThread.m b/SignalUtilitiesKit/TSContactThread.m new file mode 100644 index 000000000..6b0d5e219 --- /dev/null +++ b/SignalUtilitiesKit/TSContactThread.m @@ -0,0 +1,142 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSContactThread.h" +#import "ContactsManagerProtocol.h" +#import "ContactsUpdater.h" +#import "NotificationsProtocol.h" +#import "OWSIdentityManager.h" +#import "SSKEnvironment.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const TSContactThreadPrefix = @"c"; + +@implementation TSContactThread + +- (instancetype)initWithContactId:(NSString *)contactId { + NSString *uniqueIdentifier = [[self class] threadIdFromContactId:contactId]; + + OWSAssertDebug(contactId.length > 0); + + self = [super initWithUniqueId:uniqueIdentifier]; + + // No session reset ongoing + _sessionResetStatus = SNSessionRestorationStatusNone; + _sessionRestoreDevices = @[]; + + return self; +} + ++ (instancetype)getOrCreateThreadWithContactId:(NSString *)contactId + transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssertDebug(contactId.length > 0); + + TSContactThread *thread = + [self fetchObjectWithUniqueID:[self threadIdFromContactId:contactId] transaction:transaction]; + + if (!thread) { + thread = [[TSContactThread alloc] initWithContactId:contactId]; + [thread saveWithTransaction:transaction]; + } + + return thread; +} + ++ (instancetype)getOrCreateThreadWithContactId:(NSString *)contactId +{ + OWSAssertDebug(contactId.length > 0); + + __block TSContactThread *thread; + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + thread = [self getOrCreateThreadWithContactId:contactId transaction:transaction]; + }]; + + return thread; +} + ++ (nullable instancetype)getThreadWithContactId:(NSString *)contactId transaction:(YapDatabaseReadTransaction *)transaction; +{ + return [TSContactThread fetchObjectWithUniqueID:[self threadIdFromContactId:contactId] transaction:transaction]; +} + +- (NSString *)contactIdentifier { + return [[self class] contactIdFromThreadId:self.uniqueId]; +} + +- (NSArray *)recipientIdentifiers +{ + return @[ self.contactIdentifier ]; +} + +- (BOOL)isGroupThread { + return false; +} + +- (BOOL)hasSafetyNumbers +{ + return !![[OWSIdentityManager sharedManager] identityKeyForRecipientId:self.contactIdentifier]; +} + +// TODO deprecate this? seems weird to access the displayName in the DB model +- (NSString *)name +{ + return [SSKEnvironment.shared.contactsManager displayNameForPhoneIdentifier:self.contactIdentifier]; +} + ++ (NSString *)threadIdFromContactId:(NSString *)contactId { + return [TSContactThreadPrefix stringByAppendingString:contactId]; +} + ++ (NSString *)contactIdFromThreadId:(NSString *)threadId { + return [threadId substringWithRange:NSMakeRange(1, threadId.length - 1)]; +} + ++ (NSString *)conversationColorNameForRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + + TSContactThread *_Nullable contactThread = + [TSContactThread getThreadWithContactId:recipientId transaction:transaction]; + if (contactThread) { + return contactThread.conversationColorName; + } + return [self stableColorNameForNewConversationWithString:recipientId]; +} + +#pragma mark - Loki Session Restore + +- (void)addSessionRestoreDevice:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction +{ + NSMutableSet *set = [[NSMutableSet alloc] initWithArray:_sessionRestoreDevices]; + [set addObject:hexEncodedPublicKey]; + [self setSessionRestoreDevices:set.allObjects transaction:transaction]; +} + +- (void)removeAllSessionRestoreDevicesWithTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction +{ + [self setSessionRestoreDevices:@[] transaction:transaction]; +} + +- (void)setSessionRestoreDevices:(NSArray *)sessionRestoreDevices transaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction { + _sessionRestoreDevices = sessionRestoreDevices; + void (^postNotification)() = ^() { + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.threadSessionRestoreDevicesChanged object:self.uniqueId]; + }; + if (transaction == nil) { + [self save]; + [self.dbReadWriteConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{ postNotification(); }]; + } else { + [self saveWithTransaction:transaction]; + [transaction.connection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{ postNotification(); }]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSDatabaseSecondaryIndexes.h b/SignalUtilitiesKit/TSDatabaseSecondaryIndexes.h new file mode 100644 index 000000000..f2e377654 --- /dev/null +++ b/SignalUtilitiesKit/TSDatabaseSecondaryIndexes.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TSDatabaseSecondaryIndexes : NSObject + ++ (NSString *)registerTimeStampIndexExtensionName; + ++ (YapDatabaseSecondaryIndex *)registerTimeStampIndex; + ++ (void)enumerateMessagesWithTimestamp:(uint64_t)timestamp + withBlock:(void (^)(NSString *collection, NSString *key, BOOL *stop))block + usingTransaction:(YapDatabaseReadTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSDatabaseSecondaryIndexes.m b/SignalUtilitiesKit/TSDatabaseSecondaryIndexes.m new file mode 100644 index 000000000..d31c677d9 --- /dev/null +++ b/SignalUtilitiesKit/TSDatabaseSecondaryIndexes.m @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSDatabaseSecondaryIndexes.h" +#import "OWSStorage.h" +#import "TSInteraction.h" + +NS_ASSUME_NONNULL_BEGIN + +#define TSTimeStampSQLiteIndex @"messagesTimeStamp" + +@implementation TSDatabaseSecondaryIndexes + ++ (NSString *)registerTimeStampIndexExtensionName +{ + return @"idx"; +} + ++ (YapDatabaseSecondaryIndex *)registerTimeStampIndex { + YapDatabaseSecondaryIndexSetup *setup = [[YapDatabaseSecondaryIndexSetup alloc] init]; + [setup addColumn:TSTimeStampSQLiteIndex withType:YapDatabaseSecondaryIndexTypeReal]; + + YapDatabaseSecondaryIndexWithObjectBlock block = + ^(YapDatabaseReadTransaction *transaction, NSMutableDictionary *dict, NSString *collection, NSString *key, id object) { + + if ([object isKindOfClass:[TSInteraction class]]) { + TSInteraction *interaction = (TSInteraction *)object; + + [dict setObject:@(interaction.timestamp) forKey:TSTimeStampSQLiteIndex]; + } + }; + + YapDatabaseSecondaryIndexHandler *handler = [YapDatabaseSecondaryIndexHandler withObjectBlock:block]; + + YapDatabaseSecondaryIndex *secondaryIndex = + [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler versionTag:nil]; + + return secondaryIndex; +} + + ++ (void)enumerateMessagesWithTimestamp:(uint64_t)timestamp + withBlock:(void (^)(NSString *collection, NSString *key, BOOL *stop))block + usingTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSString *formattedString = [NSString stringWithFormat:@"WHERE %@ = %lld", TSTimeStampSQLiteIndex, timestamp]; + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:formattedString]; + [[transaction ext:[self registerTimeStampIndexExtensionName]] enumerateKeysMatchingQuery:query usingBlock:block]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSDatabaseView.h b/SignalUtilitiesKit/TSDatabaseView.h new file mode 100644 index 000000000..65fa2571a --- /dev/null +++ b/SignalUtilitiesKit/TSDatabaseView.h @@ -0,0 +1,74 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSStorage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const TSInboxGroup; +extern NSString *const TSArchiveGroup; +extern NSString *const TSUnreadIncomingMessagesGroup; +extern NSString *const TSSecondaryDevicesGroup; + +extern NSString *const TSThreadDatabaseViewExtensionName; + +extern NSString *const TSMessageDatabaseViewExtensionName; +extern NSString *const TSMessageDatabaseViewExtensionName_Legacy; + +extern NSString *const TSUnreadDatabaseViewExtensionName; +extern NSString *const TSUnseenDatabaseViewExtensionName; +extern NSString *const TSThreadOutgoingMessageDatabaseViewExtensionName; +extern NSString *const TSThreadSpecialMessagesDatabaseViewExtensionName; + +extern NSString *const TSSecondaryDevicesDatabaseViewExtensionName; + +extern NSString *const TSLazyRestoreAttachmentsGroup; +extern NSString *const TSLazyRestoreAttachmentsDatabaseViewExtensionName; + +@interface TSDatabaseView : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +#pragma mark - Views + +// Returns the "unseen" database view if it is ready; +// otherwise it returns the "unread" database view. ++ (id)unseenDatabaseViewExtension:(YapDatabaseReadTransaction *)transaction; + ++ (id)threadOutgoingMessageDatabaseView:(YapDatabaseReadTransaction *)transaction; + ++ (id)threadSpecialMessagesDatabaseView:(YapDatabaseReadTransaction *)transaction; + +#pragma mark - Registration + ++ (void)registerCrossProcessNotifier:(OWSStorage *)storage; + +// This method must be called _AFTER_ asyncRegisterThreadInteractionsDatabaseView. ++ (void)asyncRegisterThreadDatabaseView:(OWSStorage *)storage; + ++ (void)asyncRegisterThreadInteractionsDatabaseView:(OWSStorage *)storage; ++ (void)asyncRegisterLegacyThreadInteractionsDatabaseView:(OWSStorage *)storage; + ++ (void)asyncRegisterThreadOutgoingMessagesDatabaseView:(OWSStorage *)storage; + +// Instances of OWSReadTracking for wasRead is NO and shouldAffectUnreadCounts is YES. +// +// Should be used for "unread message counts". ++ (void)asyncRegisterUnreadDatabaseView:(OWSStorage *)storage; + +// Should be used for "unread indicator". +// +// Instances of OWSReadTracking for wasRead is NO. ++ (void)asyncRegisterUnseenDatabaseView:(OWSStorage *)storage; + ++ (void)asyncRegisterThreadSpecialMessagesDatabaseView:(OWSStorage *)storage; + ++ (void)asyncRegisterSecondaryDevicesDatabaseView:(OWSStorage *)storage; + ++ (void)asyncRegisterLazyRestoreAttachmentsDatabaseView:(OWSStorage *)storage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSDatabaseView.m b/SignalUtilitiesKit/TSDatabaseView.m new file mode 100644 index 000000000..e7d0f15f7 --- /dev/null +++ b/SignalUtilitiesKit/TSDatabaseView.m @@ -0,0 +1,515 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSDatabaseView.h" +#import "OWSDevice.h" +#import "OWSReadTracking.h" +#import "TSAttachment.h" +#import "TSAttachmentPointer.h" +#import "TSIncomingMessage.h" +#import "TSInvalidIdentityKeyErrorMessage.h" +#import "TSOutgoingMessage.h" +#import "TSThread.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const TSInboxGroup = @"TSInboxGroup"; +NSString *const TSArchiveGroup = @"TSArchiveGroup"; + +NSString *const TSUnreadIncomingMessagesGroup = @"TSUnreadIncomingMessagesGroup"; +NSString *const TSSecondaryDevicesGroup = @"TSSecondaryDevicesGroup"; + +// YAPDB BUG: when changing from non-persistent to persistent view, we had to rename TSThreadDatabaseViewExtensionName +// -> TSThreadDatabaseViewExtensionName2 to work around https://github.com/yapstudios/YapDatabase/issues/324 +NSString *const TSThreadDatabaseViewExtensionName = @"TSThreadDatabaseViewExtensionName2"; + +// We sort interactions by a monotonically increasing counter. +// +// Previously we sorted the interactions database by local timestamp, which was problematic if the local clock changed. +// We need to maintain the legacy extension for purposes of migration. +// +// The "Legacy" sorting extension name constant has the same value as always, so that it won't need to be rebuilt, while +// the "Modern" sorting extension name constant has the same symbol name that we've always used for sorting +// interactions, so that the callsites won't need to change. +NSString *const TSMessageDatabaseViewExtensionName = @"TSMessageDatabaseViewExtensionName_Monotonic"; +NSString *const TSMessageDatabaseViewExtensionName_Legacy = @"TSMessageDatabaseViewExtensionName"; + +NSString *const TSThreadOutgoingMessageDatabaseViewExtensionName = @"TSThreadOutgoingMessageDatabaseViewExtensionName"; +NSString *const TSUnreadDatabaseViewExtensionName = @"TSUnreadDatabaseViewExtensionName"; +NSString *const TSUnseenDatabaseViewExtensionName = @"TSUnseenDatabaseViewExtensionName"; +NSString *const TSThreadSpecialMessagesDatabaseViewExtensionName = @"TSThreadSpecialMessagesDatabaseViewExtensionName"; +NSString *const TSSecondaryDevicesDatabaseViewExtensionName = @"TSSecondaryDevicesDatabaseViewExtensionName"; +NSString *const TSLazyRestoreAttachmentsDatabaseViewExtensionName + = @"TSLazyRestoreAttachmentsDatabaseViewExtensionName"; +NSString *const TSLazyRestoreAttachmentsGroup = @"TSLazyRestoreAttachmentsGroup"; + +@interface OWSStorage (TSDatabaseView) + +- (BOOL)registerExtension:(YapDatabaseExtension *)extension withName:(NSString *)extensionName; + +@end + +#pragma mark - + +@implementation TSDatabaseView + ++ (void)registerCrossProcessNotifier:(OWSStorage *)storage +{ + OWSAssertDebug(storage); + + // I don't think the identifier and name of this extension matter for our purposes, + // so long as they don't conflict with any other extension names. + YapDatabaseExtension *extension = + [[YapDatabaseCrossProcessNotification alloc] initWithIdentifier:@"SignalCrossProcessNotifier"]; + [storage registerExtension:extension withName:@"SignalCrossProcessNotifier"]; +} + ++ (void)registerMessageDatabaseViewWithName:(NSString *)viewName + viewGrouping:(YapDatabaseViewGrouping *)viewGrouping + version:(NSString *)version + storage:(OWSStorage *)storage +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(viewName.length > 0); + OWSAssertDebug((viewGrouping)); + OWSAssertDebug(storage); + + YapDatabaseView *existingView = [storage registeredExtension:viewName]; + if (existingView) { + OWSFailDebug(@"Registered database view twice: %@", viewName); + return; + } + + YapDatabaseViewSorting *viewSorting = [self messagesSorting]; + + YapDatabaseViewOptions *options = [[YapDatabaseViewOptions alloc] init]; + options.isPersistent = YES; + options.allowedCollections = + [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSInteraction collection]]]; + + YapDatabaseView *view = [[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping + sorting:viewSorting + versionTag:version + options:options]; + [storage asyncRegisterExtension:view withName:viewName]; +} + ++ (void)asyncRegisterUnreadDatabaseView:(OWSStorage *)storage +{ + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if ([object conformsToProtocol:@protocol(OWSReadTracking)]) { + id possiblyRead = (id)object; + if (!possiblyRead.wasRead && possiblyRead.shouldAffectUnreadCounts) { + return possiblyRead.uniqueThreadId; + } + } + return nil; + }]; + + [self registerMessageDatabaseViewWithName:TSUnreadDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"2" + storage:storage]; +} + ++ (void)asyncRegisterUnseenDatabaseView:(OWSStorage *)storage +{ + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if ([object conformsToProtocol:@protocol(OWSReadTracking)]) { + id possiblyRead = (id)object; + if (!possiblyRead.wasRead) { + return possiblyRead.uniqueThreadId; + } + } + return nil; + }]; + + [self registerMessageDatabaseViewWithName:TSUnseenDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"2" + storage:storage]; +} + ++ (void)asyncRegisterThreadSpecialMessagesDatabaseView:(OWSStorage *)storage +{ + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if (![object isKindOfClass:[TSInteraction class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object class], collection); + return nil; + } + TSInteraction *interaction = (TSInteraction *)object; + if ([interaction isDynamicInteraction]) { + return interaction.uniqueThreadId; + } else if ([object isKindOfClass:[TSInvalidIdentityKeyErrorMessage class]]) { + return interaction.uniqueThreadId; + } else if ([object isKindOfClass:[TSErrorMessage class]]) { + TSErrorMessage *errorMessage = (TSErrorMessage *)object; + if (errorMessage.errorType == TSErrorMessageNonBlockingIdentityChange) { + return errorMessage.uniqueThreadId; + } + } + return nil; + }]; + + [self registerMessageDatabaseViewWithName:TSThreadSpecialMessagesDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"2" + storage:storage]; +} + ++ (void)asyncRegisterLegacyThreadInteractionsDatabaseView:(OWSStorage *)storage +{ + OWSAssertIsOnMainThread(); + OWSAssert(storage); + + YapDatabaseView *existingView = [storage registeredExtension:TSMessageDatabaseViewExtensionName_Legacy]; + if (existingView) { + OWSFailDebug(@"Registered database view twice: %@", TSMessageDatabaseViewExtensionName_Legacy); + return; + } + + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if (![object isKindOfClass:[TSInteraction class]]) { + OWSFailDebug(@"%@ Unexpected entity %@ in collection: %@", self.logTag, [object class], collection); + return nil; + } + TSInteraction *interaction = (TSInteraction *)object; + + return interaction.uniqueThreadId; + }]; + + YapDatabaseViewSorting *viewSorting = + [YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *transaction, + NSString *group, + NSString *collection1, + NSString *key1, + id object1, + NSString *collection2, + NSString *key2, + id object2) { + if (![object1 isKindOfClass:[TSInteraction class]]) { + OWSFailDebug(@"%@ Unexpected entity %@ in collection: %@", self.logTag, [object1 class], collection1); + return NSOrderedSame; + } + if (![object2 isKindOfClass:[TSInteraction class]]) { + OWSFailDebug(@"%@ Unexpected entity %@ in collection: %@", self.logTag, [object2 class], collection2); + return NSOrderedSame; + } + TSInteraction *interaction1 = (TSInteraction *)object1; + TSInteraction *interaction2 = (TSInteraction *)object2; + + // Legit usage of timestampForLegacySorting since we're registering the + // legacy extension + uint64_t timestamp1 = interaction1.timestampForLegacySorting; + uint64_t timestamp2 = interaction2.timestampForLegacySorting; + + if (timestamp1 > timestamp2) { + return NSOrderedDescending; + } else if (timestamp1 < timestamp2) { + return NSOrderedAscending; + } else { + return NSOrderedSame; + } + }]; + + YapDatabaseViewOptions *options = [YapDatabaseViewOptions new]; + options.isPersistent = YES; + options.allowedCollections = + [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSInteraction collection]]]; + + YapDatabaseView *view = + [[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"1" options:options]; + + [storage asyncRegisterExtension:view withName:TSMessageDatabaseViewExtensionName_Legacy]; +} + ++ (void)asyncRegisterThreadInteractionsDatabaseView:(OWSStorage *)storage +{ + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if (![object isKindOfClass:[TSInteraction class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object class], collection); + return nil; + } + TSInteraction *interaction = (TSInteraction *)object; + + return interaction.uniqueThreadId; + }]; + + [self registerMessageDatabaseViewWithName:TSMessageDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"2" + storage:storage]; +} + ++ (void)asyncRegisterThreadOutgoingMessagesDatabaseView:(OWSStorage *)storage +{ + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if ([object isKindOfClass:[TSOutgoingMessage class]]) { + return ((TSOutgoingMessage *)object).uniqueThreadId; + } + return nil; + }]; + + [self registerMessageDatabaseViewWithName:TSThreadOutgoingMessageDatabaseViewExtensionName + viewGrouping:viewGrouping + version:@"3" + storage:storage]; +} + ++ (void)asyncRegisterThreadDatabaseView:(OWSStorage *)storage +{ + YapDatabaseView *threadView = [storage registeredExtension:TSThreadDatabaseViewExtensionName]; + if (threadView) { + OWSFailDebug(@"Registered database view twice: %@", TSThreadDatabaseViewExtensionName); + return; + } + + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if (![object isKindOfClass:[TSThread class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object class], collection); + return nil; + } + TSThread *thread = (TSThread *)object; + if (thread.isSlaveThread) { return nil; } + + if (thread.shouldThreadBeVisible) { + // Do nothing; we never hide threads that have ever had a message. + } else { + YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSMessageDatabaseViewExtensionName]; + OWSAssertDebug(viewTransaction); + NSUInteger threadMessageCount = [viewTransaction numberOfItemsInGroup:thread.uniqueId]; + if (threadMessageCount < 1) { + return nil; + } + } + + return [thread isArchivedWithTransaction:transaction] ? TSArchiveGroup : TSInboxGroup; + }]; + + YapDatabaseViewSorting *viewSorting = [self threadSorting]; + + YapDatabaseViewOptions *options = [[YapDatabaseViewOptions alloc] init]; + options.isPersistent = YES; + options.allowedCollections = + [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSThread collection]]]; + + YapDatabaseView *databaseView = + [[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"4" options:options]; + + [storage asyncRegisterExtension:databaseView withName:TSThreadDatabaseViewExtensionName]; +} + ++ (YapDatabaseViewSorting *)threadSorting { + return [YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *transaction, + NSString *group, + NSString *collection1, + NSString *key1, + id object1, + NSString *collection2, + NSString *key2, + id object2) { + if (![object1 isKindOfClass:[TSThread class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object1 class], collection1); + return NSOrderedSame; + } + if (![object2 isKindOfClass:[TSThread class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object2 class], collection2); + return NSOrderedSame; + } + TSThread *thread1 = (TSThread *)object1; + TSThread *thread2 = (TSThread *)object2; + if ([group isEqualToString:TSArchiveGroup] || [group isEqualToString:TSInboxGroup]) { + + TSInteraction *_Nullable lastInteractionForInbox1 = + [thread1 lastInteractionForInboxWithTransaction:transaction]; + NSDate *date1 = lastInteractionForInbox1 ? lastInteractionForInbox1.receivedAtDate : thread1.creationDate; + + TSInteraction *_Nullable lastInteractionForInbox2 = + [thread2 lastInteractionForInboxWithTransaction:transaction]; + NSDate *date2 = lastInteractionForInbox2 ? lastInteractionForInbox2.receivedAtDate : thread2.creationDate; + + return [date1 compare:date2]; + } + + return NSOrderedSame; + }]; +} + ++ (YapDatabaseViewSorting *)messagesSorting { + return [YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *transaction, + NSString *group, + NSString *collection1, + NSString *key1, + id object1, + NSString *collection2, + NSString *key2, + id object2) { + if (![object1 isKindOfClass:[TSInteraction class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object1 class], collection1); + return NSOrderedSame; + } + if (![object2 isKindOfClass:[TSInteraction class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object2 class], collection2); + return NSOrderedSame; + } + TSInteraction *message1 = (TSInteraction *)object1; + TSInteraction *message2 = (TSInteraction *)object2; + + return [message1 compareForSorting:message2]; + }]; +} + ++ (void)asyncRegisterSecondaryDevicesDatabaseView:(OWSStorage *)storage +{ + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if (![object isKindOfClass:[OWSDevice class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object class], collection); + return nil; + } + OWSDevice *device = (OWSDevice *)object; + if (![device isPrimaryDevice]) { + return TSSecondaryDevicesGroup; + } + return nil; + }]; + + YapDatabaseViewSorting *viewSorting = [YapDatabaseViewSorting withObjectBlock:^NSComparisonResult( + YapDatabaseReadTransaction *transaction, + NSString *group, + NSString *collection1, + NSString *key1, + id object1, + NSString *collection2, + NSString *key2, + id object2) { + if (![object1 isKindOfClass:[OWSDevice class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object1 class], collection1); + return NSOrderedSame; + } + if (![object2 isKindOfClass:[OWSDevice class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object2 class], collection2); + return NSOrderedSame; + } + OWSDevice *device1 = (OWSDevice *)object1; + OWSDevice *device2 = (OWSDevice *)object2; + + return [device2.createdAt compare:device1.createdAt]; + }]; + + YapDatabaseViewOptions *options = [YapDatabaseViewOptions new]; + options.isPersistent = YES; + + NSSet *deviceCollection = [NSSet setWithObject:[OWSDevice collection]]; + options.allowedCollections = [[YapWhitelistBlacklist alloc] initWithWhitelist:deviceCollection]; + + YapDatabaseView *view = + [[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"3" options:options]; + + [storage asyncRegisterExtension:view withName:TSSecondaryDevicesDatabaseViewExtensionName]; +} + ++ (void)asyncRegisterLazyRestoreAttachmentsDatabaseView:(OWSStorage *)storage +{ + YapDatabaseViewGrouping *viewGrouping = [YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable( + YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object) { + if (![object isKindOfClass:[TSAttachment class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object class], collection); + return nil; + } + if (![object isKindOfClass:[TSAttachmentPointer class]]) { + return nil; + } + TSAttachmentPointer *attachmentPointer = (TSAttachmentPointer *)object; + if (attachmentPointer.lazyRestoreFragment) { + return TSLazyRestoreAttachmentsGroup; + } else { + return nil; + } + }]; + + YapDatabaseViewSorting *viewSorting = + [YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *transaction, + NSString *group, + NSString *collection1, + NSString *key1, + id object1, + NSString *collection2, + NSString *key2, + id object2) { + if (![object1 isKindOfClass:[TSAttachmentPointer class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object1 class], collection1); + return NSOrderedSame; + } + if (![object2 isKindOfClass:[TSAttachmentPointer class]]) { + OWSFailDebug(@"Unexpected entity %@ in collection: %@", [object2 class], collection2); + return NSOrderedSame; + } + + // Specific ordering doesn't matter; we just need a stable ordering. + TSAttachmentPointer *attachmentPointer1 = (TSAttachmentPointer *)object1; + TSAttachmentPointer *attachmentPointer2 = (TSAttachmentPointer *)object2; + return [attachmentPointer1.uniqueId compare:attachmentPointer2.uniqueId]; + }]; + + YapDatabaseViewOptions *options = [YapDatabaseViewOptions new]; + options.isPersistent = YES; + options.allowedCollections = + [[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[TSAttachment collection]]]; + YapDatabaseView *view = + [[YapDatabaseAutoView alloc] initWithGrouping:viewGrouping sorting:viewSorting versionTag:@"4" options:options]; + [storage asyncRegisterExtension:view withName:TSLazyRestoreAttachmentsDatabaseViewExtensionName]; +} + ++ (id)unseenDatabaseViewExtension:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + id _Nullable result = [transaction ext:TSUnseenDatabaseViewExtensionName]; + OWSAssertDebug(result); + + // TODO: I believe we can now safely remove this? + if (!result) { + result = [transaction ext:TSUnreadDatabaseViewExtensionName]; + OWSAssertDebug(result); + } + + return result; +} + +// MJK TODO - dynamic interactions ++ (id)threadOutgoingMessageDatabaseView:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + id result = [transaction ext:TSThreadOutgoingMessageDatabaseViewExtensionName]; + OWSAssertDebug(result); + + return result; +} + ++ (id)threadSpecialMessagesDatabaseView:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + id result = [transaction ext:TSThreadSpecialMessagesDatabaseViewExtensionName]; + OWSAssertDebug(result); + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSErrorMessage.h b/SignalUtilitiesKit/TSErrorMessage.h new file mode 100644 index 000000000..ba44c1f5e --- /dev/null +++ b/SignalUtilitiesKit/TSErrorMessage.h @@ -0,0 +1,76 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSReadTracking.h" +#import "TSMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoEnvelope; + +typedef NS_ENUM(int32_t, TSErrorMessageType) { + TSErrorMessageNoSession, + // DEPRECATED: We no longer create TSErrorMessageWrongTrustedIdentityKey, but + // persisted legacy messages could exist indefinitly. + TSErrorMessageWrongTrustedIdentityKey, + TSErrorMessageInvalidKeyException, + // unused + TSErrorMessageMissingKeyId, + TSErrorMessageInvalidMessage, + // unused + TSErrorMessageDuplicateMessage, + TSErrorMessageInvalidVersion, + TSErrorMessageNonBlockingIdentityChange, + TSErrorMessageUnknownContactBlockOffer, + TSErrorMessageGroupCreationFailed, +}; + +@interface TSErrorMessage : TSMessage + +- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contact + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt NS_UNAVAILABLE; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + failedMessageType:(TSErrorMessageType)errorMessageType + recipientId:(nullable NSString *)recipientId NS_DESIGNATED_INITIALIZER; + ++ (instancetype)corruptedMessageWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction; + ++ (instancetype)corruptedMessageInUnknownThread; + ++ (instancetype)invalidVersionWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction; + ++ (instancetype)invalidKeyExceptionWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction; + ++ (instancetype)missingSessionWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction; + ++ (instancetype)nonblockingIdentityChangeInThread:(TSThread *)thread recipientId:(NSString *)recipientId; + +@property (nonatomic, readonly) TSErrorMessageType errorType; +@property (nullable, nonatomic, readonly) NSString *recipientId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSErrorMessage.m b/SignalUtilitiesKit/TSErrorMessage.m new file mode 100644 index 000000000..9ac1072d2 --- /dev/null +++ b/SignalUtilitiesKit/TSErrorMessage.m @@ -0,0 +1,228 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSErrorMessage.h" +#import "ContactsManagerProtocol.h" +#import "OWSMessageManager.h" +#import "SSKEnvironment.h" +#import "TSContactThread.h" +#import "TSErrorMessage_privateConstructor.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSUInteger TSErrorMessageSchemaVersion = 1; + +@interface TSErrorMessage () + +@property (nonatomic, getter=wasRead) BOOL read; + +@property (nonatomic, readonly) NSUInteger errorMessageSchemaVersion; + +@end + +#pragma mark - + +@implementation TSErrorMessage + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (self.errorMessageSchemaVersion < 1) { + _read = YES; + } + + _errorMessageSchemaVersion = TSErrorMessageSchemaVersion; + + if (self.isDynamicInteraction) { + self.read = YES; + } + + return self; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + failedMessageType:(TSErrorMessageType)errorMessageType + recipientId:(nullable NSString *)recipientId +{ + self = [super initMessageWithTimestamp:timestamp + inThread:thread + messageBody:nil + attachmentIds:@[] + expiresInSeconds:0 + expireStartedAt:0 + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + + if (!self) { + return self; + } + + _errorType = errorMessageType; + _recipientId = recipientId; + _errorMessageSchemaVersion = TSErrorMessageSchemaVersion; + + if (self.isDynamicInteraction) { + self.read = YES; + } + + return self; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + failedMessageType:(TSErrorMessageType)errorMessageType +{ + return [self initWithTimestamp:timestamp inThread:thread failedMessageType:errorMessageType recipientId:nil]; +} + +- (instancetype)initWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction + failedMessageType:(TSErrorMessageType)errorMessageType +{ + NSString *source = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source; + TSContactThread *contactThread = + [TSContactThread getOrCreateThreadWithContactId:source transaction:transaction]; + + // Legit usage of senderTimestamp. We don't actually currently surface it in the UI, but it serves as + // a reference to the envelope which we failed to process. + return [self initWithTimestamp:envelope.timestamp inThread:contactThread failedMessageType:errorMessageType]; +} + +- (OWSInteractionType)interactionType +{ + return OWSInteractionType_Error; +} + +- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + switch (_errorType) { + case TSErrorMessageNoSession: + return NSLocalizedString(@"ERROR_MESSAGE_NO_SESSION", @""); + case TSErrorMessageInvalidMessage: + return NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""); + case TSErrorMessageInvalidVersion: + return NSLocalizedString(@"ERROR_MESSAGE_INVALID_VERSION", @""); + case TSErrorMessageDuplicateMessage: + return NSLocalizedString(@"ERROR_MESSAGE_DUPLICATE_MESSAGE", @""); + case TSErrorMessageInvalidKeyException: + return NSLocalizedString(@"ERROR_MESSAGE_INVALID_KEY_EXCEPTION", @""); + case TSErrorMessageWrongTrustedIdentityKey: + return NSLocalizedString(@"ERROR_MESSAGE_WRONG_TRUSTED_IDENTITY_KEY", @""); + case TSErrorMessageNonBlockingIdentityChange: { + if (self.recipientId) { + NSString *messageFormat = NSLocalizedString(@"ERROR_MESSAGE_NON_BLOCKING_IDENTITY_CHANGE_FORMAT", + @"Shown when signal users safety numbers changed, embeds the user's {{name or phone number}}"); + + NSString *recipientDisplayName = + [SSKEnvironment.shared.contactsManager displayNameForPhoneIdentifier:self.recipientId + transaction:transaction]; + return [NSString stringWithFormat:messageFormat, recipientDisplayName]; + } else { + // recipientId will be nil for legacy errors + return NSLocalizedString( + @"ERROR_MESSAGE_NON_BLOCKING_IDENTITY_CHANGE", @"Shown when signal users safety numbers changed"); + } + break; + } + case TSErrorMessageUnknownContactBlockOffer: + return NSLocalizedString(@"UNKNOWN_CONTACT_BLOCK_OFFER", + @"Message shown in conversation view that offers to block an unknown user."); + case TSErrorMessageGroupCreationFailed: + return NSLocalizedString(@"GROUP_CREATION_FAILED", + @"Message shown in conversation view that indicates there were issues with group creation."); + default: + return NSLocalizedString(@"ERROR_MESSAGE_UNKNOWN_ERROR", @""); + break; + } +} + ++ (instancetype)corruptedMessageWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + return [[self alloc] initWithEnvelope:envelope + withTransaction:transaction + failedMessageType:TSErrorMessageInvalidMessage]; +} + ++ (instancetype)corruptedMessageInUnknownThread +{ + // MJK TODO - Seems like we could safely remove this timestamp + return [[self alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:nil + failedMessageType:TSErrorMessageInvalidMessage]; +} + ++ (instancetype)invalidVersionWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + return [[self alloc] initWithEnvelope:envelope + withTransaction:transaction + failedMessageType:TSErrorMessageInvalidVersion]; +} + ++ (instancetype)invalidKeyExceptionWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + return [[self alloc] initWithEnvelope:envelope + withTransaction:transaction + failedMessageType:TSErrorMessageInvalidKeyException]; +} + ++ (instancetype)missingSessionWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + return + [[self alloc] initWithEnvelope:envelope withTransaction:transaction failedMessageType:TSErrorMessageNoSession]; +} + ++ (instancetype)nonblockingIdentityChangeInThread:(TSThread *)thread recipientId:(NSString *)recipientId +{ + // MJK TODO - should be safe to remove this senderTimestamp + return [[self alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + failedMessageType:TSErrorMessageNonBlockingIdentityChange + recipientId:recipientId]; +} + +#pragma mark - OWSReadTracking + +- (uint64_t)expireStartedAt +{ + return 0; +} + +- (BOOL)shouldAffectUnreadCounts +{ + return NO; +} + +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + if (_read) { + return; + } + + OWSLogDebug(@"marking as read uniqueId: %@ which has timestamp: %llu", self.uniqueId, self.timestamp); + _read = YES; + [self saveWithTransaction:transaction]; + + // Ignore sendReadReceipt - it doesn't apply to error messages. +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSErrorMessage_privateConstructor.h b/SignalUtilitiesKit/TSErrorMessage_privateConstructor.h new file mode 100644 index 000000000..77e5a39f4 --- /dev/null +++ b/SignalUtilitiesKit/TSErrorMessage_privateConstructor.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSErrorMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface TSErrorMessage () + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + failedMessageType:(TSErrorMessageType)errorMessageType NS_DESIGNATED_INITIALIZER; + +@property (atomic, nullable) NSData *envelopeData; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSGroupModel.h b/SignalUtilitiesKit/TSGroupModel.h new file mode 100644 index 000000000..5b312157b --- /dev/null +++ b/SignalUtilitiesKit/TSGroupModel.h @@ -0,0 +1,47 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "ContactsManagerProtocol.h" +#import "TSYapDatabaseObject.h" +#import "TSAccountManager.h" + + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, GroupType) { + closedGroup = 0, // a.k.a. private group chat + openGroup = 1, // a.k.a. public group chat + rssFeed = 2 +}; + +extern const int32_t kGroupIdLength; + +@interface TSGroupModel : TSYapDatabaseObject + +@property (nonatomic) NSArray *groupMemberIds; +@property (nonatomic) NSArray *groupAdminIds; +@property (nullable, readonly, nonatomic) NSString *groupName; +@property (readonly, nonatomic) NSData *groupId; +@property (nonatomic) GroupType groupType; +@property (nonatomic) NSMutableSet *removedMembers; + +#if TARGET_OS_IOS +@property (nullable, nonatomic, strong) UIImage *groupImage; + +- (instancetype)initWithTitle:(nullable NSString *)title + memberIds:(NSArray *)memberIds + image:(nullable UIImage *)image + groupId:(NSData *)groupId + groupType:(GroupType)groupType + adminIds:(NSArray *)adminIds; + +- (BOOL)isEqual:(id)other; +- (BOOL)isEqualToGroupModel:(TSGroupModel *)model; +- (NSString *)getInfoStringAboutUpdateTo:(TSGroupModel *)model contactsManager:(id)contactsManager; +- (void)updateGroupId: (NSData *)newGroupId; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSGroupModel.m b/SignalUtilitiesKit/TSGroupModel.m new file mode 100644 index 000000000..5b0564b1b --- /dev/null +++ b/SignalUtilitiesKit/TSGroupModel.m @@ -0,0 +1,194 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSGroupModel.h" +#import "FunctionalUtil.h" +#import "NSString+SSK.h" +#import +#import "OWSIdentityManager.h" + +NS_ASSUME_NONNULL_BEGIN + +const int32_t kGroupIdLength = 16; + +@interface TSGroupModel () + +@property (nullable, nonatomic) NSString *groupName; + +@end + +#pragma mark - + +@implementation TSGroupModel + +#if TARGET_OS_IOS +- (instancetype)initWithTitle:(nullable NSString *)title + memberIds:(NSArray *)memberIds + image:(nullable UIImage *)image + groupId:(NSData *)groupId + groupType:(GroupType)groupType + adminIds:(NSArray *)adminIds +{ + OWSAssertDebug(memberIds); + + _groupName = title; + _groupMemberIds = [memberIds copy]; + _groupImage = image; // image is stored in DB + _groupType = groupType; + _groupId = groupId; + _groupAdminIds = [adminIds copy]; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + // Occasionally seeing this as nil in legacy data, + // which causes crashes. + if (_groupMemberIds == nil) { + _groupMemberIds = [NSArray new]; + } + + if (_groupAdminIds == nil) { + _groupAdminIds = [NSArray new]; + } + + if (_removedMembers == nil) { + _removedMembers = [NSMutableSet new]; + } + + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (!other || ![other isKindOfClass:[self class]]) { + return NO; + } + return [self isEqualToGroupModel:other]; +} + +- (BOOL)isEqualToGroupModel:(TSGroupModel *)other { + if (self == other) + return YES; + if (![_groupId isEqualToData:other.groupId]) { + return NO; + } + if (![_groupName isEqual:other.groupName]) { + return NO; + } + if (!(_groupImage != nil && other.groupImage != nil && + [UIImagePNGRepresentation(_groupImage) isEqualToData:UIImagePNGRepresentation(other.groupImage)])) { + return NO; + } + if (_groupType != other.groupType) { + return NO; + } + NSMutableArray *compareMyGroupMemberIds = [NSMutableArray arrayWithArray:_groupMemberIds]; + [compareMyGroupMemberIds removeObjectsInArray:other.groupMemberIds]; + if ([compareMyGroupMemberIds count] > 0) { + return NO; + } + return YES; +} + +- (NSString *)getInfoStringAboutUpdateTo:(TSGroupModel *)newModel contactsManager:(id)contactsManager { + NSString *updatedGroupInfoString = @""; + if (self == newModel) { + return NSLocalizedString(@"GROUP_UPDATED", @""); + } + if (![_groupName isEqual:newModel.groupName]) { + if (newModel.groupName.length == 0) { + updatedGroupInfoString = [updatedGroupInfoString stringByAppendingString:@"Closed group created"]; + } else { + updatedGroupInfoString = [updatedGroupInfoString stringByAppendingString:[NSString stringWithFormat:NSLocalizedString(@"GROUP_TITLE_CHANGED", @""), newModel.groupName]]; + } + } + if (_groupImage != nil && newModel.groupImage != nil && + !([UIImagePNGRepresentation(_groupImage) isEqualToData:UIImagePNGRepresentation(newModel.groupImage)])) { + updatedGroupInfoString = + [updatedGroupInfoString stringByAppendingString:NSLocalizedString(@"GROUP_AVATAR_CHANGED", @"")]; + } + NSSet *oldMembers = [NSSet setWithArray:_groupMemberIds]; + NSSet *newMembers = [NSSet setWithArray:newModel.groupMemberIds]; + + NSMutableSet *membersWhoJoined = [NSMutableSet setWithSet:newMembers]; + [membersWhoJoined minusSet:oldMembers]; + + NSMutableSet *membersWhoLeft = [NSMutableSet setWithSet:oldMembers]; + [membersWhoLeft minusSet:newMembers]; + [membersWhoLeft minusSet:newModel.removedMembers]; + + + if ([membersWhoLeft count] > 0) { + NSArray *oldMembersNames = [[membersWhoLeft allObjects] map:^NSString*(NSString* item) { + return [LKUserDisplayNameUtilities getPrivateChatDisplayNameAvoidWriteTransaction:item]; + }]; + updatedGroupInfoString = [updatedGroupInfoString + stringByAppendingString:[NSString + stringWithFormat:NSLocalizedString(@"GROUP_MEMBER_LEFT", @""), + [oldMembersNames componentsJoinedByString:@", "]]]; + } + + if (membersWhoJoined.count > 0) { + updatedGroupInfoString = @"New members joined"; + } + + if (newModel.removedMembers.count > 0) { + NSString *masterDeviceHexEncodedPublicKey = [NSUserDefaults.standardUserDefaults stringForKey:@"masterDeviceHexEncodedPublicKey"]; + NSString *hexEncodedPublicKey = masterDeviceHexEncodedPublicKey != nil ? masterDeviceHexEncodedPublicKey : TSAccountManager.localNumber; + if ([newModel.removedMembers containsObject:hexEncodedPublicKey]) { + updatedGroupInfoString = [updatedGroupInfoString + stringByAppendingString:NSLocalizedString(@"YOU_WERE_REMOVED", @"")]; + } else { + NSArray *removedMemberNames = [newModel.removedMembers.allObjects map:^NSString*(NSString* publicKey) { + return [LKUserDisplayNameUtilities getPrivateChatDisplayNameFor:publicKey]; + }]; + if ([removedMemberNames count] > 1) { + updatedGroupInfoString = [updatedGroupInfoString + stringByAppendingString:[NSString + stringWithFormat:NSLocalizedString(@"GROUP_MEMBERS_REMOVED", @""), + [removedMemberNames componentsJoinedByString:@", "]]]; + } + else { + updatedGroupInfoString = [updatedGroupInfoString + stringByAppendingString:[NSString + stringWithFormat:NSLocalizedString(@"GROUP_MEMBER_REMOVED", @""), + removedMemberNames[0]]]; + } + } + } + if ([updatedGroupInfoString length] == 0) { + updatedGroupInfoString = NSLocalizedString(@"GROUP_UPDATED", @""); + } + return updatedGroupInfoString; +} + +#endif + +- (nullable NSString *)groupName +{ + return _groupName.filterStringForDisplay; +} + +- (void)setRemovedMembers:(NSMutableSet *)removedMembers +{ + _removedMembers = removedMembers; +} + +- (void)updateGroupId: (NSData *)newGroupId +{ + _groupId = newGroupId; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSGroupThread.h b/SignalUtilitiesKit/TSGroupThread.h new file mode 100644 index 000000000..b6378cda1 --- /dev/null +++ b/SignalUtilitiesKit/TSGroupThread.h @@ -0,0 +1,67 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSGroupModel.h" +#import "TSThread.h" +#import "LKGroupUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSAttachmentStream; +@class YapDatabaseReadWriteTransaction; + +extern NSString *const TSGroupThreadAvatarChangedNotification; +extern NSString *const TSGroupThread_NotificationKey_UniqueId; + +@interface TSGroupThread : TSThread + +@property (nonatomic, strong) TSGroupModel *groupModel; +@property (nonatomic, readonly) BOOL isRSSFeed; +@property (nonatomic, readonly) BOOL isPublicChat; +@property (nonatomic) BOOL usesSharedSenderKeys; + ++ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel; ++ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel + transaction:(YapDatabaseReadWriteTransaction *)transaction; + ++ (instancetype)getOrCreateThreadWithGroupId:(NSData *)groupId + groupType:(GroupType) groupType; ++ (instancetype)getOrCreateThreadWithGroupId:(NSData *)groupId + groupType:(GroupType) groupType + transaction:(YapDatabaseReadWriteTransaction *)transaction; + ++ (nullable instancetype)threadWithGroupId:(NSData *)groupId transaction:(YapDatabaseReadTransaction *)transaction; + ++ (NSString *)threadIdFromGroupId:(NSData *)groupId; + ++ (NSString *)defaultGroupName; + +- (BOOL)isLocalUserInGroup; +- (BOOL)isCurrentUserInGroupWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (BOOL)isUserMemberInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction; +- (BOOL)isUserAdminInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction; + +// all group threads containing recipient as a member ++ (NSArray *)groupThreadsWithRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)setGroupModel:(TSGroupModel *)newGroupModel withTransaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)leaveGroupWithSneakyTransaction; +- (void)leaveGroupWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)softDeleteGroupThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark - Avatar + +- (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream; +- (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)fireAvatarChangedNotification; + ++ (ConversationColorName)defaultConversationColorNameForGroupId:(NSData *)groupId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSGroupThread.m b/SignalUtilitiesKit/TSGroupThread.m new file mode 100644 index 000000000..903b75b31 --- /dev/null +++ b/SignalUtilitiesKit/TSGroupThread.m @@ -0,0 +1,339 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSGroupThread.h" +#import "TSAttachmentStream.h" +#import +#import +#import +#import +#import +#import "OWSPrimaryStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const TSGroupThreadAvatarChangedNotification = @"TSGroupThreadAvatarChangedNotification"; +NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_NotificationKey_UniqueId"; + +@implementation TSGroupThread + +#define TSGroupThreadPrefix @"g" + +- (instancetype)initWithGroupModel:(TSGroupModel *)groupModel +{ + OWSAssertDebug(groupModel); + OWSAssertDebug(groupModel.groupId.length > 0); + OWSAssertDebug(groupModel.groupMemberIds.count > 0); + + for (NSString *recipientId in groupModel.groupMemberIds) { + OWSAssertDebug(recipientId.length > 0); + } + + NSString *uniqueIdentifier = [[self class] threadIdFromGroupId:groupModel.groupId]; + self = [super initWithUniqueId:uniqueIdentifier]; + + if (!self) { + return self; + } + + _groupModel = groupModel; + + return self; +} + +- (instancetype)initWithGroupId:(NSData *)groupId groupType:(GroupType)groupType +{ + OWSAssertDebug(groupId.length > 0); + + NSString *localNumber = [TSAccountManager localNumber]; + OWSAssertDebug(localNumber.length > 0); + + TSGroupModel *groupModel = [[TSGroupModel alloc] initWithTitle:nil + memberIds:@[ localNumber ] + image:nil + groupId:groupId + groupType:groupType + adminIds:@[ localNumber ]]; + + self = [self initWithGroupModel:groupModel]; + + if (!self) { + return self; + } + + return self; +} + ++ (nullable instancetype)threadWithGroupId:(NSData *)groupId transaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(groupId.length > 0); + + return [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId] transaction:transaction]; +} + ++ (instancetype)getOrCreateThreadWithGroupId:(NSData *)groupId + groupType:(GroupType)groupType + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(groupId.length > 0); + OWSAssertDebug(transaction); + + TSGroupThread *thread = [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId] transaction:transaction]; + + if (!thread) { + thread = [[self alloc] initWithGroupId:groupId groupType:groupType]; + [thread saveWithTransaction:transaction]; + } + + return thread; +} + ++ (instancetype)getOrCreateThreadWithGroupId:(NSData *)groupId groupType:(GroupType)groupType +{ + OWSAssertDebug(groupId.length > 0); + + __block TSGroupThread *thread; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + thread = [self getOrCreateThreadWithGroupId:groupId groupType:groupType transaction:transaction]; + }]; + + return thread; +} + ++ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel + transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssertDebug(groupModel); + OWSAssertDebug(groupModel.groupId.length > 0); + OWSAssertDebug(transaction); + + TSGroupThread *thread = + [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupModel.groupId] transaction:transaction]; + + if (!thread) { + thread = [[TSGroupThread alloc] initWithGroupModel:groupModel]; + [thread saveWithTransaction:transaction]; + } + + return thread; +} + ++ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel +{ + OWSAssertDebug(groupModel); + OWSAssertDebug(groupModel.groupId.length > 0); + + __block TSGroupThread *thread; + + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + thread = [self getOrCreateThreadWithGroupModel:groupModel transaction:transaction]; + }]; + + return thread; +} + ++ (NSString *)threadIdFromGroupId:(NSData *)groupId +{ + OWSAssertDebug(groupId.length > 0); + + return [TSGroupThreadPrefix stringByAppendingString:[[LKGroupUtilities getDecodedGroupIDAsData:groupId] base64EncodedString]]; +} + ++ (NSData *)groupIdFromThreadId:(NSString *)threadId +{ + OWSAssertDebug(threadId.length > 0); + + return [NSData dataFromBase64String:[threadId substringWithRange:NSMakeRange(1, threadId.length - 1)]]; +} + +- (NSArray *)recipientIdentifiers +{ + NSMutableArray *groupMemberIds = [self.groupModel.groupMemberIds mutableCopy]; + + if (groupMemberIds == nil) { + return @[]; + } + + [groupMemberIds removeObject:TSAccountManager.localNumber]; + + return [groupMemberIds copy]; +} + +// @returns all threads to which the recipient is a member. +// +// @note If this becomes a hotspot we can extract into a YapDB View. +// As is, the number of groups should be small (dozens, *maybe* hundreds), and we only enumerate them upon SN changes. ++ (NSArray *)groupThreadsWithRecipientId:(NSString *)recipientId + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + NSMutableArray *groupThreads = [NSMutableArray new]; + + [self enumerateCollectionObjectsWithTransaction:transaction usingBlock:^(id obj, BOOL *stop) { + if ([obj isKindOfClass:[TSGroupThread class]]) { + TSGroupThread *groupThread = (TSGroupThread *)obj; + if ([groupThread.groupModel.groupMemberIds containsObject:recipientId]) { + [groupThreads addObject:groupThread]; + } + } + }]; + + return [groupThreads copy]; +} + +- (BOOL)isGroupThread +{ + return true; +} + +- (BOOL)isPublicChat +{ + return (self.groupModel.groupType == openGroup); +} + +- (BOOL)isRSSFeed +{ + return (self.groupModel.groupType == rssFeed); +} + +- (BOOL)isContactFriend +{ + return false; +} + +- (BOOL)isLocalUserInGroup +{ + __block BOOL result = NO; + + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + result = [self isCurrentUserInGroupWithTransaction:transaction]; + }]; + + return result; +} + +- (BOOL)isCurrentUserInGroupWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSString *userHexEncodedPublicKey = TSAccountManager.localNumber; + return [self isUserMemberInGroup:userHexEncodedPublicKey transaction:transaction]; +} + +- (BOOL)isUserMemberInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction +{ + if (hexEncodedPublicKey == nil) { return NO; } + NSSet *linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:hexEncodedPublicKey in:transaction]; + return [linkedDeviceHexEncodedPublicKeys intersectsSet:[NSSet setWithArray:self.groupModel.groupMemberIds]]; +} + +- (BOOL)isUserAdminInGroup:(NSString *)hexEncodedPublicKey transaction:(YapDatabaseReadTransaction *)transaction +{ + if (hexEncodedPublicKey == nil) { return NO; } + NSSet *linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:hexEncodedPublicKey in:transaction]; + return [linkedDeviceHexEncodedPublicKeys intersectsSet:[NSSet setWithArray:self.groupModel.groupAdminIds]]; +} + +- (NSString *)name +{ + // TODO sometimes groupName is set to the empty string. I'm hesitent to change + // the semantics here until we have time to thouroughly test the fallout. + // Instead, see the `groupNameOrDefault` which is appropriate for use when displaying + // text corresponding to a group. + return self.groupModel.groupName ?: self.class.defaultGroupName; +} + ++ (NSString *)defaultGroupName +{ + return NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @""); +} + +- (void)setGroupModel:(TSGroupModel *)newGroupModel withTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + self.groupModel = newGroupModel; + + [self saveWithTransaction:transaction]; + + [transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{ + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.groupThreadUpdated object:self.uniqueId]; + }]; +} + +- (void)leaveGroupWithSneakyTransaction +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self leaveGroupWithTransaction:transaction]; + }]; +} + +- (void)leaveGroupWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + NSMutableSet *newGroupMemberIDs = [NSMutableSet setWithArray:self.groupModel.groupMemberIds]; + NSString *userPublicKey = TSAccountManager.localNumber; + if (userPublicKey == nil) { return; } + NSSet *userLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userPublicKey in:transaction]; + [newGroupMemberIDs minusSet:userLinkedDevices]; + self.groupModel.groupMemberIds = newGroupMemberIDs.allObjects; + [self saveWithTransaction:transaction]; + [transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{ + [NSNotificationCenter.defaultCenter postNotificationName:NSNotification.groupThreadUpdated object:self.uniqueId]; + }]; +} + +- (void)softDeleteGroupThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self removeAllThreadInteractionsWithTransaction:transaction]; + self.shouldThreadBeVisible = NO; + [self saveWithTransaction:transaction]; +} + +#pragma mark - Avatar + +- (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self updateAvatarWithAttachmentStream:attachmentStream transaction:transaction]; + }]; +} + +- (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(attachmentStream); + OWSAssertDebug(transaction); + + self.groupModel.groupImage = [attachmentStream thumbnailImageSmallSync]; + [self saveWithTransaction:transaction]; + + [transaction addCompletionQueue:nil + completionBlock:^{ + [self fireAvatarChangedNotification]; + }]; + + // Avatars are stored directly in the database, so there's no need + // to keep the attachment around after assigning the image. + [attachmentStream removeWithTransaction:transaction]; +} + +- (void)fireAvatarChangedNotification +{ + OWSAssertIsOnMainThread(); + + NSDictionary *userInfo = @{ TSGroupThread_NotificationKey_UniqueId : self.uniqueId }; + + [[NSNotificationCenter defaultCenter] postNotificationName:TSGroupThreadAvatarChangedNotification + object:self.uniqueId + userInfo:userInfo]; +} + ++ (ConversationColorName)defaultConversationColorNameForGroupId:(NSData *)groupId +{ + OWSAssertDebug(groupId.length > 0); + + return [self.class stableColorNameForNewConversationWithString:[self threadIdFromGroupId:groupId]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSIncomingMessage.h b/SignalUtilitiesKit/TSIncomingMessage.h new file mode 100644 index 000000000..771723369 --- /dev/null +++ b/SignalUtilitiesKit/TSIncomingMessage.h @@ -0,0 +1,90 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSReadTracking.h" +#import "TSMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSContactThread; +@class TSGroupThread; + +@interface TSIncomingMessage : TSMessage + +@property (nonatomic, readonly, nullable) NSNumber *serverTimestamp; + +@property (nonatomic, readonly) BOOL wasReceivedByUD; + +- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +/** + * Inits an incoming group message that expires. + * + * @param timestamp + * When the message was created in milliseconds since epoch + * @param thread + * Thread to which the message belongs + * @param authorId + * Signal ID (i.e. e164) of the user who sent the message + * @param sourceDeviceId + * Numeric ID of the device used to send the message. Used to detect duplicate messages. + * @param body + * Body of the message + * @param attachmentIds + * The uniqueIds for the message's attachments, possibly an empty list. + * @param expiresInSeconds + * Seconds from when the message is read until it is deleted. + * @param quotedMessage + * If this message is a quoted reply to another message, contains data about that message. + * + * @return initiated incoming group message + */ +- (instancetype)initIncomingMessageWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview + serverTimestamp:(nullable NSNumber *)serverTimestamp + wasReceivedByUD:(BOOL)wasReceivedByUD NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +/* + * Find a message matching the senderId and timestamp, if any. + * + * @param authorId + * Signal ID (i.e. e164) of the user who sent the message + * @params timestamp + * When the message was created in milliseconds since epoch + * + */ ++ (nullable instancetype)findMessageWithAuthorId:(NSString *)authorId + timestamp:(uint64_t)timestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This will be 0 for messages created before we were tracking sourceDeviceId +@property (nonatomic, readonly) UInt32 sourceDeviceId; + +@property (nonatomic, readonly) NSString *authorId; + +// convenience method for expiring a message which was just read +- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSIncomingMessage.m b/SignalUtilitiesKit/TSIncomingMessage.m new file mode 100644 index 000000000..b8a8b31b3 --- /dev/null +++ b/SignalUtilitiesKit/TSIncomingMessage.m @@ -0,0 +1,180 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSIncomingMessage.h" +#import "NSNotificationCenter+OWS.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import "OWSDisappearingMessagesJob.h" +#import "OWSReadReceiptManager.h" +#import "TSAttachmentPointer.h" +#import "TSContactThread.h" +#import "TSDatabaseSecondaryIndexes.h" +#import "TSGroupThread.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TSIncomingMessage () + +@property (nonatomic, getter=wasRead) BOOL read; + +@property (nonatomic, nullable) NSNumber *serverTimestamp; + +@end + +#pragma mark - + +@implementation TSIncomingMessage + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (_authorId == nil) { + OWSAssertDebug([self.uniqueThreadId hasPrefix:TSContactThreadPrefix]); + _authorId = [TSContactThread contactIdFromThreadId:self.uniqueThreadId]; + } + + return self; +} + +- (instancetype)initIncomingMessageWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview + serverTimestamp:(nullable NSNumber *)serverTimestamp + wasReceivedByUD:(BOOL)wasReceivedByUD +{ + self = [super initMessageWithTimestamp:timestamp + inThread:thread + messageBody:body + attachmentIds:attachmentIds + expiresInSeconds:expiresInSeconds + expireStartedAt:0 + quotedMessage:quotedMessage + contactShare:contactShare + linkPreview:linkPreview]; + + if (!self) { + return self; + } + + _authorId = authorId; + _sourceDeviceId = sourceDeviceId; + _read = NO; + _serverTimestamp = serverTimestamp; + _wasReceivedByUD = wasReceivedByUD; + + return self; +} + ++ (nullable instancetype)findMessageWithAuthorId:(NSString *)authorId + timestamp:(uint64_t)timestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + __block TSIncomingMessage *foundMessage; + // In theory we could build a new secondaryIndex for (authorId,timestamp), but in practice there should + // be *very* few (millisecond) timestamps with multiple authors. + [TSDatabaseSecondaryIndexes + enumerateMessagesWithTimestamp:timestamp + withBlock:^(NSString *collection, NSString *key, BOOL *stop) { + TSInteraction *interaction = + [TSInteraction fetchObjectWithUniqueID:key transaction:transaction]; + if ([interaction isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *message = (TSIncomingMessage *)interaction; + + OWSAssertDebug(message.authorId > 0); + + if ([message.authorId isEqualToString:authorId]) { + foundMessage = message; + } + } + } + usingTransaction:transaction]; + + return foundMessage; +} + + +- (OWSInteractionType)interactionType +{ + return OWSInteractionType_IncomingMessage; +} + +- (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + for (NSString *attachmentId in self.attachmentIds) { + TSAttachment *_Nullable attachment = + [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; + if ([attachment isKindOfClass:[TSAttachmentPointer class]]) { + return NO; + } + } + return self.isExpiringMessage; +} + +#pragma mark - OWSReadTracking + +- (BOOL)shouldAffectUnreadCounts +{ + return YES; +} + +- (void)markAsReadNowWithSendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; +{ + [self markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] + sendReadReceipt:sendReadReceipt + transaction:transaction]; +} + +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction; +{ + OWSAssertDebug(transaction); + + if (_read && readTimestamp >= self.expireStartedAt) { + return; + } + + NSTimeInterval secondsAgoRead = ((NSTimeInterval)[NSDate ows_millisecondTimeStamp] - (NSTimeInterval)readTimestamp) / 1000; + OWSLogDebug(@"marking uniqueId: %@ which has timestamp: %llu as read: %f seconds ago", + self.uniqueId, + self.timestamp, + secondsAgoRead); + _read = YES; + [self saveWithTransaction:transaction]; + + [transaction addCompletionQueue:nil + completionBlock:^{ + [[NSNotificationCenter defaultCenter] + postNotificationNameAsync:kIncomingMessageMarkedAsReadNotification + object:self]; + }]; + + [[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:self + expirationStartedAt:readTimestamp + transaction:transaction]; + + if (sendReadReceipt) { + [OWSReadReceiptManager.sharedManager messageWasReadLocally:self]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInfoMessage.h b/SignalUtilitiesKit/TSInfoMessage.h new file mode 100644 index 000000000..26c42e819 --- /dev/null +++ b/SignalUtilitiesKit/TSInfoMessage.h @@ -0,0 +1,69 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "OWSReadTracking.h" +#import "TSMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface TSInfoMessage : TSMessage + +typedef NS_ENUM(NSInteger, TSInfoMessageType) { + TSInfoMessageTypeSessionDidEnd, + TSInfoMessageUserNotRegistered, + // TSInfoMessageTypeUnsupportedMessage appears to be obsolete. + TSInfoMessageTypeUnsupportedMessage, + TSInfoMessageTypeGroupUpdate, + TSInfoMessageTypeGroupQuit, + TSInfoMessageTypeDisappearingMessagesUpdate, + TSInfoMessageAddToContactsOffer, + TSInfoMessageVerificationStateChange, + TSInfoMessageAddUserToProfileWhitelistOffer, + TSInfoMessageAddGroupToProfileWhitelistOffer, + TSInfoMessageTypeLokiSessionResetInProgress, + TSInfoMessageTypeLokiSessionResetDone, +}; + ++ (instancetype)userNotRegisteredMessageInThread:(TSThread *)thread recipientId:(NSString *)recipientId; + +@property (atomic, readonly) TSInfoMessageType messageType; +@property (atomic, readonly, nullable) NSString *customMessage; +@property (atomic, readonly, nullable) NSString *unregisteredRecipientId; + +- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contact + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)contact + messageType:(TSInfoMessageType)infoMessage NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + messageType:(TSInfoMessageType)infoMessage + customMessage:(NSString *)customMessage; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + messageType:(TSInfoMessageType)infoMessage + unregisteredRecipientId:(NSString *)unregisteredRecipientId; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInfoMessage.m b/SignalUtilitiesKit/TSInfoMessage.m new file mode 100644 index 000000000..3a7d5be6e --- /dev/null +++ b/SignalUtilitiesKit/TSInfoMessage.m @@ -0,0 +1,194 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSInfoMessage.h" +#import "ContactsManagerProtocol.h" +#import "SSKEnvironment.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSUInteger TSInfoMessageSchemaVersion = 1; + +@interface TSInfoMessage () + +@property (nonatomic, getter=wasRead) BOOL read; + +@property (nonatomic, readonly) NSUInteger infoMessageSchemaVersion; + +@end + +#pragma mark - + +@implementation TSInfoMessage + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (self.infoMessageSchemaVersion < 1) { + _read = YES; + } + + _infoMessageSchemaVersion = TSInfoMessageSchemaVersion; + + if (self.isDynamicInteraction) { + self.read = YES; + } + + return self; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + messageType:(TSInfoMessageType)infoMessage +{ + // MJK TODO - remove senderTimestamp + self = [super initMessageWithTimestamp:timestamp + inThread:thread + messageBody:nil + attachmentIds:@[] + expiresInSeconds:0 + expireStartedAt:0 + quotedMessage:nil + contactShare:nil + linkPreview:nil]; + + if (!self) { + return self; + } + + _messageType = infoMessage; + _infoMessageSchemaVersion = TSInfoMessageSchemaVersion; + + if (self.isDynamicInteraction) { + self.read = YES; + } + + return self; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + messageType:(TSInfoMessageType)infoMessage + customMessage:(NSString *)customMessage +{ + self = [self initWithTimestamp:timestamp inThread:thread messageType:infoMessage]; + if (self) { + _customMessage = customMessage; + } + return self; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + messageType:(TSInfoMessageType)infoMessage + unregisteredRecipientId:(NSString *)unregisteredRecipientId +{ + self = [self initWithTimestamp:timestamp inThread:thread messageType:infoMessage]; + if (self) { + _unregisteredRecipientId = unregisteredRecipientId; + } + return self; +} + ++ (instancetype)userNotRegisteredMessageInThread:(TSThread *)thread recipientId:(NSString *)recipientId +{ + OWSAssertDebug(thread); + OWSAssertDebug(recipientId); + + // MJK TODO - remove senderTimestamp + return [[self alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageType:TSInfoMessageUserNotRegistered + unregisteredRecipientId:recipientId]; +} + +- (OWSInteractionType)interactionType +{ + return OWSInteractionType_Info; +} + +- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + switch (_messageType) { + case TSInfoMessageTypeLokiSessionResetInProgress: + return NSLocalizedString(@"Secure session reset in progress", nil); + case TSInfoMessageTypeLokiSessionResetDone: + return NSLocalizedString(@"Secure session reset done", nil); + case TSInfoMessageTypeSessionDidEnd: + return NSLocalizedString(@"SECURE_SESSION_RESET", nil); + case TSInfoMessageTypeUnsupportedMessage: + return NSLocalizedString(@"UNSUPPORTED_ATTACHMENT", nil); + case TSInfoMessageUserNotRegistered: + if (self.unregisteredRecipientId.length > 0) { + id contactsManager = SSKEnvironment.shared.contactsManager; + NSString *recipientName = [contactsManager displayNameForPhoneIdentifier:self.unregisteredRecipientId + transaction:transaction]; + return [NSString stringWithFormat:NSLocalizedString(@"ERROR_UNREGISTERED_USER_FORMAT", + @"Format string for 'unregistered user' error. Embeds {{the " + @"unregistered user's name or signal id}}."), + recipientName]; + } else { + return NSLocalizedString(@"CONTACT_DETAIL_COMM_TYPE_INSECURE", nil); + } + case TSInfoMessageTypeGroupQuit: + return NSLocalizedString(@"GROUP_YOU_LEFT", nil); + case TSInfoMessageTypeGroupUpdate: + return _customMessage != nil ? _customMessage : NSLocalizedString(@"GROUP_UPDATED", nil); + case TSInfoMessageAddToContactsOffer: + return NSLocalizedString(@"ADD_TO_CONTACTS_OFFER", + @"Message shown in conversation view that offers to add an unknown user to your phone's contacts."); + case TSInfoMessageVerificationStateChange: + return NSLocalizedString(@"VERIFICATION_STATE_CHANGE_GENERIC", + @"Generic message indicating that verification state changed for a given user."); + case TSInfoMessageAddUserToProfileWhitelistOffer: + return NSLocalizedString(@"ADD_USER_TO_PROFILE_WHITELIST_OFFER", + @"Message shown in conversation view that offers to share your profile with a user."); + case TSInfoMessageAddGroupToProfileWhitelistOffer: + return NSLocalizedString(@"ADD_GROUP_TO_PROFILE_WHITELIST_OFFER", + @"Message shown in conversation view that offers to share your profile with a group."); + default: + break; + } + + return @"Unknown Info Message Type"; +} + +#pragma mark - OWSReadTracking + +- (BOOL)shouldAffectUnreadCounts +{ + return NO; +} + +- (uint64_t)expireStartedAt +{ + return 0; +} + +- (void)markAsReadAtTimestamp:(uint64_t)readTimestamp + sendReadReceipt:(BOOL)sendReadReceipt + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + if (_read) { + return; + } + + OWSLogDebug(@"marking as read uniqueId: %@ which has timestamp: %llu", self.uniqueId, self.timestamp); + _read = YES; + [self saveWithTransaction:transaction]; + + // Ignore sendReadReceipt, it doesn't apply to info messages. +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInteraction.h b/SignalUtilitiesKit/TSInteraction.h new file mode 100644 index 000000000..2d230db56 --- /dev/null +++ b/SignalUtilitiesKit/TSInteraction.h @@ -0,0 +1,88 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSThread; + +typedef NS_ENUM(NSInteger, OWSInteractionType) { + OWSInteractionType_Unknown, + OWSInteractionType_IncomingMessage, + OWSInteractionType_OutgoingMessage, + OWSInteractionType_Error, + OWSInteractionType_Call, + OWSInteractionType_Info, + OWSInteractionType_Offer, + OWSInteractionType_TypingIndicator, +}; + +NSString *NSStringFromOWSInteractionType(OWSInteractionType value); + +@protocol OWSPreviewText + +- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction; + +@end + +@interface TSInteraction : TSYapDatabaseObject + +- (instancetype)initInteractionWithUniqueId:(NSString *)uniqueId + timestamp:(uint64_t)timestamp + inThread:(TSThread *)thread; +- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread; + +@property (nonatomic, readonly) NSString *uniqueThreadId; +@property (nonatomic, readonly) TSThread *thread; +@property (nonatomic, readonly) uint64_t timestamp; +@property (nonatomic, readonly) uint64_t sortId; +@property (nonatomic, readonly) uint64_t receivedAtTimestamp; +@property (nonatomic, readonly) BOOL shouldUseServerTime; +/// Used for public chats where a message sent from a slave device is interpreted as having been sent from the master device. +@property (nonatomic) NSString *actualSenderHexEncodedPublicKey; + +- (void)setServerTimestampToReceivedTimestamp:(uint64_t)receivedAtTimestamp; + +- (uint64_t)timestampForUI; + +- (NSDate *)receivedAtDate; + +- (OWSInteractionType)interactionType; + +- (TSThread *)threadWithTransaction:(YapDatabaseReadTransaction *)transaction; + +/** + * When an interaction is updated, it often affects the UI for it's containing thread. Touching it's thread will notify + * any observers so they can redraw any related UI. + */ +- (void)touchThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark Utility Method + ++ (NSArray *)interactionsWithTimestamp:(uint64_t)timestamp + ofClass:(Class)clazz + withTransaction:(YapDatabaseReadTransaction *)transaction; + ++ (NSArray *)interactionsWithTimestamp:(uint64_t)timestamp + filter:(BOOL (^_Nonnull)(TSInteraction *))filter + withTransaction:(YapDatabaseReadTransaction *)transaction; + +- (uint64_t)timestampForLegacySorting; +- (NSComparisonResult)compareForSorting:(TSInteraction *)other; + +// "Dynamic" interactions are not messages or static events (like +// info messages, error messages, etc.). They are interactions +// created, updated and deleted by the views. +// +// These include block offers, "add to contact" offers, +// unseen message indicators, etc. +- (BOOL)isDynamicInteraction; + +- (void)saveNextSortIdWithTransaction:(YapDatabaseReadWriteTransaction *)transaction + NS_SWIFT_NAME(saveNextSortId(transaction:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInteraction.m b/SignalUtilitiesKit/TSInteraction.m new file mode 100644 index 000000000..725d169ca --- /dev/null +++ b/SignalUtilitiesKit/TSInteraction.m @@ -0,0 +1,292 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSInteraction.h" +#import "TSDatabaseSecondaryIndexes.h" +#import "TSThread.h" +#import "TSGroupThread.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *NSStringFromOWSInteractionType(OWSInteractionType value) +{ + switch (value) { + case OWSInteractionType_Unknown: + return @"OWSInteractionType_Unknown"; + case OWSInteractionType_IncomingMessage: + return @"OWSInteractionType_IncomingMessage"; + case OWSInteractionType_OutgoingMessage: + return @"OWSInteractionType_OutgoingMessage"; + case OWSInteractionType_Error: + return @"OWSInteractionType_Error"; + case OWSInteractionType_Call: + return @"OWSInteractionType_Call"; + case OWSInteractionType_Info: + return @"OWSInteractionType_Info"; + case OWSInteractionType_Offer: + return @"OWSInteractionType_Offer"; + case OWSInteractionType_TypingIndicator: + return @"OWSInteractionType_TypingIndicator"; + } +} + +@interface TSInteraction () + +@property (nonatomic) uint64_t sortId; + +@end + +@implementation TSInteraction + +@synthesize timestamp = _timestamp; + ++ (NSArray *)interactionsWithTimestamp:(uint64_t)timestamp + ofClass:(Class)clazz + withTransaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(timestamp > 0); + + // Accept any interaction. + return [self interactionsWithTimestamp:timestamp + filter:^(TSInteraction *interaction) { + return [interaction isKindOfClass:clazz]; + } + withTransaction:transaction]; +} + ++ (NSArray *)interactionsWithTimestamp:(uint64_t)timestamp + filter:(BOOL (^_Nonnull)(TSInteraction *))filter + withTransaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(timestamp > 0); + + NSMutableArray *interactions = [NSMutableArray new]; + + [TSDatabaseSecondaryIndexes + enumerateMessagesWithTimestamp:timestamp + withBlock:^(NSString *collection, NSString *key, BOOL *stop) { + TSInteraction *interaction = + [TSInteraction fetchObjectWithUniqueID:key transaction:transaction]; + if (!filter(interaction)) { + return; + } + [interactions addObject:interaction]; + } + usingTransaction:transaction]; + + return [interactions copy]; +} + ++ (NSString *)collection { + return @"TSInteraction"; +} + +- (instancetype)initInteractionWithUniqueId:(NSString *)uniqueId + timestamp:(uint64_t)timestamp + inThread:(TSThread *)thread +{ + OWSAssertDebug(timestamp > 0); + + self = [super initWithUniqueId:uniqueId]; + + if (!self) { + return self; + } + + _timestamp = timestamp; + _uniqueThreadId = thread.uniqueId; + + return self; +} + +- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread +{ + OWSAssertDebug(timestamp > 0); + + self = [super initWithUniqueId:[[NSUUID UUID] UUIDString]]; + + if (!self) { + return self; + } + + _timestamp = timestamp; + _uniqueThreadId = thread.uniqueId; + _receivedAtTimestamp = [NSDate ows_millisecondTimeStamp]; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return nil; + } + + // Previously the receivedAtTimestamp field lived on TSMessage, but we've moved it up + // to the TSInteraction superclass. + if (_receivedAtTimestamp == 0) { + // Upgrade from the older "TSMessage.receivedAtDate" and "TSMessage.receivedAt" properties if + // necessary. + NSDate *receivedAtDate = [coder decodeObjectForKey:@"receivedAtDate"]; + if (!receivedAtDate) { + receivedAtDate = [coder decodeObjectForKey:@"receivedAt"]; + } + + if (receivedAtDate) { + _receivedAtTimestamp = [NSDate ows_millisecondsSince1970ForDate:receivedAtDate]; + } + + // For TSInteractions which are not TSMessage's, the timestamp *is* the receivedAtTimestamp + if (_receivedAtTimestamp == 0) { + _receivedAtTimestamp = _timestamp; + } + } + + return self; +} + +#pragma mark Thread + +- (TSThread *)thread +{ + return [TSThread fetchObjectWithUniqueID:self.uniqueThreadId]; +} + +- (TSThread *)threadWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [TSThread fetchObjectWithUniqueID:self.uniqueThreadId transaction:transaction]; +} + +- (void)touchThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [transaction touchObjectForKey:self.uniqueThreadId inCollection:[TSThread collection]]; +} + +- (void)applyChangeToSelfAndLatestCopy:(YapDatabaseReadWriteTransaction *)transaction + changeBlock:(void (^)(id))changeBlock +{ + OWSAssertDebug(transaction); + + [super applyChangeToSelfAndLatestCopy:transaction changeBlock:changeBlock]; + [self touchThreadWithTransaction:transaction]; +} + +#pragma mark Date operations + +- (uint64_t)timestampForUI +{ + if (_shouldUseServerTime) { + return _receivedAtTimestamp; + } + return _timestamp; +} + +- (uint64_t)timestampForLegacySorting +{ + return self.timestamp; +} + +- (void)setServerTimestampToReceivedTimestamp:(uint64_t)receivedAtTimestamp +{ + _shouldUseServerTime = YES; + _receivedAtTimestamp = receivedAtTimestamp; +} + +- (NSDate *)receivedAtDate +{ + return [NSDate ows_dateWithMillisecondsSince1970:self.receivedAtTimestamp]; +} + +- (NSComparisonResult)compareForSorting:(TSInteraction *)other +{ + OWSAssertDebug(other); + + // Sort the messages by the sender's timestamp (Signal uses sortId) + uint64_t sortId1 = self.timestamp; + uint64_t sortId2 = other.timestamp; + + // In open groups messages should be sorted by their server timestamp. `sortId` represents the order in which messages + // were processed. Since in the open group poller we sort messages by their server timestamp, sorting by `sortId` is + // effectively the same as sorting by server timestamp. + if (self.thread.isGroupThread) { + TSGroupThread *thread = (TSGroupThread *)self.thread; + if (thread.isPublicChat) { + sortId1 = self.sortId; + sortId2 = other.sortId; + } + } + + if (sortId1 > sortId2) { + return NSOrderedDescending; + } else if (sortId1 < sortId2) { + return NSOrderedAscending; + } else { + return NSOrderedSame; + } +} + +- (OWSInteractionType)interactionType +{ + OWSFailDebug(@"unknown interaction type."); + + return OWSInteractionType_Unknown; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"%@ in thread: %@ timestamp: %lu", + [super description], + self.uniqueThreadId, + (unsigned long)self.timestamp]; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!self.uniqueId) { + OWSFailDebug(@"Missing uniqueId."); + self.uniqueId = [NSUUID new].UUIDString; + } + if (self.sortId == 0) { + self.sortId = [SSKIncrementingIdFinder nextIdWithKey:[TSInteraction collection] transaction:transaction]; + } + + [super saveWithTransaction:transaction]; + + TSThread *fetchedThread = [self threadWithTransaction:transaction]; + + [fetchedThread updateWithLastMessage:self transaction:transaction]; +} + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [super removeWithTransaction:transaction]; + + [self touchThreadWithTransaction:transaction]; +} + +- (BOOL)isDynamicInteraction +{ + return NO; +} + +#pragma mark - sorting migration + +- (void)saveNextSortIdWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (self.sortId != 0) { + // This could happen if something else in our startup process saved the interaction + // e.g. another migration ran. + // During the migration, since we're enumerating the interactions in the proper order, + // we want to ignore any previously assigned sortId + self.sortId = 0; + } + [self saveWithTransaction:transaction]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInvalidIdentityKeyErrorMessage.h b/SignalUtilitiesKit/TSInvalidIdentityKeyErrorMessage.h new file mode 100644 index 000000000..2f4f5ff08 --- /dev/null +++ b/SignalUtilitiesKit/TSInvalidIdentityKeyErrorMessage.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSErrorMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSFingerprint; + +@interface TSInvalidIdentityKeyErrorMessage : TSErrorMessage + +- (void)throws_acceptNewIdentityKey NS_SWIFT_UNAVAILABLE("throws objc exceptions"); +- (nullable NSData *)throws_newIdentityKey NS_SWIFT_UNAVAILABLE("throws objc exceptions"); +- (NSString *)theirSignalId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInvalidIdentityKeyErrorMessage.m b/SignalUtilitiesKit/TSInvalidIdentityKeyErrorMessage.m new file mode 100644 index 000000000..028466323 --- /dev/null +++ b/SignalUtilitiesKit/TSInvalidIdentityKeyErrorMessage.m @@ -0,0 +1,31 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInvalidIdentityKeyErrorMessage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation TSInvalidIdentityKeyErrorMessage + +- (void)throws_acceptNewIdentityKey +{ + OWSAbstractMethod(); +} + +- (nullable NSData *)throws_newIdentityKey +{ + OWSAbstractMethod(); + return nil; +} + +- (NSString *)theirSignalId +{ + OWSAbstractMethod(); + return nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInvalidIdentityKeyReceivingErrorMessage.h b/SignalUtilitiesKit/TSInvalidIdentityKeyReceivingErrorMessage.h new file mode 100644 index 000000000..9bb4fe456 --- /dev/null +++ b/SignalUtilitiesKit/TSInvalidIdentityKeyReceivingErrorMessage.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInvalidIdentityKeyErrorMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoEnvelope; + +// DEPRECATED - we no longer create new instances of this class (as of mid-2017); However, existing instances may +// exist, so we should keep this class around to honor their old behavior. +__attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage : TSInvalidIdentityKeyErrorMessage + +#ifdef DEBUG ++ (nullable instancetype)untrustedKeyWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInvalidIdentityKeyReceivingErrorMessage.m b/SignalUtilitiesKit/TSInvalidIdentityKeyReceivingErrorMessage.m new file mode 100644 index 000000000..caf7ccbdb --- /dev/null +++ b/SignalUtilitiesKit/TSInvalidIdentityKeyReceivingErrorMessage.m @@ -0,0 +1,154 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInvalidIdentityKeyReceivingErrorMessage.h" +#import "OWSFingerprint.h" +#import "OWSIdentityManager.h" +#import "OWSMessageManager.h" +#import "OWSMessageReceiver.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSPrimaryStorage.h" +#import "SSKEnvironment.h" +#import "TSContactThread.h" +#import "TSDatabaseView.h" +#import "TSErrorMessage_privateConstructor.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +__attribute__((deprecated)) @interface TSInvalidIdentityKeyReceivingErrorMessage() + +@property (nonatomic, readonly, copy) NSString *authorId; + +@end + +@implementation TSInvalidIdentityKeyReceivingErrorMessage { + // Not using a property declaration in order to exclude from DB serialization + SSKProtoEnvelope *_Nullable _envelope; +} + +@synthesize envelopeData = _envelopeData; + +#ifdef DEBUG +// We no longer create these messages, but they might exist on legacy clients so it's useful to be able to +// create them with the debug UI ++ (nullable instancetype)untrustedKeyWithEnvelope:(SSKProtoEnvelope *)envelope + withTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + TSContactThread *contactThread = + [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction]; + + // Legit usage of senderTimestamp, references message which failed to decrypt + TSInvalidIdentityKeyReceivingErrorMessage *errorMessage = + [[self alloc] initForUnknownIdentityKeyWithTimestamp:envelope.timestamp + inThread:contactThread + incomingEnvelope:envelope]; + return errorMessage; +} + +- (nullable instancetype)initForUnknownIdentityKeyWithTimestamp:(uint64_t)timestamp + inThread:(TSThread *)thread + incomingEnvelope:(SSKProtoEnvelope *)envelope +{ + self = [self initWithTimestamp:timestamp inThread:thread failedMessageType:TSErrorMessageWrongTrustedIdentityKey]; + if (!self) { + return self; + } + + NSError *error; + _envelopeData = [envelope serializedDataAndReturnError:&error]; + if (!_envelopeData || error != nil) { + OWSFailDebug(@"failure: envelope data failed with error: %@", error); + return nil; + } + + _authorId = envelope.source; + + return self; +} +#endif + +- (nullable SSKProtoEnvelope *)envelope +{ + if (!_envelope) { + NSError *error; + SSKProtoEnvelope *_Nullable envelope = [SSKProtoEnvelope parseData:self.envelopeData error:&error]; + if (error || envelope == nil) { + OWSFailDebug(@"Could not parse proto: %@", error); + } else { + _envelope = envelope; + } + } + return _envelope; +} + +- (void)throws_acceptNewIdentityKey +{ + OWSAssertIsOnMainThread(); + + if (self.errorType != TSErrorMessageWrongTrustedIdentityKey) { + OWSLogError(@"Refusing to accept identity key for anything but a Key error."); + return; + } + + NSData *_Nullable newKey = [self throws_newIdentityKey]; + if (!newKey) { + OWSFailDebug(@"Couldn't extract identity key to accept"); + return; + } + + [[OWSIdentityManager sharedManager] saveRemoteIdentity:newKey recipientId:self.envelope.source]; + + // Decrypt this and any old messages for the newly accepted key + NSArray *messagesToDecrypt = + [self.thread receivedMessagesForInvalidKey:newKey]; + + for (TSInvalidIdentityKeyReceivingErrorMessage *errorMessage in messagesToDecrypt) { + [SSKEnvironment.shared.messageReceiver handleReceivedEnvelopeData:errorMessage.envelopeData]; + + // Here we remove the existing error message because handleReceivedEnvelope will either + // 1.) succeed and create a new successful message in the thread or... + // 2.) fail and create a new identical error message in the thread. + [errorMessage remove]; + } +} + +- (nullable NSData *)throws_newIdentityKey +{ + if (!self.envelope) { + OWSLogError(@"Error message had no envelope data to extract key from"); + return nil; + } + + if (self.envelope.type != SSKProtoEnvelopeTypePrekeyBundle) { + OWSLogError(@"Refusing to attempt key extraction from an envelope which isn't a prekey bundle"); + return nil; + } + + NSData *pkwmData = self.envelope.content; + if (!pkwmData) { + OWSLogError(@"Ignoring acceptNewIdentityKey for empty message"); + return nil; + } + + PreKeyWhisperMessage *message = [[PreKeyWhisperMessage alloc] init_throws_withData:pkwmData]; + return [message.identityKey throws_removeKeyType]; +} + +- (NSString *)theirSignalId +{ + if (self.authorId) { + return self.authorId; + } else { + // for existing messages before we were storing author id. + return self.envelope.source; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInvalidIdentityKeySendingErrorMessage.h b/SignalUtilitiesKit/TSInvalidIdentityKeySendingErrorMessage.h new file mode 100644 index 000000000..8b86e43c7 --- /dev/null +++ b/SignalUtilitiesKit/TSInvalidIdentityKeySendingErrorMessage.h @@ -0,0 +1,24 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInvalidIdentityKeyErrorMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@class PreKeyBundle; +@class TSOutgoingMessage; +@class TSThread; + +extern NSString *TSInvalidPreKeyBundleKey; +extern NSString *TSInvalidRecipientKey; + +// DEPRECATED - we no longer create new instances of this class (as of mid-2017); However, existing instances may +// exist, so we should keep this class around to honor their old behavior. +__attribute__((deprecated)) @interface TSInvalidIdentityKeySendingErrorMessage : TSInvalidIdentityKeyErrorMessage + +@property (nonatomic, readonly) NSString *messageId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSInvalidIdentityKeySendingErrorMessage.m b/SignalUtilitiesKit/TSInvalidIdentityKeySendingErrorMessage.m new file mode 100644 index 000000000..78379f6d2 --- /dev/null +++ b/SignalUtilitiesKit/TSInvalidIdentityKeySendingErrorMessage.m @@ -0,0 +1,61 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSInvalidIdentityKeySendingErrorMessage.h" +#import "OWSFingerprint.h" +#import "OWSIdentityManager.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSPrimaryStorage.h" +#import "PreKeyBundle+jsonDict.h" +#import "TSContactThread.h" +#import "TSErrorMessage_privateConstructor.h" +#import "TSOutgoingMessage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *TSInvalidPreKeyBundleKey = @"TSInvalidPreKeyBundleKey"; +NSString *TSInvalidRecipientKey = @"TSInvalidRecipientKey"; + +@interface TSInvalidIdentityKeySendingErrorMessage () + +@property (nonatomic, readonly) PreKeyBundle *preKeyBundle; + +@end + +// DEPRECATED - we no longer create new instances of this class (as of mid-2017); However, existing instances may +// exist, so we should keep this class around to honor their old behavior. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +@implementation TSInvalidIdentityKeySendingErrorMessage +#pragma clang diagnostic pop + +- (void)throws_acceptNewIdentityKey +{ + // Shouldn't really get here, since we're no longer creating blocking SN changes. + // But there may still be some old unaccepted SN errors in the wild that need to be accepted. + OWSFailDebug(@"accepting new identity key is deprecated."); + + NSData *_Nullable newIdentityKey = [self throws_newIdentityKey]; + if (!newIdentityKey) { + OWSFailDebug(@"newIdentityKey is unexpectedly nil. Bad Prekey bundle?: %@", self.preKeyBundle); + return; + } + + [[OWSIdentityManager sharedManager] saveRemoteIdentity:newIdentityKey recipientId:self.recipientId]; +} + +- (nullable NSData *)throws_newIdentityKey +{ + return [self.preKeyBundle.identityKey throws_removeKeyType]; +} + +- (NSString *)theirSignalId +{ + return self.recipientId; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSMessage.h b/SignalUtilitiesKit/TSMessage.h new file mode 100644 index 000000000..e4085c094 --- /dev/null +++ b/SignalUtilitiesKit/TSMessage.h @@ -0,0 +1,84 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSInteraction.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Abstract message class. + */ + +@class OWSContact; +@class OWSLinkPreview; +@class TSAttachment; +@class TSAttachmentStream; +@class TSQuotedMessage; +@class YapDatabaseReadWriteTransaction; + +@interface TSMessage : TSInteraction + +@property (nonatomic, readonly) NSMutableArray *attachmentIds; +@property (nonatomic, readonly, nullable) NSString *body; +@property (nonatomic, readonly) uint32_t expiresInSeconds; +@property (nonatomic, readonly) uint64_t expireStartedAt; +@property (nonatomic, readonly) uint64_t expiresAt; +@property (nonatomic, readonly) BOOL isExpiringMessage; +@property (nonatomic, readonly, nullable) TSQuotedMessage *quotedMessage; +@property (nonatomic, readonly, nullable) OWSContact *contactShare; +@property (nonatomic, nullable) OWSLinkPreview *linkPreview; +@property BOOL skipSave; +// P2P +@property (nonatomic) BOOL isP2P; +// Open groups +@property (nonatomic) uint64_t openGroupServerMessageID; +@property (nonatomic, readonly) BOOL isOpenGroupMessage; + +- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; + +- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (BOOL)hasAttachments; +- (NSArray *)attachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (NSArray *)mediaAttachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (nullable TSAttachment *)oversizeTextAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (void)addAttachmentWithID:(NSString *)attachmentID in:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)removeAttachment:(TSAttachment *)attachment + transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(removeAttachment(_:transaction:)); + +// Returns ids for all attachments, including message ("body") attachments, +// quoted reply thumbnails, contact share avatars, link preview images, etc. +- (NSArray *)allAttachmentIds; + +- (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream; + +- (nullable NSString *)oversizeTextWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (nullable NSString *)bodyTextWithTransaction:(YapDatabaseReadTransaction *)transaction; + +- (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction; + +#pragma mark - Update With... Methods + +- (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark - Open Groups + +- (void)saveOpenGroupServerMessageID:(uint64_t)serverMessageID in:(YapDatabaseReadWriteTransaction *_Nullable)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSMessage.m b/SignalUtilitiesKit/TSMessage.m new file mode 100644 index 000000000..6e00eb845 --- /dev/null +++ b/SignalUtilitiesKit/TSMessage.m @@ -0,0 +1,466 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSMessage.h" +#import "AppContext.h" +#import "MIMETypeUtil.h" +#import "NSString+SSK.h" +#import "OWSContact.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import "TSAttachment.h" +#import "TSAttachmentStream.h" +#import "TSQuotedMessage.h" +#import "TSThread.h" +#import +#import +#import +#import +#import "OWSPrimaryStorage+Loki.h" +#import "TSContactThread.h" + +NS_ASSUME_NONNULL_BEGIN + +static const NSUInteger OWSMessageSchemaVersion = 4; + +#pragma mark - + +@interface TSMessage () + +@property (nonatomic, nullable) NSString *body; +@property (nonatomic) uint32_t expiresInSeconds; +@property (nonatomic) uint64_t expireStartedAt; + +/** + * The version of the model class's schema last used to serialize this model. Use this to manage data migrations during + * object de/serialization. + * + * e.g. + * + * - (id)initWithCoder:(NSCoder *)coder + * { + * self = [super initWithCoder:coder]; + * if (!self) { return self; } + * if (_schemaVersion < 2) { + * _newName = [coder decodeObjectForKey:@"oldName"] + * } + * ... + * _schemaVersion = 2; + * } + */ +@property (nonatomic, readonly) NSUInteger schemaVersion; + +@end + +#pragma mark - + +@implementation TSMessage + +- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview +{ + self = [super initInteractionWithTimestamp:timestamp inThread:thread]; + + if (!self) { + return self; + } + + _schemaVersion = OWSMessageSchemaVersion; + + _body = body; + _attachmentIds = attachmentIds ? [attachmentIds mutableCopy] : [NSMutableArray new]; + _expiresInSeconds = expiresInSeconds; + _expireStartedAt = expireStartedAt; + [self updateExpiresAt]; + _quotedMessage = quotedMessage; + _contactShare = contactShare; + _linkPreview = linkPreview; + _openGroupServerMessageID = -1; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (_schemaVersion < 2) { + // renamed _attachments to _attachmentIds + if (!_attachmentIds) { + _attachmentIds = [coder decodeObjectForKey:@"attachments"]; + } + } + + if (_schemaVersion < 3) { + _expiresInSeconds = 0; + _expireStartedAt = 0; + _expiresAt = 0; + } + + if (_schemaVersion < 4) { + // Wipe out the body field on these legacy attachment messages. + // + // Explantion: Historically, a message sent from iOS could be an attachment XOR a text message, + // but now we support sending an attachment+caption as a single message. + // + // Other clients have supported sending attachment+caption in a single message for a long time. + // So the way we used to handle receiving them was to make it look like they'd sent two messages: + // first the attachment+caption (we'd ignore this caption when rendering), followed by a separate + // message with just the caption (which we'd render as a simple independent text message), for + // which we'd offset the timestamp by a little bit to get the desired ordering. + // + // Now that we can properly render an attachment+caption message together, these legacy "dummy" text + // messages are not only unnecessary, but worse, would be rendered redundantly. For safety, rather + // than building the logic to try to find and delete the redundant "dummy" text messages which users + // have been seeing and interacting with, we delete the body field from the attachment message, + // which iOS users have never seen directly. + if (_attachmentIds.count > 0) { + _body = nil; + } + } + + if (!_attachmentIds) { + _attachmentIds = [NSMutableArray new]; + } + + _schemaVersion = OWSMessageSchemaVersion; + + return self; +} + +- (void)setExpiresInSeconds:(uint32_t)expiresInSeconds +{ + uint32_t maxExpirationDuration = [OWSDisappearingMessagesConfiguration maxDurationSeconds]; + if (expiresInSeconds > maxExpirationDuration) { + OWSFailDebug(@"using `maxExpirationDuration` instead of: %u", maxExpirationDuration); + } + + _expiresInSeconds = MIN(expiresInSeconds, maxExpirationDuration); + [self updateExpiresAt]; +} + +- (void)setExpireStartedAt:(uint64_t)expireStartedAt +{ + if (_expireStartedAt != 0 && _expireStartedAt < expireStartedAt) { + OWSLogDebug(@"ignoring later startedAt time"); + return; + } + + uint64_t now = [NSDate ows_millisecondTimeStamp]; + if (expireStartedAt > now) { + OWSLogWarn(@"using `now` instead of future time"); + } + + _expireStartedAt = MIN(now, expireStartedAt); + [self updateExpiresAt]; +} + +- (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return self.isExpiringMessage; +} + +// TODO a downloaded media doesn't start counting until download is complete. +- (void)updateExpiresAt +{ + if (_expiresInSeconds > 0 && _expireStartedAt > 0) { + _expiresAt = _expireStartedAt + _expiresInSeconds * 1000; + } else { + _expiresAt = 0; + } +} + +- (BOOL)hasAttachments +{ + return self.attachmentIds ? (self.attachmentIds.count > 0) : NO; +} + +- (NSArray *)allAttachmentIds +{ + NSMutableArray *result = [NSMutableArray new]; + if (self.attachmentIds.count > 0) { + [result addObjectsFromArray:self.attachmentIds]; + } + + if (self.quotedMessage) { + [result addObjectsFromArray:self.quotedMessage.thumbnailAttachmentStreamIds]; + } + + if (self.contactShare.avatarAttachmentId) { + [result addObject:self.contactShare.avatarAttachmentId]; + } + + if (self.linkPreview.imageAttachmentId) { + [result addObject:self.linkPreview.imageAttachmentId]; + } + + return [result copy]; +} + +- (NSArray *)attachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSMutableArray *attachments = [NSMutableArray new]; + for (NSString *attachmentId in self.attachmentIds) { + TSAttachment *_Nullable attachment = + [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; + if (attachment) { + [attachments addObject:attachment]; + } + } + return [attachments copy]; +} + +- (NSArray *)attachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction + contentType:(NSString *)contentType +{ + NSArray *attachments = [self attachmentsWithTransaction:transaction]; + return [attachments filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(TSAttachment *evaluatedObject, + NSDictionary *_Nullable bindings) { + return [evaluatedObject.contentType isEqualToString:contentType]; + }]]; +} + +- (NSArray *)attachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction + exceptContentType:(NSString *)contentType +{ + NSArray *attachments = [self attachmentsWithTransaction:transaction]; + return [attachments filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(TSAttachment *evaluatedObject, + NSDictionary *_Nullable bindings) { + return ![evaluatedObject.contentType isEqualToString:contentType]; + }]]; +} + +- (void)removeAttachment:(TSAttachment *)attachment transaction:(YapDatabaseReadWriteTransaction *)transaction; +{ + OWSAssertDebug([self.attachmentIds containsObject:attachment.uniqueId]); + [attachment removeWithTransaction:transaction]; + + [self.attachmentIds removeObject:attachment.uniqueId]; + + [self saveWithTransaction:transaction]; +} + +- (void)addAttachmentWithID:(NSString *)attachmentID in:(YapDatabaseReadWriteTransaction *)transaction { + if (!self.attachmentIds) { return; } + [self.attachmentIds addObject:attachmentID]; + [self saveWithTransaction:transaction]; +} + +- (NSString *)debugDescription +{ + if ([self hasAttachments] && self.body.length > 0) { + NSString *attachmentId = self.attachmentIds[0]; + return [NSString + stringWithFormat:@"Media Message with attachmentId: %@ and caption: '%@'", attachmentId, self.body]; + } else if ([self hasAttachments]) { + NSString *attachmentId = self.attachmentIds[0]; + return [NSString stringWithFormat:@"Media Message with attachmentId: %@", attachmentId]; + } else { + return [NSString stringWithFormat:@"%@ with body: %@", [self class], self.body]; + } +} + +- (nullable TSAttachment *)oversizeTextAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self attachmentsWithTransaction:transaction contentType:OWSMimeTypeOversizeTextMessage].firstObject; +} + +- (NSArray *)mediaAttachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [self attachmentsWithTransaction:transaction exceptContentType:OWSMimeTypeOversizeTextMessage]; +} + +- (nullable NSString *)oversizeTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + TSAttachment *_Nullable attachment = [self oversizeTextAttachmentWithTransaction:transaction]; + if (!attachment) { + return nil; + } + + if (![attachment isKindOfClass:TSAttachmentStream.class]) { + return nil; + } + + TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; + + NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.originalFilePath]; + if (!data) { + OWSFailDebug(@"Can't load oversize text data."); + return nil; + } + NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (!text) { + OWSFailDebug(@"Can't parse oversize text data."); + return nil; + } + return text.filterStringForDisplay; +} + +- (nullable NSString *)bodyTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSString *_Nullable oversizeText = [self oversizeTextWithTransaction:transaction]; + if (oversizeText) { + return oversizeText; + } + + if (self.body.length > 0) { + return self.body.filterStringForDisplay; + } + + return nil; +} + +// TODO: This method contains view-specific logic and probably belongs in NotificationsManager, not in SSK. +- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSString *_Nullable bodyDescription = nil; + if (self.body.length > 0) { + bodyDescription = self.body; + } + + if (bodyDescription == nil) { + TSAttachment *_Nullable oversizeTextAttachment = [self oversizeTextAttachmentWithTransaction:transaction]; + if ([oversizeTextAttachment isKindOfClass:[TSAttachmentStream class]]) { + TSAttachmentStream *oversizeTextAttachmentStream = (TSAttachmentStream *)oversizeTextAttachment; + NSData *_Nullable data = [NSData dataWithContentsOfFile:oversizeTextAttachmentStream.originalFilePath]; + if (data) { + NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (text) { + bodyDescription = text.filterStringForDisplay; + } + } + } + } + + NSString *_Nullable attachmentDescription = nil; + TSAttachment *_Nullable mediaAttachment = [self mediaAttachmentsWithTransaction:transaction].firstObject; + if (mediaAttachment != nil) { + attachmentDescription = mediaAttachment.description; + } + + if (attachmentDescription.length > 0 && bodyDescription.length > 0) { + // Attachment with caption. + if ([CurrentAppContext() isRTL]) { + return [[bodyDescription stringByAppendingString:@": "] stringByAppendingString:attachmentDescription]; + } else { + return [[attachmentDescription stringByAppendingString:@": "] stringByAppendingString:bodyDescription]; + } + } else if (bodyDescription.length > 0) { + return bodyDescription; + } else if (attachmentDescription.length > 0) { + return attachmentDescription; + } else if (self.contactShare) { + if (CurrentAppContext().isRTL) { + return [self.contactShare.name.displayName stringByAppendingString:@" 👤"]; + } else { + return [@"👤 " stringByAppendingString:self.contactShare.name.displayName]; + } + } else { + // TODO: We should do better here. + return @""; + } +} + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [super removeWithTransaction:transaction]; + + for (NSString *attachmentId in self.allAttachmentIds) { + // We need to fetch each attachment, since [TSAttachment removeWithTransaction:] does important work. + TSAttachment *_Nullable attachment = + [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; + if (!attachment) { + OWSFailDebug(@"couldn't load interaction's attachment for deletion."); + continue; + } + [attachment removeWithTransaction:transaction]; + }; +} + +- (BOOL)isExpiringMessage +{ + return self.expiresInSeconds > 0; +} + +- (uint64_t)timestampForLegacySorting +{ + if ([self shouldUseReceiptDateForSorting] && self.receivedAtTimestamp > 0) { + return self.receivedAtTimestamp; + } else { + OWSAssertDebug(self.timestamp > 0); + return self.timestamp; + } +} + +- (BOOL)shouldUseReceiptDateForSorting +{ + return YES; +} + +- (nullable NSString *)body +{ + return _body.filterStringForDisplay; +} + +- (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream +{ + OWSAssertDebug([attachmentStream isKindOfClass:[TSAttachmentStream class]]); + OWSAssertDebug(self.quotedMessage); + OWSAssertDebug(self.quotedMessage.quotedAttachments.count == 1); + + [self.quotedMessage setThumbnailAttachmentStream:attachmentStream]; +} + +#pragma mark - Update With... Methods + +- (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(expireStartedAt > 0); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSMessage *message) { + [message setExpireStartedAt:expireStartedAt]; + }]; +} + +- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(linkPreview); + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSMessage *message) { + [message setLinkPreview:linkPreview]; + }]; +} + +#pragma mark - Open Groups + +- (BOOL)isOpenGroupMessage { + return self.openGroupServerMessageID > 0; +} + +- (void)saveOpenGroupServerMessageID:(uint64_t)serverMessageID in:(YapDatabaseReadWriteTransaction *_Nullable)transaction { + self.openGroupServerMessageID = serverMessageID; + if (transaction == nil) { + [self save]; + [self.dbReadWriteConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{}]; + } else { + [self saveWithTransaction:transaction]; + [transaction.connection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{}]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSNetworkManager.h b/SignalUtilitiesKit/TSNetworkManager.h new file mode 100644 index 000000000..0dda2495a --- /dev/null +++ b/SignalUtilitiesKit/TSNetworkManager.h @@ -0,0 +1,45 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +extern NSErrorDomain const TSNetworkManagerErrorDomain; +typedef NS_ERROR_ENUM(TSNetworkManagerErrorDomain, TSNetworkManagerError){ + // It's a shame to use 0 as an enum value for anything other than something like default or unknown, because it's + // indistinguishable from "not set" in Objc. + // However this value was existing behavior for connectivity errors, and since we might be using this in other + // places I didn't want to change it out of hand + TSNetworkManagerErrorFailedConnection = 0, + // Other TSNetworkManagerError's use HTTP status codes (e.g. 404, etc) +}; + +BOOL IsNSErrorNetworkFailure(NSError *_Nullable error); + +typedef void (^TSNetworkManagerSuccess)(NSURLSessionDataTask *task, _Nullable id responseObject); +typedef void (^TSNetworkManagerFailure)(NSURLSessionDataTask *task, NSError *error); + +@class TSRequest; + +@interface TSNetworkManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initDefault; + ++ (instancetype)sharedManager; + +- (void)makeRequest:(TSRequest *)request + success:(TSNetworkManagerSuccess)success + failure:(TSNetworkManagerFailure)failure NS_SWIFT_NAME(makeRequest(_:success:failure:)); + +- (void)makeRequest:(TSRequest *)request + completionQueue:(dispatch_queue_t)completionQueue + success:(TSNetworkManagerSuccess)success + failure:(TSNetworkManagerFailure)failure NS_SWIFT_NAME(makeRequest(_:completionQueue:success:failure:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSNetworkManager.m b/SignalUtilitiesKit/TSNetworkManager.m new file mode 100644 index 000000000..a257cbc1d --- /dev/null +++ b/SignalUtilitiesKit/TSNetworkManager.m @@ -0,0 +1,583 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSNetworkManager.h" +#import "AppContext.h" +#import "NSError+messageSending.h" +#import "NSURLSessionDataTask+StatusCode.h" +#import "OWSError.h" +#import "OWSQueues.h" +#import "OWSSignalService.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSRequest.h" +#import +#import +#import +#import "SSKAsserts.h" + +NS_ASSUME_NONNULL_BEGIN + +NSErrorDomain const TSNetworkManagerErrorDomain = @"SignalServiceKit.TSNetworkManager"; + +BOOL IsNSErrorNetworkFailure(NSError *_Nullable error) +{ + return ([error.domain isEqualToString:TSNetworkManagerErrorDomain] + && error.code == TSNetworkManagerErrorFailedConnection); +} + +dispatch_queue_t NetworkManagerQueue() +{ + static dispatch_queue_t serialQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + serialQueue = dispatch_queue_create("org.whispersystems.networkManager", DISPATCH_QUEUE_SERIAL); + }); + return serialQueue; +} + +#pragma mark - + +@interface OWSSessionManager : NSObject + +@property (nonatomic, readonly) AFHTTPSessionManager *sessionManager; +@property (nonatomic, readonly) NSDictionary *defaultHeaders; + +@end + +#pragma mark - + +@implementation OWSSessionManager + +#pragma mark - Dependencies + +- (OWSSignalService *)signalService +{ + return [OWSSignalService sharedInstance]; +} + +#pragma mark - + +- (instancetype)init +{ + AssertOnDispatchQueue(NetworkManagerQueue()); + + self = [super init]; + if (!self) { + return self; + } + + _sessionManager = [self.signalService buildSignalServiceSessionManager]; + self.sessionManager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + // NOTE: We could enable HTTPShouldUsePipelining here. + // Make a copy of the default headers for this session manager. + _defaultHeaders = [self.sessionManager.requestSerializer.HTTPRequestHeaders copy]; + + return self; +} + +// TSNetworkManager.serialQueue +- (void)performRequest:(TSRequest *)request + canUseAuth:(BOOL)canUseAuth + success:(TSNetworkManagerSuccess)success + failure:(TSNetworkManagerFailure)failure +{ + AssertOnDispatchQueue(NetworkManagerQueue()); + OWSAssertDebug(request); + OWSAssertDebug(success); + OWSAssertDebug(failure); + + // Clear all headers so that we don't retain headers from previous requests. + for (NSString *headerField in self.sessionManager.requestSerializer.HTTPRequestHeaders.allKeys.copy) { + [self.sessionManager.requestSerializer setValue:nil forHTTPHeaderField:headerField]; + } + + // Apply the default headers for this session manager. + for (NSString *headerField in self.defaultHeaders) { + NSString *headerValue = self.defaultHeaders[headerField]; + [self.sessionManager.requestSerializer setValue:headerValue forHTTPHeaderField:headerField]; + } + +// if (canUseAuth && request.shouldHaveAuthorizationHeaders) { +// [self.sessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:request.authUsername +// password:request.authPassword]; +// } + + // Honor the request's headers. + for (NSString *headerField in request.allHTTPHeaderFields) { + NSString *headerValue = request.allHTTPHeaderFields[headerField]; + [self.sessionManager.requestSerializer setValue:headerValue forHTTPHeaderField:headerField]; + } + + self.sessionManager.requestSerializer.timeoutInterval = request.timeoutInterval; + + if ([request.HTTPMethod isEqualToString:@"GET"]) { + [self.sessionManager GET:request.URL.absoluteString + parameters:request.parameters + progress:nil + success:success + failure:failure]; + } else if ([request.HTTPMethod isEqualToString:@"POST"]) { + [self.sessionManager POST:request.URL.absoluteString + parameters:request.parameters + progress:nil + success:success + failure:failure]; + } else if ([request.HTTPMethod isEqualToString:@"PUT"]) { + [self.sessionManager PUT:request.URL.absoluteString + parameters:request.parameters + success:success + failure:failure]; + } else if ([request.HTTPMethod isEqualToString:@"DELETE"]) { + [self.sessionManager DELETE:request.URL.absoluteString + parameters:request.parameters + success:success + failure:failure]; + } else if ([request.HTTPMethod isEqualToString:@"PATCH"]) { + [self.sessionManager PATCH:request.URL.absoluteString + parameters:request.parameters + success:success + failure:failure]; + } else { + OWSLogError(@"Trying to perform HTTP operation with unknown verb: %@", request.HTTPMethod); + } +} + +@end + +#pragma mark - + +// You might be asking: "why use a pool at all? We're only using the session manager +// on the serial queue, so can't we just have two session managers (1 UD, 1 non-UD) +// that we use for all requests?" +// +// That assumes that the session managers are not stateful in a way where concurrent +// requests can interfere with each other. I audited the AFNetworking codebase and my +// reading is that sessions managers are safe to use in that way - that the state of +// their properties (e.g. header values) is only used when building the request and +// can be safely changed after performRequest is complete. +// +// But I decided that I didn't want to (silently) bake that assumption into the +// codebase, since the stakes are high. The session managers aren't expensive. IMO +// better to use a pool and not re-use a session manager until its request succeeds +// or fails. +@interface OWSSessionManagerPool : NSObject + +@property (nonatomic) NSMutableArray *pool; + +@end + +#pragma mark - + +@implementation OWSSessionManagerPool + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + self.pool = [NSMutableArray new]; + + return self; +} + +- (OWSSessionManager *)get +{ + AssertOnDispatchQueue(NetworkManagerQueue()); + + OWSSessionManager *_Nullable sessionManager = [self.pool lastObject]; + if (sessionManager) { + [self.pool removeLastObject]; + } else { + sessionManager = [OWSSessionManager new]; + } + OWSAssertDebug(sessionManager); + return sessionManager; +} + +- (void)returnToPool:(OWSSessionManager *)sessionManager +{ + AssertOnDispatchQueue(NetworkManagerQueue()); + + OWSAssertDebug(sessionManager); + const NSUInteger kMaxPoolSize = 3; + if (self.pool.count >= kMaxPoolSize) { + // Discard + return; + } + [self.pool addObject:sessionManager]; +} + +@end + +#pragma mark - + +@interface TSNetworkManager () + +// These properties should only be accessed on serialQueue. +@property (atomic, readonly) OWSSessionManagerPool *udSessionManagerPool; +@property (atomic, readonly) OWSSessionManagerPool *nonUdSessionManagerPool; + +@end + +#pragma mark - + +@implementation TSNetworkManager + +#pragma mark - Dependencies + ++ (TSAccountManager *)tsAccountManager +{ + return TSAccountManager.sharedInstance; +} + +#pragma mark - Singleton + ++ (instancetype)sharedManager +{ + OWSAssertDebug(SSKEnvironment.shared.networkManager); + + return SSKEnvironment.shared.networkManager; +} + +- (instancetype)initDefault +{ + self = [super init]; + if (!self) { + return self; + } + + _udSessionManagerPool = [OWSSessionManagerPool new]; + _nonUdSessionManagerPool = [OWSSessionManagerPool new]; + + OWSSingletonAssert(); + + return self; +} + +#pragma mark Manager Methods + +- (void)makeRequest:(TSRequest *)request + success:(TSNetworkManagerSuccess)success + failure:(TSNetworkManagerFailure)failure +{ + return [self makeRequest:request completionQueue:dispatch_get_main_queue() success:success failure:failure]; +} + +- (void)makeRequest:(TSRequest *)request + completionQueue:(dispatch_queue_t)completionQueue + success:(TSNetworkManagerSuccess)success + failure:(TSNetworkManagerFailure)failure +{ + OWSAssertDebug(request); + OWSAssertDebug(success); + OWSAssertDebug(failure); + + dispatch_async(NetworkManagerQueue(), ^{ + [self makeRequestSync:request completionQueue:completionQueue success:success failure:failure]; + }); +} + +- (void)makeRequestSync:(TSRequest *)request + completionQueue:(dispatch_queue_t)completionQueue + success:(TSNetworkManagerSuccess)successParam + failure:(TSNetworkManagerFailure)failureParam +{ + OWSAssertDebug(request); + OWSAssertDebug(successParam); + OWSAssertDebug(failureParam); + +// BOOL isUDRequest = request.isUDRequest; + NSString *label = @"UD request"; +// BOOL canUseAuth = !isUDRequest; +// if (isUDRequest) { +// OWSAssert(!request.shouldHaveAuthorizationHeaders); +// } +// OWSLogInfo(@"Making %@: %@", label, request); + + OWSSessionManagerPool *sessionManagerPool = self.udSessionManagerPool; + OWSSessionManager *sessionManager = [sessionManagerPool get]; + + TSNetworkManagerSuccess success = ^(NSURLSessionDataTask *task, _Nullable id responseObject) { + dispatch_async(NetworkManagerQueue(), ^{ + [sessionManagerPool returnToPool:sessionManager]; + }); + + dispatch_async(completionQueue, ^{ + OWSLogInfo(@"%@ succeeded : %@", label, request); + +// if (canUseAuth && request.shouldHaveAuthorizationHeaders) { +// [TSNetworkManager.tsAccountManager setIsDeregistered:NO]; +// } + + successParam(task, responseObject); + + [OutageDetection.sharedManager reportConnectionSuccess]; + }); + }; + TSNetworkManagerFailure failure = ^(NSURLSessionDataTask *task, NSError *error) { + dispatch_async(NetworkManagerQueue(), ^{ + [sessionManagerPool returnToPool:sessionManager]; + }); + + [TSNetworkManager + handleNetworkFailure:^(NSURLSessionDataTask *task, NSError *error) { + dispatch_async(completionQueue, ^{ + failureParam(task, error); + }); + } + request:request + task:task + error:error]; + }; + + [sessionManager performRequest:request canUseAuth:NO success:success failure:failure]; +} + +#ifdef DEBUG ++ (void)logCurlForTask:(NSURLSessionDataTask *)task +{ + NSMutableArray *curlComponents = [NSMutableArray new]; + [curlComponents addObject:@"curl"]; + // Verbose + [curlComponents addObject:@"-v"]; + // Insecure + [curlComponents addObject:@"-k"]; + // Method, e.g. GET + [curlComponents addObject:@"-X"]; + [curlComponents addObject:task.originalRequest.HTTPMethod]; + // Headers + for (NSString *header in task.originalRequest.allHTTPHeaderFields) { + NSString *headerValue = task.originalRequest.allHTTPHeaderFields[header]; + // We don't yet support escaping header values. + // If these asserts trip, we'll need to add that. + OWSAssertDebug([header rangeOfString:@"'"].location == NSNotFound); + OWSAssertDebug([headerValue rangeOfString:@"'"].location == NSNotFound); + + [curlComponents addObject:@"-H"]; + [curlComponents addObject:[NSString stringWithFormat:@"'%@: %@'", header, headerValue]]; + } + // Body/parameters (e.g. JSON payload) + if (task.originalRequest.HTTPBody) { + NSString *jsonBody = + [[NSString alloc] initWithData:task.originalRequest.HTTPBody encoding:NSUTF8StringEncoding]; + // We don't yet support escaping JSON. + // If these asserts trip, we'll need to add that. + OWSAssertDebug([jsonBody rangeOfString:@"'"].location == NSNotFound); + [curlComponents addObject:@"--data-ascii"]; + [curlComponents addObject:[NSString stringWithFormat:@"'%@'", jsonBody]]; + } + // TODO: Add support for cookies. + [curlComponents addObject:task.originalRequest.URL.absoluteString]; + NSString *curlCommand = [curlComponents componentsJoinedByString:@" "]; + OWSLogVerbose(@"curl for failed request: %@", curlCommand); +} +#endif + ++ (void)handleNetworkFailure:(TSNetworkManagerFailure)failureBlock + request:(TSRequest *)request + task:(NSURLSessionDataTask *)task + error:(NSError *)networkError +{ + OWSAssertDebug(failureBlock); + OWSAssertDebug(request); + OWSAssertDebug(networkError); + + NSInteger statusCode = [task statusCode]; + +#ifdef DEBUG + [TSNetworkManager logCurlForTask:task]; +#endif + + [OutageDetection.sharedManager reportConnectionFailure]; + + NSError *error = [self errorWithHTTPCode:statusCode + description:nil + failureReason:nil + recoverySuggestion:nil + fallbackError:networkError]; + + switch (statusCode) { + case 0: { + NSError *connectivityError = + [self errorWithHTTPCode:TSNetworkManagerErrorFailedConnection + description:NSLocalizedString(@"ERROR_DESCRIPTION_NO_INTERNET", + @"Generic error used whenever Signal can't contact the server") + failureReason:networkError.localizedFailureReason + recoverySuggestion:NSLocalizedString(@"NETWORK_ERROR_RECOVERY", nil) + fallbackError:networkError]; + connectivityError.isRetryable = YES; + + OWSLogWarn(@"The network request failed because of a connectivity error: %@", request); + failureBlock(task, connectivityError); + break; + } + case 400: { + OWSLogError(@"The request contains an invalid parameter : %@, %@", networkError.debugDescription, request); + + error.isRetryable = NO; + + failureBlock(task, error); + break; + } + case 401: { + OWSLogError(@"The server returned an error about the authorization header: %@, %@", + networkError.debugDescription, + request); + error.isRetryable = NO; + [self deregisterAfterAuthErrorIfNecessary:task request:request statusCode:statusCode]; + failureBlock(task, error); + break; + } + case 403: { + OWSLogError( + @"The server returned an authentication failure: %@, %@", networkError.debugDescription, request); + error.isRetryable = NO; + [self deregisterAfterAuthErrorIfNecessary:task request:request statusCode:statusCode]; + failureBlock(task, error); + break; + } + case 404: { + OWSLogError(@"The requested resource could not be found: %@, %@", networkError.debugDescription, request); + error.isRetryable = NO; + failureBlock(task, error); + break; + } + case 411: { + OWSLogInfo(@"Multi-device pairing: %ld, %@, %@", (long)statusCode, networkError.debugDescription, request); + NSError *customError = [self errorWithHTTPCode:statusCode + description:NSLocalizedString(@"MULTIDEVICE_PAIRING_MAX_DESC", + @"alert title: cannot link - reached max linked devices") + failureReason:networkError.localizedFailureReason + recoverySuggestion:NSLocalizedString(@"MULTIDEVICE_PAIRING_MAX_RECOVERY", + @"alert body: cannot link - reached max linked devices") + fallbackError:networkError]; + customError.isRetryable = NO; + failureBlock(task, customError); + break; + } + case 413: { + OWSLogWarn(@"Rate limit exceeded: %@", request); + NSError *customError = [self errorWithHTTPCode:statusCode + description:NSLocalizedString(@"REGISTER_RATE_LIMITING_ERROR", nil) + failureReason:networkError.localizedFailureReason + recoverySuggestion:NSLocalizedString(@"REGISTER_RATE_LIMITING_BODY", nil) + fallbackError:networkError]; + customError.isRetryable = NO; + failureBlock(task, customError); + break; + } + case 417: { + // TODO: Is this response code obsolete? + OWSLogWarn(@"The number is already registered on a relay. Please unregister there first: %@", request); + NSError *customError = [self errorWithHTTPCode:statusCode + description:NSLocalizedString(@"REGISTRATION_ERROR", nil) + failureReason:networkError.localizedFailureReason + recoverySuggestion:NSLocalizedString(@"RELAY_REGISTERED_ERROR_RECOVERY", nil) + fallbackError:networkError]; + customError.isRetryable = NO; + failureBlock(task, customError); + break; + } + case 422: { + OWSLogError(@"The registration was requested over an unknown transport: %@, %@", + networkError.debugDescription, + request); + error.isRetryable = NO; + failureBlock(task, error); + break; + } + default: { + OWSLogWarn(@"Unknown error: %ld, %@, %@", (long)statusCode, networkError.debugDescription, request); + error.isRetryable = NO; + failureBlock(task, error); + break; + } + } +} + ++ (void)deregisterAfterAuthErrorIfNecessary:(NSURLSessionDataTask *)task + request:(TSRequest *)request + statusCode:(NSInteger)statusCode { + /* Loki: Original code + * We don't care about invalid auth + * ======== + + OWSLogVerbose(@"Invalid auth: %@", task.originalRequest.allHTTPHeaderFields); + + // We only want to de-register for: + // + // * Auth errors... + // * ...received from Signal service... + // * ...that used standard authorization. + // + // * We don't want want to deregister for: + // + // * CDS requests. + // * Requests using UD auth. + // * etc. + if ([task.originalRequest.URL.absoluteString hasPrefix:textSecureServerURL] + && request.shouldHaveAuthorizationHeaders) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.tsAccountManager.isRegisteredAndReady) { + [self.tsAccountManager setIsDeregistered:YES]; + } else { + OWSLogWarn( + @"Ignoring auth failure; not registered and ready: %@.", task.originalRequest.URL.absoluteString); + } + }); + } else { + OWSLogWarn(@"Ignoring %d for URL: %@", (int)statusCode, task.originalRequest.URL.absoluteString); + } + + * ======== + */ +} + ++ (NSError *)errorWithHTTPCode:(NSInteger)code + description:(nullable NSString *)description + failureReason:(nullable NSString *)failureReason + recoverySuggestion:(nullable NSString *)recoverySuggestion + fallbackError:(NSError *)fallbackError +{ + OWSAssertDebug(fallbackError); + + if (!description) { + description = fallbackError.localizedDescription; + } + if (!failureReason) { + failureReason = fallbackError.localizedFailureReason; + } + if (!recoverySuggestion) { + recoverySuggestion = fallbackError.localizedRecoverySuggestion; + } + + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + + if (description) { + [dict setObject:description forKey:NSLocalizedDescriptionKey]; + } + if (failureReason) { + [dict setObject:failureReason forKey:NSLocalizedFailureReasonErrorKey]; + } + if (recoverySuggestion) { + [dict setObject:recoverySuggestion forKey:NSLocalizedRecoverySuggestionErrorKey]; + } + + NSData *failureData = fallbackError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; + + if (failureData) { + [dict setObject:failureData forKey:AFNetworkingOperationFailingURLResponseDataErrorKey]; + } + + dict[NSUnderlyingErrorKey] = fallbackError; + + return [NSError errorWithDomain:TSNetworkManagerErrorDomain code:code userInfo:dict]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSOutgoingMessage.h b/SignalUtilitiesKit/TSOutgoingMessage.h new file mode 100644 index 000000000..8ea563e7b --- /dev/null +++ b/SignalUtilitiesKit/TSOutgoingMessage.h @@ -0,0 +1,265 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +// Feature flag. +// +// TODO: Remove. +BOOL AreRecipientUpdatesEnabled(void); + +typedef NS_ENUM(NSInteger, TSOutgoingMessageState) { + // The message is either: + // a) Enqueued for sending. + // b) Waiting on attachment upload(s). + // c) Being sent to the service. + TSOutgoingMessageStateSending, + // The failure state. + TSOutgoingMessageStateFailed, + // These two enum values have been combined into TSOutgoingMessageStateSent. + TSOutgoingMessageStateSent_OBSOLETE, + TSOutgoingMessageStateDelivered_OBSOLETE, + // The message has been sent to the service. + TSOutgoingMessageStateSent, +}; + +NSString *NSStringForOutgoingMessageState(TSOutgoingMessageState value); + +typedef NS_ENUM(NSInteger, OWSOutgoingMessageRecipientState) { + // Message could not be sent to recipient. + OWSOutgoingMessageRecipientStateFailed = 0, + // Message is being sent to the recipient (enqueued, uploading or sending). + OWSOutgoingMessageRecipientStateSending, + // The message was not sent because the recipient is not valid. + // For example, this recipient may have left the group. + OWSOutgoingMessageRecipientStateSkipped, + // The message has been sent to the service. It may also have been delivered or read. + OWSOutgoingMessageRecipientStateSent, + + OWSOutgoingMessageRecipientStateMin = OWSOutgoingMessageRecipientStateFailed, + OWSOutgoingMessageRecipientStateMax = OWSOutgoingMessageRecipientStateSent, +}; + +NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientState value); + +typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { + TSGroupMetaMessageUnspecified, + TSGroupMetaMessageNew, + TSGroupMetaMessageUpdate, + TSGroupMetaMessageDeliver, + TSGroupMetaMessageQuit, + TSGroupMetaMessageRequestInfo, +}; + +@class SSKProtoAttachmentPointer; +@class SSKProtoContentBuilder; +@class SSKProtoDataMessage; +@class SSKProtoDataMessageBuilder; +@class SignalRecipient; + +@interface TSOutgoingMessageRecipientState : MTLModel + +@property (atomic, readonly) OWSOutgoingMessageRecipientState state; +// This property should only be set if state == .sent. +@property (atomic, nullable, readonly) NSNumber *deliveryTimestamp; +// This property should only be set if state == .sent. +@property (atomic, nullable, readonly) NSNumber *readTimestamp; + +@property (atomic, readonly) BOOL wasSentByUD; + +@end + +#pragma mark - + +@interface TSOutgoingMessage : TSMessage + +- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_UNAVAILABLE; + +// MJK TODO - Can we remove the sender timestamp param? +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentId:(nullable NSString *)attachmentId; + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentId:(nullable NSString *)attachmentId + expiresInSeconds:(uint32_t)expiresInSeconds; + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentId:(nullable NSString *)attachmentId + expiresInSeconds:(uint32_t)expiresInSeconds + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + linkPreview:(nullable OWSLinkPreview *)linkPreview; + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + expiresInSeconds:(uint32_t)expiresInSeconds; + +@property (readonly) TSOutgoingMessageState messageState; +@property (readonly) BOOL wasDeliveredToAnyRecipient; +@property (readonly) BOOL wasSentToAnyRecipient; + +@property (atomic, readonly) BOOL hasSyncedTranscript; +@property (atomic, readonly) NSString *customMessage; +@property (atomic, readonly) NSString *mostRecentFailureText; +// A map of attachment id-to-"source" filename. +@property (nonatomic, readonly) NSMutableDictionary *attachmentFilenameMap; + +@property (atomic, readonly) TSGroupMetaMessage groupMetaMessage; + +@property (nonatomic, readonly) BOOL isVoiceMessage; + +// This property won't be accurate for legacy messages. +@property (atomic, readonly) BOOL isFromLinkedDevice; + +@property (nonatomic, readonly) BOOL isSilent; + +@property (nonatomic, readonly) BOOL isOnline; + +/// Loki: Whether proof of work is being calculated for this message. +@property (atomic, readonly) BOOL isCalculatingPoW; + +/// Loki: Time to live for the message in milliseconds. +@property (nonatomic, readonly) uint ttl; + +/** + * The data representation of this message, to be encrypted, before being sent. + */ +- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient; + +/** + * Intermediate protobuf representation + * Subclasses can augment if they want to manipulate the data message before building. + */ +- (nullable id)dataMessageBuilder; + +- (nullable SSKProtoDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId; + +/** + * Allows subclasses to supply a custom content builder that has already prepared part of the message. + */ +- (nullable id)prepareCustomContentBuilder:(SignalRecipient *)recipient; + +/** + * Should this message be synced to the users other registered devices? This is + * generally always true, except in the case of the sync messages themseleves + * (so we don't end up in an infinite loop). + */ +- (BOOL)shouldSyncTranscript; + +- (BOOL)shouldBeSaved; + +// All recipients of this message. +- (NSArray *)recipientIds; + +// All recipients of this message who we are currently trying to send to (queued, uploading or during send). +- (NSArray *)sendingRecipientIds; + +// All recipients of this message to whom it has been sent (and possibly delivered or read). +- (NSArray *)sentRecipientIds; + +// All recipients of this message to whom it has been sent and delivered (and possibly read). +- (NSArray *)deliveredRecipientIds; + +// All recipients of this message to whom it has been sent, delivered and read. +- (NSArray *)readRecipientIds; + +// Number of recipients of this message to whom it has been sent. +- (NSUInteger)sentRecipientsCount; + +- (nullable TSOutgoingMessageRecipientState *)recipientStateForRecipientId:(NSString *)recipientId; + +#pragma mark - Update With... Methods + +// When sending a message, when proof of work calculation is started, we should mark it as such +- (void)saveIsCalculatingProofOfWork:(BOOL)isCalculatingPoW withTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to record a successful send to one recipient. +- (void)updateWithSentRecipient:(NSString *)recipientId + wasSentByUD:(BOOL)wasSentByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to record a skipped send to one recipient. +- (void)updateWithSkippedRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// On app launch, all "sending" recipients should be marked as "failed". +- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(YapDatabaseReadWriteTransaction *)transaction; + +// When we start a message send, all "failed" recipients should be marked as "sending". +- (void)updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to forge the message state for fake messages. +// +// NOTE: This method should only be used by Debug UI, etc. +- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to record a failed send to all "sending" recipients. +- (void)updateWithSendingError:(NSError *)error + transaction:(YapDatabaseReadWriteTransaction *)transaction + NS_SWIFT_NAME(update(sendingError:transaction:)); + +- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript + transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateWithCustomMessage:(NSString *)customMessage; + +// This method is used to record a successful delivery to one recipient. +// +// deliveryTimestamp is an optional parameter, since legacy +// delivery receipts don't have a "delivery timestamp". Those +// messages repurpose the "timestamp" field to indicate when the +// corresponding message was originally sent. +- (void)updateWithDeliveredRecipient:(NSString *)recipientId + deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)updateWithWasSentFromLinkedDeviceWithUDRecipientIds:(nullable NSArray *)udRecipientIds + nonUdRecipientIds:(nullable NSArray *)nonUdRecipientIds + isSentUpdate:(BOOL)isSentUpdate + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to rewrite the recipient list with a single recipient. +// It is used to reply to a "group info request", which should only be +// delivered to the requestor. +- (void)updateWithSendingToSingleGroupRecipient:(NSString *)singleGroupRecipient + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +// This method is used to record a successful "read" by one recipient. +- (void)updateWithReadRecipientId:(NSString *)recipientId + readTimestamp:(uint64_t)readTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (nullable NSNumber *)firstRecipientReadTimestamp; + +- (NSString *)statusDescription; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSOutgoingMessage.m b/SignalUtilitiesKit/TSOutgoingMessage.m new file mode 100644 index 000000000..b8c0bcc7f --- /dev/null +++ b/SignalUtilitiesKit/TSOutgoingMessage.m @@ -0,0 +1,1172 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +@import Foundation; + +#import "TSOutgoingMessage.h" +#import "NSString+SSK.h" +#import "OWSContact.h" +#import "OWSMessageSender.h" +#import "OWSOutgoingSyncMessage.h" +#import "OWSPrimaryStorage.h" +#import "ProfileManagerProtocol.h" +#import "ProtoUtils.h" +#import "SSKEnvironment.h" +#import "SignalRecipient.h" +#import "TSAccountManager.h" +#import "TSAttachmentStream.h" +#import "TSContactThread.h" +#import "TSGroupThread.h" +#import "TSQuotedMessage.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +BOOL AreRecipientUpdatesEnabled(void) +{ + return NO; +} + +NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll"; + +NSString *NSStringForOutgoingMessageState(TSOutgoingMessageState value) +{ + switch (value) { + case TSOutgoingMessageStateSending: + return @"TSOutgoingMessageStateSending"; + case TSOutgoingMessageStateFailed: + return @"TSOutgoingMessageStateFailed"; + case TSOutgoingMessageStateSent_OBSOLETE: + return @"TSOutgoingMessageStateSent_OBSOLETE"; + case TSOutgoingMessageStateDelivered_OBSOLETE: + return @"TSOutgoingMessageStateDelivered_OBSOLETE"; + case TSOutgoingMessageStateSent: + return @"TSOutgoingMessageStateSent"; + } +} + +NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientState value) +{ + switch (value) { + case OWSOutgoingMessageRecipientStateFailed: + return @"OWSOutgoingMessageRecipientStateFailed"; + case OWSOutgoingMessageRecipientStateSending: + return @"OWSOutgoingMessageRecipientStateSending"; + case OWSOutgoingMessageRecipientStateSkipped: + return @"OWSOutgoingMessageRecipientStateSkipped"; + case OWSOutgoingMessageRecipientStateSent: + return @"OWSOutgoingMessageRecipientStateSent"; + } +} + +@interface TSOutgoingMessageRecipientState () + +@property (atomic) OWSOutgoingMessageRecipientState state; +@property (atomic, nullable) NSNumber *deliveryTimestamp; +@property (atomic, nullable) NSNumber *readTimestamp; +@property (atomic) BOOL wasSentByUD; + +@end + +#pragma mark - + +@implementation TSOutgoingMessageRecipientState + +@end + +#pragma mark - + +@interface TSOutgoingMessage () + +@property (atomic) BOOL hasSyncedTranscript; +@property (atomic) NSString *customMessage; +@property (atomic) NSString *mostRecentFailureText; +@property (atomic) BOOL isFromLinkedDevice; +@property (atomic) TSGroupMetaMessage groupMetaMessage; + +@property (nonatomic, readonly) TSOutgoingMessageState legacyMessageState; +@property (nonatomic, readonly) BOOL legacyWasDelivered; +@property (nonatomic, readonly) BOOL hasLegacyMessageState; + +@property (atomic, nullable) NSDictionary *recipientStateMap; + +@property (atomic) BOOL isCalculatingPoW; + +@end + +#pragma mark - + +@implementation TSOutgoingMessage + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + + if (self) { + if (!_attachmentFilenameMap) { + _attachmentFilenameMap = [NSMutableDictionary new]; + } + + if (!self.recipientStateMap) { + [self migrateRecipientStateMapWithCoder:coder]; + OWSAssertDebug(self.recipientStateMap); + } + } + + return self; +} + +- (void)migrateRecipientStateMapWithCoder:(NSCoder *)coder +{ + OWSAssertDebug(!self.recipientStateMap); + OWSAssertDebug(coder); + + // Determine the "overall message state." + TSOutgoingMessageState oldMessageState = TSOutgoingMessageStateFailed; + NSNumber *_Nullable messageStateValue = [coder decodeObjectForKey:@"messageState"]; + if (messageStateValue) { + oldMessageState = (TSOutgoingMessageState)messageStateValue.intValue; + } + _hasLegacyMessageState = YES; + _legacyMessageState = oldMessageState; + + OWSOutgoingMessageRecipientState defaultState; + switch (oldMessageState) { + case TSOutgoingMessageStateFailed: + defaultState = OWSOutgoingMessageRecipientStateFailed; + break; + case TSOutgoingMessageStateSending: + defaultState = OWSOutgoingMessageRecipientStateSending; + break; + case TSOutgoingMessageStateSent: + case TSOutgoingMessageStateSent_OBSOLETE: + case TSOutgoingMessageStateDelivered_OBSOLETE: + // Convert legacy values. + defaultState = OWSOutgoingMessageRecipientStateSent; + break; + } + + // Try to leverage the "per-recipient state." + NSDictionary *_Nullable recipientDeliveryMap = + [coder decodeObjectForKey:@"recipientDeliveryMap"]; + NSDictionary *_Nullable recipientReadMap = [coder decodeObjectForKey:@"recipientReadMap"]; + NSArray *_Nullable sentRecipients = [coder decodeObjectForKey:@"sentRecipients"]; + + NSMutableDictionary *recipientStateMap = [NSMutableDictionary new]; + __block BOOL isGroupThread = NO; + // Our default recipient list is the current thread members. + __block NSArray *recipientIds = @[]; + // To avoid deadlock while migrating these records, we use a dedicated + // migration connection. For legacy records (created more than ~9 months + // before the migration), we need to infer the recipient list for this + // message from the current thread membership. This inference isn't + // always accurate, so not using the same connection for both reads is + // acceptable. + [TSOutgoingMessage.dbMigrationConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + TSThread *thread = [self threadWithTransaction:transaction]; + recipientIds = [thread recipientIdentifiers]; + isGroupThread = [thread isGroupThread]; + }]; + + NSNumber *_Nullable wasDelivered = [coder decodeObjectForKey:@"wasDelivered"]; + _legacyWasDelivered = wasDelivered && wasDelivered.boolValue; + BOOL wasDeliveredToContact = NO; + if (isGroupThread) { + // If we have a `sentRecipients` list, prefer that as it is more accurate. + if (sentRecipients) { + recipientIds = sentRecipients; + } + } else { + // Special-case messages in contact threads; if "was delivered", we know + // it was delivered to the contact. + wasDeliveredToContact = _legacyWasDelivered; + } + + NSString *_Nullable singleGroupRecipient = [coder decodeObjectForKey:@"singleGroupRecipient"]; + if (singleGroupRecipient) { + OWSFailDebug(@"unexpected single group recipient message."); + // If this is a "single group recipient message", treat it as such. + recipientIds = @[ + singleGroupRecipient, + ]; + } + + for (NSString *recipientId in recipientIds) { + TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new]; + + NSNumber *_Nullable readTimestamp = recipientReadMap[recipientId]; + NSNumber *_Nullable deliveryTimestamp = recipientDeliveryMap[recipientId]; + if (readTimestamp) { + // If we have a read timestamp for this recipient, mark it as read. + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.readTimestamp = readTimestamp; + // deliveryTimestamp might be nil here. + recipientState.deliveryTimestamp = deliveryTimestamp; + } else if (deliveryTimestamp) { + // If we have a delivery timestamp for this recipient, mark it as delivered. + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.deliveryTimestamp = deliveryTimestamp; + } else if (wasDeliveredToContact) { + OWSAssertDebug(!isGroupThread); + recipientState.state = OWSOutgoingMessageRecipientStateSent; + // Use message time as an estimate of delivery time. + recipientState.deliveryTimestamp = @(self.timestamp); + } else if ([sentRecipients containsObject:recipientId]) { + // If this recipient is in `sentRecipients`, mark it as sent. + recipientState.state = OWSOutgoingMessageRecipientStateSent; + } else { + // Use the default state for this message. + recipientState.state = defaultState; + } + + recipientStateMap[recipientId] = recipientState; + } + self.recipientStateMap = [recipientStateMap copy]; +} + ++ (YapDatabaseConnection *)dbMigrationConnection +{ + return SSKEnvironment.shared.migrationDBConnection; +} + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentId:(nullable NSString *)attachmentId +{ + return [self outgoingMessageInThread:thread + messageBody:body + attachmentId:attachmentId + expiresInSeconds:0 + quotedMessage:nil + linkPreview:nil]; +} + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentId:(nullable NSString *)attachmentId + expiresInSeconds:(uint32_t)expiresInSeconds +{ + return [self outgoingMessageInThread:thread + messageBody:body + attachmentId:attachmentId + expiresInSeconds:expiresInSeconds + quotedMessage:nil + linkPreview:nil]; +} + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentId:(nullable NSString *)attachmentId + expiresInSeconds:(uint32_t)expiresInSeconds + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + linkPreview:(nullable OWSLinkPreview *)linkPreview +{ + NSMutableArray *attachmentIds = [NSMutableArray new]; + if (attachmentId) { + [attachmentIds addObject:attachmentId]; + } + + // MJK TODO remove SenderTimestamp? + return [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:body + attachmentIds:attachmentIds + expiresInSeconds:expiresInSeconds + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:TSGroupMetaMessageUnspecified + quotedMessage:quotedMessage + contactShare:nil + linkPreview:linkPreview]; +} + ++ (instancetype)outgoingMessageInThread:(nullable TSThread *)thread + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + expiresInSeconds:(uint32_t)expiresInSeconds; +{ + // MJK TODO remove SenderTimestamp? + return [[TSOutgoingMessage alloc] initOutgoingMessageWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:nil + attachmentIds:[NSMutableArray new] + expiresInSeconds:expiresInSeconds + expireStartedAt:0 + isVoiceMessage:NO + groupMetaMessage:groupMetaMessage + quotedMessage:nil + contactShare:nil + linkPreview:nil]; +} + +- (instancetype)initOutgoingMessageWithTimestamp:(uint64_t)timestamp + inThread:(nullable TSThread *)thread + messageBody:(nullable NSString *)body + attachmentIds:(NSMutableArray *)attachmentIds + expiresInSeconds:(uint32_t)expiresInSeconds + expireStartedAt:(uint64_t)expireStartedAt + isVoiceMessage:(BOOL)isVoiceMessage + groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + contactShare:(nullable OWSContact *)contactShare + linkPreview:(nullable OWSLinkPreview *)linkPreview +{ + self = [super initMessageWithTimestamp:timestamp + inThread:thread + messageBody:body + attachmentIds:attachmentIds + expiresInSeconds:expiresInSeconds + expireStartedAt:expireStartedAt + quotedMessage:quotedMessage + contactShare:contactShare + linkPreview:linkPreview]; + if (!self) { + return self; + } + + _hasSyncedTranscript = NO; + _isCalculatingPoW = NO; + + if ([thread isKindOfClass:TSGroupThread.class]) { + // Unless specified, we assume group messages are "Delivery" i.e. normal messages. + if (groupMetaMessage == TSGroupMetaMessageUnspecified) { + _groupMetaMessage = TSGroupMetaMessageDeliver; + } else { + _groupMetaMessage = groupMetaMessage; + } + } else { + OWSAssertDebug(groupMetaMessage == TSGroupMetaMessageUnspecified); + // Specifying a group meta message only makes sense for Group threads + _groupMetaMessage = TSGroupMetaMessageUnspecified; + } + + _isVoiceMessage = isVoiceMessage; + + _attachmentFilenameMap = [NSMutableDictionary new]; + + // New outgoing messages should immediately determine their + // recipient list from current thread state. + NSMutableDictionary *recipientStateMap = [NSMutableDictionary new]; + NSArray *recipientIds; + if ([self isKindOfClass:[OWSOutgoingSyncMessage class]]) { + NSString *_Nullable localNumber = [TSAccountManager localNumber]; + OWSAssertDebug(localNumber); + recipientIds = @[ + localNumber, + ]; + } else { + recipientIds = [thread recipientIdentifiers]; + } + for (NSString *recipientId in recipientIds) { + TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new]; + recipientState.state = OWSOutgoingMessageRecipientStateSending; + recipientStateMap[recipientId] = recipientState; + } + self.recipientStateMap = [recipientStateMap copy]; + + return self; +} + +- (void)dealloc +{ + [self removeTemporaryAttachments]; +} + +// Each message has the responsibility for eagerly cleaning up its attachments. +// Normally this is done in [TSMessage removeWithTransaction], but that doesn't +// apply for "transient", unsaved messages (i.e. shouldBeSaved == NO). These +// messages should clean up their attachments upon deallocation. +- (void)removeTemporaryAttachments +{ + if (self.shouldBeSaved) { + // Message is not transient; no need to clean up attachments. + return; + } + NSArray *_Nullable attachmentIds = self.attachmentIds; + if (attachmentIds.count < 1) { + return; + } + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSString *attachmentId in attachmentIds) { + // We need to fetch each attachment, since [TSAttachment removeWithTransaction:] does important work. + TSAttachment *_Nullable attachment = + [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; + if (!attachment) { + OWSLogError(@"Couldn't load interaction's attachment for deletion."); + continue; + } + [attachment removeWithTransaction:transaction]; + }; + }]; +} + +#pragma mark - + +- (TSOutgoingMessageState)messageState +{ + TSOutgoingMessageState newMessageState = + [TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues]; + if (self.hasLegacyMessageState) { + if (newMessageState == TSOutgoingMessageStateSent || self.legacyMessageState == TSOutgoingMessageStateSent) { + return TSOutgoingMessageStateSent; + } + } + return newMessageState; +} + +- (BOOL)wasDeliveredToAnyRecipient +{ + if ([self deliveredRecipientIds].count > 0) { + return YES; + } + return (self.hasLegacyMessageState && self.legacyWasDelivered && self.messageState == TSOutgoingMessageStateSent); +} + +- (BOOL)wasSentToAnyRecipient +{ + if ([self sentRecipientIds].count > 0) { + return YES; + } + return (self.hasLegacyMessageState && self.messageState == TSOutgoingMessageStateSent); +} + ++ (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray *)recipientStates +{ + OWSAssertDebug(recipientStates); + + // If there are any "sending" recipients, consider this message "sending". + BOOL hasFailed = NO; + for (TSOutgoingMessageRecipientState *recipientState in recipientStates) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + return TSOutgoingMessageStateSending; + } else if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) { + hasFailed = YES; + } + } + + // If there are any "failed" recipients, consider this message "failed". + if (hasFailed) { + return TSOutgoingMessageStateFailed; + } + + // Otherwise, consider the message "sent". + // + // NOTE: This includes messages with no recipients. + return TSOutgoingMessageStateSent; +} + +- (BOOL)shouldBeSaved +{ + if (self.groupMetaMessage == TSGroupMetaMessageDeliver || self.groupMetaMessage == TSGroupMetaMessageUnspecified) { + return YES; + } + + return NO; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if (!self.shouldBeSaved) { + // There's no need to save this message, since it's not displayed to the user. + // + // Should we find a need to save this in the future, we need to exclude any non-serializable properties. + OWSLogDebug(@"Skipping save for transient outgoing message."); + + return; + } + + [super saveWithTransaction:transaction]; +} + +- (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + // It's not clear if we should wait until _all_ recipients have reached "sent or later" + // (which could never occur if one group member is unregistered) or only wait until + // the first recipient has reached "sent or later" (which could cause partially delivered + // messages to expire). For now, we'll do the latter. + // + // TODO: Revisit this decision. + + if (!self.isExpiringMessage) { + return NO; + } else if (self.messageState == TSOutgoingMessageStateSent) { + return YES; + } else { + if (self.expireStartedAt > 0) { + // Our initial migration to populate the recipient state map was incomplete. It's since been + // addressed, but it's possible there are edge cases where a previously sent message would + // no longer be considered sent. + // So here we take extra care not to stop any expiration that had previously started. + // This can also happen under normal cirumstances with an outgoing group message. + OWSLogWarn(@"expiration previously started"); + + return YES; + } + + return NO; + } +} + +- (BOOL)isSilent +{ + return NO; +} + +- (BOOL)isOnline +{ + return NO; +} + +- (OWSInteractionType)interactionType +{ + return OWSInteractionType_OutgoingMessage; +} + +- (NSArray *)recipientIds +{ + return self.recipientStateMap.allKeys; +} + +- (NSArray *)sendingRecipientIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + [result addObject:recipientId]; + } + } + return result; +} + +- (NSArray *)sentRecipientIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + if (recipientState.state == OWSOutgoingMessageRecipientStateSent) { + [result addObject:recipientId]; + } + } + return result; +} + +- (NSArray *)deliveredRecipientIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + if (recipientState.deliveryTimestamp != nil) { + [result addObject:recipientId]; + } + } + return result; +} + +- (NSArray *)readRecipientIds +{ + NSMutableArray *result = [NSMutableArray new]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + if (recipientState.readTimestamp != nil) { + [result addObject:recipientId]; + } + } + return result; +} + +- (NSUInteger)sentRecipientsCount +{ + return [self.recipientStateMap.allValues + filteredArrayUsingPredicate:[NSPredicate + predicateWithBlock:^BOOL(TSOutgoingMessageRecipientState *recipientState, + NSDictionary *_Nullable bindings) { + return recipientState.state == OWSOutgoingMessageRecipientStateSent; + }]] + .count; +} + +- (nullable TSOutgoingMessageRecipientState *)recipientStateForRecipientId:(NSString *)recipientId +{ + OWSAssertDebug(recipientId.length > 0); + + TSOutgoingMessageRecipientState *_Nullable result = self.recipientStateMap[recipientId]; + OWSAssertDebug(result); + return [result copy]; +} + +#pragma mark - Update With... Methods + +- (void)updateWithSendingError:(NSError *)error transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(error); + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + // Mark any "sending" recipients as "failed." + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + recipientState.state = OWSOutgoingMessageRecipientStateFailed; + } + } + [message setMostRecentFailureText:error.localizedDescription]; + [message setIsCalculatingPoW:NO]; + }]; +} + +- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + // Mark any "sending" recipients as "failed." + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + recipientState.state = OWSOutgoingMessageRecipientStateFailed; + } + } + [message setIsCalculatingPoW:NO]; + }]; +} + +- (void)updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + // Mark any "sending" recipients as "failed." + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) { + recipientState.state = OWSOutgoingMessageRecipientStateSending; + } + } + }]; +} + +- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + [message setHasSyncedTranscript:hasSyncedTranscript]; + }]; +} + +- (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(customMessage); + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + [message setCustomMessage:customMessage]; + }]; +} + +- (void)updateWithCustomMessage:(NSString *)customMessage +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self updateWithCustomMessage:customMessage transaction:transaction]; + }]; +} + +- (void)saveIsCalculatingProofOfWork:(BOOL)isCalculatingPoW withTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + [self applyChangeToSelfAndLatestCopy:transaction changeBlock:^(TSOutgoingMessage *message) { + [message setIsCalculatingPoW:isCalculatingPoW]; + }]; +} + +- (void)updateWithSentRecipient:(NSString *)recipientId + wasSentByUD:(BOOL)wasSentByUD + transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + TSOutgoingMessageRecipientState *_Nullable recipientState + = message.recipientStateMap[recipientId]; + if (!recipientState) { return; } + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.wasSentByUD = wasSentByUD; + [message setIsCalculatingPoW:NO]; + }]; +} + +- (void)updateWithSkippedRecipient:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + TSOutgoingMessageRecipientState *_Nullable recipientState + = message.recipientStateMap[recipientId]; + if (!recipientState) { return; } + recipientState.state = OWSOutgoingMessageRecipientStateSkipped; + [message setIsCalculatingPoW:NO]; + }]; +} + +- (void)updateWithDeliveredRecipient:(NSString *)recipientId + deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + // If delivery notification doesn't include timestamp, use "now" as an estimate. + if (!deliveryTimestamp) { + deliveryTimestamp = @([NSDate ows_millisecondTimeStamp]); + } + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + TSOutgoingMessageRecipientState *_Nullable recipientState + = message.recipientStateMap[recipientId]; + if (!recipientState) { + // OWSFailDebug(@"Missing recipient state for delivered recipient: %@", recipientId); + return; + } + if (recipientState.state != OWSOutgoingMessageRecipientStateSent) { + OWSLogWarn(@"marking unsent message as delivered."); + } + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.deliveryTimestamp = deliveryTimestamp; + [message setIsCalculatingPoW:NO]; + }]; +} + +- (void)updateWithReadRecipientId:(NSString *)recipientId + readTimestamp:(uint64_t)readTimestamp + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(recipientId.length > 0); + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + TSOutgoingMessageRecipientState *_Nullable recipientState = message.recipientStateMap[recipientId]; + if (!recipientState) { return; } + if (recipientState.state != OWSOutgoingMessageRecipientStateSent) { + OWSLogWarn(@"Marking unsent message as delivered."); + } + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.readTimestamp = @(readTimestamp); + [message setIsCalculatingPoW:NO]; + }]; +} + +- (void)updateWithWasSentFromLinkedDeviceWithUDRecipientIds:(nullable NSArray *)udRecipientIds + nonUdRecipientIds:(nullable NSArray *)nonUdRecipientIds + isSentUpdate:(BOOL)isSentUpdate + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [self + applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + if (udRecipientIds.count > 0 || nonUdRecipientIds.count > 0) { + // If we have specific recipient info from the transcript, + // build a new recipient state map. + NSMutableDictionary *recipientStateMap + = [NSMutableDictionary new]; + for (NSString *recipientId in udRecipientIds) { + if (recipientStateMap[recipientId]) { + OWSFailDebug( + @"recipient appears more than once in recipient lists: %@", recipientId); + continue; + } + TSOutgoingMessageRecipientState *recipientState = + [TSOutgoingMessageRecipientState new]; + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.wasSentByUD = YES; + recipientStateMap[recipientId] = recipientState; + } + for (NSString *recipientId in nonUdRecipientIds) { + if (recipientStateMap[recipientId]) { + OWSFailDebug( + @"recipient appears more than once in recipient lists: %@", recipientId); + continue; + } + TSOutgoingMessageRecipientState *recipientState = + [TSOutgoingMessageRecipientState new]; + recipientState.state = OWSOutgoingMessageRecipientStateSent; + recipientState.wasSentByUD = NO; + recipientStateMap[recipientId] = recipientState; + } + + if (isSentUpdate) { + // If this is a "sent update", make sure that: + // + // a) "Sent updates" should never remove any recipients. We end up with the + // union of the existing and new recipients. + // b) "Sent updates" should never downgrade the "recipient state" for any + // recipients. Prefer existing "recipient state"; "sent updates" only + // add new recipients at the "sent" state. + // + // Therefore we retain all existing entries in the recipient state map. + [recipientStateMap addEntriesFromDictionary:self.recipientStateMap]; + } + + [message setRecipientStateMap:recipientStateMap]; + } else { + // Otherwise assume this is a legacy message before UD was introduced, and mark + // any "sending" recipient as "sent". Note that this will apply to non-legacy + // messages with no recipients. + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + if (recipientState.state == OWSOutgoingMessageRecipientStateSending) { + recipientState.state = OWSOutgoingMessageRecipientStateSent; + } + } + } + + [message setIsCalculatingPoW:NO]; + + if (!isSentUpdate) { + [message setIsFromLinkedDevice:YES]; + } + }]; +} + +- (void)updateWithSendingToSingleGroupRecipient:(NSString *)singleGroupRecipient + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + OWSAssertDebug(singleGroupRecipient.length > 0); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + TSOutgoingMessageRecipientState *recipientState = + [TSOutgoingMessageRecipientState new]; + recipientState.state = OWSOutgoingMessageRecipientStateSending; + [message setRecipientStateMap:@{ + singleGroupRecipient : recipientState, + }]; + }]; +} + +- (nullable NSNumber *)firstRecipientReadTimestamp +{ + NSNumber *result = nil; + for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) { + if (!recipientState.readTimestamp) { + continue; + } + if (!result || (result.unsignedLongLongValue > recipientState.readTimestamp.unsignedLongLongValue)) { + result = recipientState.readTimestamp; + } + } + return result; +} + +- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap + .allValues) { + switch (messageState) { + case TSOutgoingMessageStateSending: + recipientState.state = OWSOutgoingMessageRecipientStateSending; + break; + case TSOutgoingMessageStateFailed: + recipientState.state = OWSOutgoingMessageRecipientStateFailed; + break; + case TSOutgoingMessageStateSent: + recipientState.state = OWSOutgoingMessageRecipientStateSent; + break; + default: + OWSFailDebug(@"unexpected message state."); + break; + } + } + }]; +} + +#pragma mark - + +- (nullable id)dataMessageBuilder +{ + TSThread *thread = self.thread; + OWSAssertDebug(thread); + + SSKProtoDataMessageBuilder *builder = [SSKProtoDataMessage builder]; + [builder setTimestamp:self.timestamp]; + + if ([self.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold) { + [builder setBody:self.body]; + } else { + OWSFailDebug(@"message body length too long."); + NSString *truncatedBody = [self.body copy]; + while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) { + OWSLogError(@"truncating body which is too long: %lu", + (unsigned long)[truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); + truncatedBody = [truncatedBody substringToIndex:truncatedBody.length / 2]; + } + [builder setBody:truncatedBody]; + } + [builder setExpireTimer:self.expiresInSeconds]; + + // Group Messages + BOOL attachmentWasGroupAvatar = NO; + if ([thread isKindOfClass:[TSGroupThread class]]) { + TSGroupThread *gThread = (TSGroupThread *)thread; + SSKProtoGroupContextType groupMessageType; + switch (self.groupMetaMessage) { + case TSGroupMetaMessageQuit: + groupMessageType = SSKProtoGroupContextTypeQuit; + break; + case TSGroupMetaMessageUpdate: + case TSGroupMetaMessageNew: + groupMessageType = SSKProtoGroupContextTypeUpdate; + break; + default: + groupMessageType = SSKProtoGroupContextTypeDeliver; + break; + } + SSKProtoGroupContextBuilder *groupBuilder = + [SSKProtoGroupContext builderWithId:gThread.groupModel.groupId type:groupMessageType]; + if (groupMessageType == SSKProtoGroupContextTypeUpdate) { + if (gThread.groupModel.groupImage != nil && self.attachmentIds.count == 1) { + attachmentWasGroupAvatar = YES; + SSKProtoAttachmentPointer *_Nullable attachmentProto = + [TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject]; + if (!attachmentProto) { + OWSFailDebug(@"could not build protobuf."); + return nil; + } + [groupBuilder setAvatar:attachmentProto]; + } + + [groupBuilder setMembers:gThread.groupModel.groupMemberIds]; + [groupBuilder setName:gThread.groupModel.groupName]; + [groupBuilder setAdmins:gThread.groupModel.groupAdminIds]; + } + NSError *error; + SSKProtoGroupContext *_Nullable groupContextProto = [groupBuilder buildAndReturnError:&error]; + if (error || !groupContextProto) { + OWSFailDebug(@"could not build protobuf: %@.", error); + return nil; + } + [builder setGroup:groupContextProto]; + } + + // Message Attachments + if (!attachmentWasGroupAvatar) { + NSMutableArray *attachments = [NSMutableArray new]; + for (NSString *attachmentId in self.attachmentIds) { + SSKProtoAttachmentPointer *_Nullable attachmentProto = + [TSAttachmentStream buildProtoForAttachmentId:attachmentId]; + if (!attachmentProto) { + OWSFailDebug(@"could not build protobuf."); + return nil; + } + [attachments addObject:attachmentProto]; + } + [builder setAttachments:attachments]; + } + + // Quoted Reply + SSKProtoDataMessageQuoteBuilder *_Nullable quotedMessageBuilder = self.quotedMessageBuilder; + if (quotedMessageBuilder) { + NSError *error; + SSKProtoDataMessageQuote *_Nullable quoteProto = [quotedMessageBuilder buildAndReturnError:&error]; + if (error || !quoteProto) { + OWSFailDebug(@"could not build protobuf: %@.", error); + return nil; + } + [builder setQuote:quoteProto]; + } + + // Contact Share + if (self.contactShare) { + SSKProtoDataMessageContact *_Nullable contactProto = + [OWSContacts protoForContact:self.contactShare]; + if (contactProto) { + [builder addContact:contactProto]; + } else { + OWSFailDebug(@"contactProto was unexpectedly nil"); + } + } + + // Link Preview + if (self.linkPreview) { + SSKProtoDataMessagePreviewBuilder *previewBuilder = + [SSKProtoDataMessagePreview builderWithUrl:self.linkPreview.urlString]; + if (self.linkPreview.title.length > 0) { + [previewBuilder setTitle:self.linkPreview.title]; + } + if (self.linkPreview.imageAttachmentId) { + SSKProtoAttachmentPointer *_Nullable attachmentProto = + [TSAttachmentStream buildProtoForAttachmentId:self.linkPreview.imageAttachmentId]; + if (!attachmentProto) { + OWSFailDebug(@"Could not build link preview image protobuf."); + } else { + [previewBuilder setImage:attachmentProto]; + } + } + + NSError *error; + SSKProtoDataMessagePreview *_Nullable previewProto = [previewBuilder buildAndReturnError:&error]; + if (error || !previewProto) { + OWSFailDebug(@"Could not build link preview protobuf: %@.", error); + } else { + [builder addPreview:previewProto]; + } + } + + return builder; +} + +- (nullable SSKProtoDataMessageQuoteBuilder *)quotedMessageBuilder +{ + if (!self.quotedMessage) { + return nil; + } + TSQuotedMessage *quotedMessage = self.quotedMessage; + + SSKProtoDataMessageQuoteBuilder *quoteBuilder = + [SSKProtoDataMessageQuote builderWithId:quotedMessage.timestamp author:quotedMessage.authorId]; + + BOOL hasQuotedText = NO; + BOOL hasQuotedAttachment = NO; + if (self.quotedMessage.body.length > 0) { + hasQuotedText = YES; + [quoteBuilder setText:quotedMessage.body]; + } + + if (quotedMessage.quotedAttachments) { + for (OWSAttachmentInfo *attachment in quotedMessage.quotedAttachments) { + hasQuotedAttachment = YES; + + SSKProtoDataMessageQuoteQuotedAttachmentBuilder *quotedAttachmentBuilder = + [SSKProtoDataMessageQuoteQuotedAttachment builder]; + + quotedAttachmentBuilder.contentType = attachment.contentType; + quotedAttachmentBuilder.fileName = attachment.sourceFilename; + if (attachment.thumbnailAttachmentStreamId) { + quotedAttachmentBuilder.thumbnail = + [TSAttachmentStream buildProtoForAttachmentId:attachment.thumbnailAttachmentStreamId]; + } + + NSError *error; + SSKProtoDataMessageQuoteQuotedAttachment *_Nullable quotedAttachmentMessage = + [quotedAttachmentBuilder buildAndReturnError:&error]; + if (error || !quotedAttachmentMessage) { + OWSFailDebug(@"could not build protobuf: %@", error); + return nil; + } + + [quoteBuilder addAttachments:quotedAttachmentMessage]; + } + } + + if (hasQuotedText || hasQuotedAttachment) { + return quoteBuilder; + } else { + OWSFailDebug(@"Invalid quoted message data."); + return nil; + } +} + +// recipientId is nil when building "sent" sync messages for messages sent to groups. +- (nullable SSKProtoDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId +{ + OWSAssertDebug(self.thread); + SSKProtoDataMessageBuilder *_Nullable builder = [self dataMessageBuilder]; + if (builder == nil) { + OWSFailDebug(@"Couldn't build protobuf."); + return nil; + } + + [ProtoUtils addLocalProfileKeyIfNecessary:self.thread recipientId:recipientId dataMessageBuilder:builder]; + + id profileManager = SSKEnvironment.shared.profileManager; + NSString *displayName; + NSString *masterPublicKey = [NSUserDefaults.standardUserDefaults stringForKey:@"masterDeviceHexEncodedPublicKey"]; + if (masterPublicKey != nil) { + displayName = [profileManager profileNameForRecipientWithID:masterPublicKey]; + } else { + displayName = profileManager.localProfileName; + } + NSString *profilePictureURL = profileManager.profilePictureURL; + SSKProtoDataMessageLokiProfileBuilder *profileBuilder = [SSKProtoDataMessageLokiProfile builder]; + [profileBuilder setDisplayName:displayName]; + [profileBuilder setProfilePicture:profilePictureURL ?: @""]; + SSKProtoDataMessageLokiProfile *profile = [profileBuilder buildAndReturnError:nil]; + [builder setProfile:profile]; + + NSError *error; + SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error]; + if (error != nil || dataProto == nil) { + OWSFailDebug(@"Couldn't build protobuf due to error: %@.", error); + return nil; + } + return dataProto; +} + +- (nullable id)prepareCustomContentBuilder:(SignalRecipient *)recipient { + SSKProtoDataMessage *_Nullable dataMessage = [self buildDataMessage:recipient.recipientId]; + + if (dataMessage == nil) { + OWSFailDebug(@"Couldn't build protobuf."); + return nil; + } + + SSKProtoContentBuilder *contentBuilder = SSKProtoContent.builder; + [contentBuilder setDataMessage:dataMessage]; + + return contentBuilder; +} + +- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient +{ + SSKProtoContentBuilder *contentBuilder = [self prepareCustomContentBuilder:recipient]; + + NSError *error; + NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error]; + if (error != nil || contentData == nil) { + OWSFailDebug(@"Couldn't serialize protobuf due to error: %@.", error); + return nil; + } + + return contentData; +} + +- (BOOL)shouldSyncTranscript +{ + return YES; +} + +- (NSString *)statusDescription +{ + NSMutableString *result = [NSMutableString new]; + [result appendFormat:@"[status: %@", NSStringForOutgoingMessageState(self.messageState)]; + for (NSString *recipientId in self.recipientStateMap) { + TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId]; + [result appendFormat:@", %@: %@", recipientId, NSStringForOutgoingMessageRecipientState(recipientState.state)]; + } + [result appendString:@"]"]; + return [result copy]; +} + +- (uint)ttl { return (uint)[LKTTLUtilities getTTLFor:LKMessageTypeRegular]; } + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSPreKeyManager.h b/SignalUtilitiesKit/TSPreKeyManager.h new file mode 100644 index 000000000..4f9253f71 --- /dev/null +++ b/SignalUtilitiesKit/TSPreKeyManager.h @@ -0,0 +1,36 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSAccountManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface TSPreKeyManager : NSObject + +#pragma mark - State Tracking + ++ (BOOL)isAppLockedDueToPreKeyUpdateFailures; + ++ (void)incrementPreKeyUpdateFailureCount; + ++ (void)clearPreKeyUpdateFailureCount; + ++ (void)clearSignedPreKeyRecords; + +// This should only be called from the TSPreKeyManager.operationQueue ++ (void)refreshPreKeysDidSucceed; + +#pragma mark - Check/Request Initiation + ++ (void)rotateSignedPreKeyWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler; + ++ (void)createPreKeysWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler; + ++ (void)checkPreKeys; + ++ (void)checkPreKeysIfNecessary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSPreKeyManager.m b/SignalUtilitiesKit/TSPreKeyManager.m new file mode 100644 index 000000000..0a5da54c2 --- /dev/null +++ b/SignalUtilitiesKit/TSPreKeyManager.m @@ -0,0 +1,293 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSPreKeyManager.h" +#import "AppContext.h" +#import "NSURLSessionDataTask+StatusCode.h" +#import "OWSIdentityManager.h" +#import "OWSPrimaryStorage+SignedPreKeyStore.h" +#import "SSKEnvironment.h" +#import "TSNetworkManager.h" +#import "TSStorageHeaders.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +// Time before deletion of signed prekeys (measured in seconds) +#define kSignedPreKeysDeletionTime (7 * kDayInterval) + +// Time before rotation of signed prekeys (measured in seconds) +#define kSignedPreKeyRotationTime (2 * kDayInterval) + +// How often we check prekey state on app activation. +#define kPreKeyCheckFrequencySeconds (12 * kHourInterval) + +// This global should only be accessed on prekeyQueue. +static NSDate *lastPreKeyCheckTimestamp = nil; + +// Maximum number of failures while updating signed prekeys +// before the message sending is disabled. +static const NSUInteger kMaxPrekeyUpdateFailureCount = 5; + +// Maximum amount of time that can elapse without updating signed prekeys +// before the message sending is disabled. +#define kSignedPreKeyUpdateFailureMaxFailureDuration (10 * kDayInterval) + +#pragma mark - + +@implementation TSPreKeyManager + +#pragma mark - Dependencies + ++ (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +#pragma mark - State Tracking + ++ (BOOL)isAppLockedDueToPreKeyUpdateFailures +{ + // Only disable message sending if we have failed more than N times + // over a period of at least M days. + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + return ([primaryStorage prekeyUpdateFailureCount] >= kMaxPrekeyUpdateFailureCount && + [primaryStorage firstPrekeyUpdateFailureDate] != nil + && fabs([[primaryStorage firstPrekeyUpdateFailureDate] timeIntervalSinceNow]) + >= kSignedPreKeyUpdateFailureMaxFailureDuration); +} + ++ (void)incrementPreKeyUpdateFailureCount +{ + // Record a prekey update failure. + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + int failureCount = [primaryStorage incrementPrekeyUpdateFailureCount]; + OWSLogInfo(@"new failureCount: %d", failureCount); + + if (failureCount == 1 || ![primaryStorage firstPrekeyUpdateFailureDate]) { + // If this is the "first" failure, record the timestamp of that + // failure. + [primaryStorage setFirstPrekeyUpdateFailureDate:[NSDate new]]; + } +} + ++ (void)clearPreKeyUpdateFailureCount +{ + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + [primaryStorage clearFirstPrekeyUpdateFailureDate]; + [primaryStorage clearPrekeyUpdateFailureCount]; +} + ++ (void)refreshPreKeysDidSucceed +{ + lastPreKeyCheckTimestamp = [NSDate new]; +} + +#pragma mark - Check/Request Initiation + ++ (NSOperationQueue *)operationQueue +{ + static dispatch_once_t onceToken; + static NSOperationQueue *operationQueue; + + // PreKey state lives in two places - on the client and on the service. + // Some of our pre-key operations depend on the service state, e.g. we need to check our one-time-prekey count + // before we decide to upload new ones. This potentially entails multiple async operations, all of which should + // complete before starting any other pre-key operation. That's why a dispatch_queue is insufficient for + // coordinating PreKey operations and instead we use NSOperation's on a serial NSOperationQueue. + dispatch_once(&onceToken, ^{ + operationQueue = [NSOperationQueue new]; + operationQueue.name = @"TSPreKeyManager"; + operationQueue.maxConcurrentOperationCount = 1; + }); + return operationQueue; +} + ++ (void)checkPreKeysIfNecessary +{ + if (!CurrentAppContext().isMainAppAndActive) { + return; + } + if (!self.tsAccountManager.isRegisteredAndReady) { + return; + } + + SSKRefreshPreKeysOperation *refreshOperation = [SSKRefreshPreKeysOperation new]; + + __weak SSKRefreshPreKeysOperation *weakRefreshOperation = refreshOperation; + NSBlockOperation *checkIfRefreshNecessaryOperation = [NSBlockOperation blockOperationWithBlock:^{ + BOOL shouldCheck = (lastPreKeyCheckTimestamp == nil + || fabs([lastPreKeyCheckTimestamp timeIntervalSinceNow]) >= kPreKeyCheckFrequencySeconds); + if (!shouldCheck) { + [weakRefreshOperation cancel]; + } + }]; + + [refreshOperation addDependency:checkIfRefreshNecessaryOperation]; + + SSKRotateSignedPreKeyOperation *rotationOperation = [SSKRotateSignedPreKeyOperation new]; + + __weak SSKRotateSignedPreKeyOperation *weakRotationOperation = rotationOperation; + NSBlockOperation *checkIfRotationNecessaryOperation = [NSBlockOperation blockOperationWithBlock:^{ + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + SignedPreKeyRecord *_Nullable signedPreKey = [primaryStorage currentSignedPreKey]; + + BOOL shouldCheck + = !signedPreKey || fabs(signedPreKey.generatedAt.timeIntervalSinceNow) >= kSignedPreKeyRotationTime; + if (!shouldCheck) { + [weakRotationOperation cancel]; + } + }]; + + [rotationOperation addDependency:checkIfRotationNecessaryOperation]; + + // Order matters here - if we rotated *before* refreshing, we'd risk uploading + // two SPK's in a row since RefreshPreKeysOperation can also upload a new SPK. + [checkIfRotationNecessaryOperation addDependency:refreshOperation]; + + NSArray *operations = + @[ checkIfRefreshNecessaryOperation, refreshOperation, checkIfRotationNecessaryOperation, rotationOperation ]; + [self.operationQueue addOperations:operations waitUntilFinished:NO]; +} + ++ (void)createPreKeysWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(!self.tsAccountManager.isRegisteredAndReady); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + SSKCreatePreKeysOperation *operation = [SSKCreatePreKeysOperation new]; + [self.operationQueue addOperations:@[ operation ] waitUntilFinished:YES]; + + NSError *_Nullable error = operation.failingError; + if (error) { + dispatch_async(dispatch_get_main_queue(), ^{ + failureHandler(error); + }); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + successHandler(); + }); + } + }); +} + ++ (void)rotateSignedPreKeyWithSuccess:(void (^)(void))successHandler failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(!self.tsAccountManager.isRegisteredAndReady); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + SSKRotateSignedPreKeyOperation *operation = [SSKRotateSignedPreKeyOperation new]; + [self.operationQueue addOperations:@[ operation ] waitUntilFinished:YES]; + + NSError *_Nullable error = operation.failingError; + if (error) { + dispatch_async(dispatch_get_main_queue(), ^{ + failureHandler(error); + }); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + successHandler(); + }); + } + }); +} + ++ (void)checkPreKeys +{ + if (!CurrentAppContext().isMainApp) { return; } + if (!self.tsAccountManager.isRegisteredAndReady) { return; } + SSKRefreshPreKeysOperation *operation = [SSKRefreshPreKeysOperation new]; + [self.operationQueue addOperation:operation]; +} + ++ (void)clearSignedPreKeyRecords { + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + NSNumber *_Nullable currentSignedPrekeyId = [primaryStorage currentSignedPrekeyId]; + [self clearSignedPreKeyRecordsWithKeyId:currentSignedPrekeyId]; +} + ++ (void)clearSignedPreKeyRecordsWithKeyId:(NSNumber *_Nullable)keyId +{ + if (!keyId) { + // currentSignedPreKeyId should only be nil before we've completed registration. + // We have this guard here for robustness, but we should never get here. + OWSFailDebug(@"Ignoring request to clear signed preKeys since no keyId was specified"); + return; + } + + OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager]; + SignedPreKeyRecord *currentRecord = [primaryStorage loadSignedPrekeyOrNil:keyId.intValue]; + if (!currentRecord) { + OWSFailDebug(@"Couldn't find signed prekey for id: %@", keyId); + } + NSArray *allSignedPrekeys = [primaryStorage loadSignedPreKeys]; + NSArray *oldSignedPrekeys + = (currentRecord != nil ? [self removeCurrentRecord:currentRecord fromRecords:allSignedPrekeys] + : allSignedPrekeys); + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterMediumStyle; + dateFormatter.timeStyle = NSDateFormatterMediumStyle; + dateFormatter.locale = [NSLocale systemLocale]; + + // Sort the signed prekeys in ascending order of generation time. + oldSignedPrekeys = [oldSignedPrekeys sortedArrayUsingComparator:^NSComparisonResult( + SignedPreKeyRecord *_Nonnull left, SignedPreKeyRecord *_Nonnull right) { + return [left.generatedAt compare:right.generatedAt]; + }]; + + NSUInteger oldSignedPreKeyCount = oldSignedPrekeys.count; + + int oldAcceptedSignedPreKeyCount = 0; + for (SignedPreKeyRecord *signedPrekey in oldSignedPrekeys) { + if (signedPrekey.wasAcceptedByService) { + oldAcceptedSignedPreKeyCount++; + } + } + + // Iterate the signed prekeys in ascending order so that we try to delete older keys first. + for (SignedPreKeyRecord *signedPrekey in oldSignedPrekeys) { + // Always keep at least 3 keys, accepted or otherwise. + if (oldSignedPreKeyCount <= 3) { + continue; + } + + // Never delete signed prekeys until they are N days old. + if (fabs([signedPrekey.generatedAt timeIntervalSinceNow]) < kSignedPreKeysDeletionTime) { + continue; + } + + // We try to keep a minimum of 3 "old, accepted" signed prekeys. + if (signedPrekey.wasAcceptedByService) { + if (oldAcceptedSignedPreKeyCount <= 3) { + continue; + } else { + oldAcceptedSignedPreKeyCount--; + } + } + + oldSignedPreKeyCount--; + [primaryStorage removeSignedPreKey:signedPrekey.Id]; + } +} + ++ (NSArray *)removeCurrentRecord:(SignedPreKeyRecord *)currentRecord fromRecords:(NSArray *)allRecords { + NSMutableArray *oldRecords = [NSMutableArray array]; + + for (SignedPreKeyRecord *record in allRecords) { + if (currentRecord.Id != record.Id) { + [oldRecords addObject:record]; + } + } + + return oldRecords; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSPrefix.h b/SignalUtilitiesKit/TSPrefix.h new file mode 100644 index 000000000..f82e9c8da --- /dev/null +++ b/SignalUtilitiesKit/TSPrefix.h @@ -0,0 +1,21 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +@import CocoaLumberjack; + +#ifdef DEBUG +static const NSUInteger ddLogLevel = DDLogLevelAll; +#else +static const NSUInteger ddLogLevel = DDLogLevelInfo; +#endif +#import "OWSAnalytics.h" +#import "NSArray+Functional.h" +#import "NSSet+Functional.h" +#import "NSObject+Casting.h" +#import "SSKAsserts.h" +#import "TSConstants.h" +#import +#import diff --git a/SignalUtilitiesKit/TSQuotedMessage.h b/SignalUtilitiesKit/TSQuotedMessage.h new file mode 100644 index 000000000..b1b1b77db --- /dev/null +++ b/SignalUtilitiesKit/TSQuotedMessage.h @@ -0,0 +1,108 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SSKProtoDataMessage; +@class TSAttachment; +@class TSAttachmentStream; +@class TSQuotedMessage; +@class TSThread; +@class YapDatabaseReadWriteTransaction; + +@interface OWSAttachmentInfo : MTLModel + +@property (nonatomic, readonly, nullable) NSString *contentType; +@property (nonatomic, readonly, nullable) NSString *sourceFilename; + +// This is only set when sending a new attachment so we have a way +// to reference the original attachment when generating a thumbnail. +// We don't want to do this until the message is saved, when the user sends +// the message so as not to end up with an orphaned file. +@property (nonatomic, readonly, nullable) NSString *attachmentId; + +// References a yet-to-be downloaded thumbnail file +@property (atomic, nullable) NSString *thumbnailAttachmentPointerId; + +// References an already downloaded or locally generated thumbnail file +@property (atomic, nullable) NSString *thumbnailAttachmentStreamId; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithAttachmentId:(nullable NSString *)attachmentId + contentType:(NSString *)contentType + sourceFilename:(NSString *)sourceFilename NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream; + +@end + +typedef NS_ENUM(NSUInteger, TSQuotedMessageContentSource) { + TSQuotedMessageContentSourceUnknown, + TSQuotedMessageContentSourceLocal, + TSQuotedMessageContentSourceRemote +}; + +@interface TSQuotedMessage : MTLModel + +@property (nonatomic, readonly) uint64_t timestamp; +@property (nonatomic, readonly) NSString *authorId; +@property (nonatomic, readonly) TSQuotedMessageContentSource bodySource; + +// This property should be set IFF we are quoting a text message +// or attachment with caption. +@property (nullable, nonatomic, readonly) NSString *body; + +#pragma mark - Attachments + +// This is a MIME type. +// +// This property should be set IFF we are quoting an attachment message. +- (nullable NSString *)contentType; +- (nullable NSString *)sourceFilename; + +// References a yet-to-be downloaded thumbnail file +- (nullable NSString *)thumbnailAttachmentPointerId; + +// References an already downloaded or locally generated thumbnail file +- (nullable NSString *)thumbnailAttachmentStreamId; +- (void)setThumbnailAttachmentStream:(TSAttachment *)thumbnailAttachmentStream; + +// currently only used by orphan attachment cleaner +- (NSArray *)thumbnailAttachmentStreamIds; + +@property (atomic, readonly) NSArray *quotedAttachments; + +// Before sending, persist a thumbnail attachment derived from the quoted attachment +- (NSArray *)createThumbnailAttachmentsIfNecessaryWithTransaction: + (YapDatabaseReadWriteTransaction *)transaction; + +- (instancetype)init NS_UNAVAILABLE; + +// used when receiving quoted messages +- (instancetype)initWithTimestamp:(uint64_t)timestamp + authorId:(NSString *)authorId + body:(NSString *_Nullable)body + bodySource:(TSQuotedMessageContentSource)bodySource + receivedQuotedAttachmentInfos:(NSArray *)attachmentInfos; + +// used when sending quoted messages +- (instancetype)initWithTimestamp:(uint64_t)timestamp + authorId:(NSString *)authorId + body:(NSString *_Nullable)body + quotedAttachmentsForSending:(NSArray *)attachments; + + ++ (nullable instancetype)quotedMessageForDataMessage:(SSKProtoDataMessage *)dataMessage + thread:(TSThread *)thread + transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +#pragma mark - + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSQuotedMessage.m b/SignalUtilitiesKit/TSQuotedMessage.m new file mode 100644 index 000000000..d5a4068f7 --- /dev/null +++ b/SignalUtilitiesKit/TSQuotedMessage.m @@ -0,0 +1,378 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSQuotedMessage.h" +#import "TSAccountManager.h" +#import "TSAttachment.h" +#import "TSAttachmentPointer.h" +#import "TSAttachmentStream.h" +#import "TSIncomingMessage.h" +#import "TSInteraction.h" +#import "TSOutgoingMessage.h" +#import "TSThread.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSAttachmentInfo + +- (instancetype)initWithAttachmentStream:(TSAttachmentStream *)attachmentStream; +{ + OWSAssertDebug([attachmentStream isKindOfClass:[TSAttachmentStream class]]); + OWSAssertDebug(attachmentStream.uniqueId); + OWSAssertDebug(attachmentStream.contentType); + + return [self initWithAttachmentId:attachmentStream.uniqueId + contentType:attachmentStream.contentType + sourceFilename:attachmentStream.sourceFilename]; +} + +- (instancetype)initWithAttachmentId:(nullable NSString *)attachmentId + contentType:(NSString *)contentType + sourceFilename:(NSString *)sourceFilename +{ + self = [super init]; + if (!self) { + return self; + } + + _attachmentId = attachmentId; + _contentType = contentType; + _sourceFilename = sourceFilename; + + return self; +} + +@end + +@interface TSQuotedMessage () + +@property (atomic) NSArray *quotedAttachments; +@property (atomic) NSArray *quotedAttachmentsForSending; + +@end + +@implementation TSQuotedMessage + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + authorId:(NSString *)authorId + body:(NSString *_Nullable)body + bodySource:(TSQuotedMessageContentSource)bodySource + receivedQuotedAttachmentInfos:(NSArray *)attachmentInfos +{ + OWSAssertDebug(timestamp > 0); + OWSAssertDebug(authorId.length > 0); + + self = [super init]; + if (!self) { + return nil; + } + + _timestamp = timestamp; + _authorId = authorId; + _body = body; + _bodySource = bodySource; + _quotedAttachments = attachmentInfos; + + return self; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp + authorId:(NSString *)authorId + body:(NSString *_Nullable)body + quotedAttachmentsForSending:(NSArray *)attachments +{ + OWSAssertDebug(timestamp > 0); + OWSAssertDebug(authorId.length > 0); + + self = [super init]; + if (!self) { + return nil; + } + + _timestamp = timestamp; + _authorId = authorId; + _body = body; + _bodySource = TSQuotedMessageContentSourceLocal; + + NSMutableArray *attachmentInfos = [NSMutableArray new]; + for (TSAttachmentStream *attachmentStream in attachments) { + [attachmentInfos addObject:[[OWSAttachmentInfo alloc] initWithAttachmentStream:attachmentStream]]; + } + _quotedAttachments = [attachmentInfos copy]; + + return self; +} + ++ (TSQuotedMessage *_Nullable)quotedMessageForDataMessage:(SSKProtoDataMessage *)dataMessage + thread:(TSThread *)thread + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(dataMessage); + + if (!dataMessage.quote) { + return nil; + } + + SSKProtoDataMessageQuote *quoteProto = [dataMessage quote]; + + if (quoteProto.id == 0) { + OWSFailDebug(@"quoted message missing id"); + return nil; + } + uint64_t timestamp = [quoteProto id]; + + if (quoteProto.author.length == 0) { + OWSFailDebug(@"quoted message missing author"); + return nil; + } + // TODO: We could verify that this is a valid e164 value. + NSString *authorId = [quoteProto author]; + + NSString *_Nullable body = nil; + BOOL hasAttachment = NO; + TSQuotedMessageContentSource bodySource = TSQuotedMessageContentSourceUnknown; + + // Prefer to generate the text snippet locally if available. + TSMessage *_Nullable quotedMessage = [self findQuotedMessageWithTimestamp:timestamp + threadId:thread.uniqueId + authorId:authorId + transaction:transaction]; + + if (quotedMessage) { + bodySource = TSQuotedMessageContentSourceLocal; + + NSString *localText = [quotedMessage bodyTextWithTransaction:transaction]; + if (localText.length > 0) { + body = localText; + } + } + + if (body.length == 0) { + if (quoteProto.text.length > 0) { + bodySource = TSQuotedMessageContentSourceRemote; + body = quoteProto.text; + } + } + + NSMutableArray *attachmentInfos = [NSMutableArray new]; + for (SSKProtoDataMessageQuoteQuotedAttachment *quotedAttachment in quoteProto.attachments) { + hasAttachment = YES; + OWSAttachmentInfo *attachmentInfo = [[OWSAttachmentInfo alloc] initWithAttachmentId:nil + contentType:quotedAttachment.contentType + sourceFilename:quotedAttachment.fileName]; + + // We prefer deriving any thumbnail locally rather than fetching one from the network. + TSAttachmentStream *_Nullable localThumbnail = + [self tryToDeriveLocalThumbnailWithTimestamp:timestamp + threadId:thread.uniqueId + authorId:authorId + contentType:quotedAttachment.contentType + transaction:transaction]; + + if (localThumbnail) { + OWSLogDebug(@"Generated local thumbnail for quoted quoted message: %@:%lu", + thread.uniqueId, + (unsigned long)timestamp); + + [localThumbnail saveWithTransaction:transaction]; + + attachmentInfo.thumbnailAttachmentStreamId = localThumbnail.uniqueId; + } else if (quotedAttachment.thumbnail) { + OWSLogDebug(@"Saving reference for fetching remote thumbnail for quoted message: %@:%lu", + thread.uniqueId, + (unsigned long)timestamp); + + SSKProtoAttachmentPointer *thumbnailAttachmentProto = quotedAttachment.thumbnail; + TSAttachmentPointer *_Nullable thumbnailPointer = + [TSAttachmentPointer attachmentPointerFromProto:thumbnailAttachmentProto albumMessage:nil]; + if (thumbnailPointer) { + [thumbnailPointer saveWithTransaction:transaction]; + + attachmentInfo.thumbnailAttachmentPointerId = thumbnailPointer.uniqueId; + } else { + OWSFailDebug(@"Invalid thumbnail attachment."); + } + } else { + OWSLogDebug(@"No thumbnail for quoted message: %@:%lu", thread.uniqueId, (unsigned long)timestamp); + } + + [attachmentInfos addObject:attachmentInfo]; + + // For now, only support a single quoted attachment. + break; + } + + if (body.length == 0 && !hasAttachment) { + return nil; + } + + // Legit usage of senderTimestamp - this class references the message it is quoting by it's sender timestamp + return [[TSQuotedMessage alloc] initWithTimestamp:timestamp + authorId:authorId + body:body + bodySource:bodySource + receivedQuotedAttachmentInfos:attachmentInfos]; +} + ++ (nullable TSAttachmentStream *)tryToDeriveLocalThumbnailWithTimestamp:(uint64_t)timestamp + threadId:(NSString *)threadId + authorId:(NSString *)authorId + contentType:(NSString *)contentType + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + TSMessage *_Nullable quotedMessage = + [self findQuotedMessageWithTimestamp:timestamp threadId:threadId authorId:authorId transaction:transaction]; + if (!quotedMessage) { + return nil; + } + + TSAttachment *_Nullable attachmentToQuote = nil; + if (quotedMessage.attachmentIds.count > 0) { + attachmentToQuote = [quotedMessage attachmentsWithTransaction:transaction].firstObject; + } else if (quotedMessage.linkPreview && quotedMessage.linkPreview.imageAttachmentId.length > 0) { + attachmentToQuote = + [TSAttachment fetchObjectWithUniqueID:quotedMessage.linkPreview.imageAttachmentId transaction:transaction]; + } + if (![attachmentToQuote isKindOfClass:[TSAttachmentStream class]]) { + return nil; + } + if (![TSAttachmentStream hasThumbnailForMimeType:contentType]) { + return nil; + } + TSAttachmentStream *sourceStream = (TSAttachmentStream *)attachmentToQuote; + return [sourceStream cloneAsThumbnail]; +} + ++ (nullable TSMessage *)findQuotedMessageWithTimestamp:(uint64_t)timestamp + threadId:(NSString *)threadId + authorId:(NSString *)authorId + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(transaction); + + if (timestamp <= 0) { + OWSFailDebug(@"Invalid timestamp: %llu", timestamp); + return nil; + } + if (threadId.length <= 0) { + OWSFailDebug(@"Invalid thread."); + return nil; + } + if (authorId.length <= 0) { + OWSFailDebug(@"Invalid authorId: %@", authorId); + return nil; + } + + for (TSMessage *message in + [TSInteraction interactionsWithTimestamp:timestamp ofClass:TSMessage.class withTransaction:transaction]) { + if (![message.uniqueThreadId isEqualToString:threadId]) { + continue; + } + if ([message isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message; + if (![authorId isEqual:incomingMessage.authorId]) { + continue; + } + } else if ([message isKindOfClass:[TSOutgoingMessage class]]) { + if (![authorId isEqual:[TSAccountManager localNumber]]) { + continue; + } + } + + return message; + } + OWSLogWarn(@"Could not find quoted message: %llu", timestamp); + return nil; +} + +#pragma mark - Attachment (not necessarily with a thumbnail) + +- (nullable OWSAttachmentInfo *)firstAttachmentInfo +{ + OWSAssertDebug(self.quotedAttachments.count <= 1); + return self.quotedAttachments.firstObject; +} + +- (nullable NSString *)contentType +{ + OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo; + + return firstAttachment.contentType; +} + +- (nullable NSString *)sourceFilename +{ + OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo; + + return firstAttachment.sourceFilename; +} + +- (nullable NSString *)thumbnailAttachmentPointerId +{ + OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo; + + return firstAttachment.thumbnailAttachmentPointerId; +} + +- (nullable NSString *)thumbnailAttachmentStreamId +{ + OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo; + + return firstAttachment.thumbnailAttachmentStreamId; +} + +- (void)setThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream +{ + OWSAssertDebug([attachmentStream isKindOfClass:[TSAttachmentStream class]]); + OWSAssertDebug(self.quotedAttachments.count == 1); + + OWSAttachmentInfo *firstAttachment = self.firstAttachmentInfo; + firstAttachment.thumbnailAttachmentStreamId = attachmentStream.uniqueId; +} + +- (NSArray *)thumbnailAttachmentStreamIds +{ + NSMutableArray *streamIds = [NSMutableArray new]; + for (OWSAttachmentInfo *info in self.quotedAttachments) { + if (info.thumbnailAttachmentStreamId) { + [streamIds addObject:info.thumbnailAttachmentStreamId]; + } + } + + return [streamIds copy]; +} + +// Before sending, persist a thumbnail attachment derived from the quoted attachment +- (NSArray *)createThumbnailAttachmentsIfNecessaryWithTransaction: + (YapDatabaseReadWriteTransaction *)transaction +{ + NSMutableArray *thumbnailAttachments = [NSMutableArray new]; + + for (OWSAttachmentInfo *info in self.quotedAttachments) { + + OWSAssertDebug(info.attachmentId); + TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:info.attachmentId transaction:transaction]; + if (![attachment isKindOfClass:[TSAttachmentStream class]]) { + continue; + } + TSAttachmentStream *sourceStream = (TSAttachmentStream *)attachment; + + TSAttachmentStream *_Nullable thumbnailStream = [sourceStream cloneAsThumbnail]; + if (!thumbnailStream) { + continue; + } + + [thumbnailStream saveWithTransaction:transaction]; + info.thumbnailAttachmentStreamId = thumbnailStream.uniqueId; + [thumbnailAttachments addObject:thumbnailStream]; + } + + return [thumbnailAttachments copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSSocketManager.h b/SignalUtilitiesKit/TSSocketManager.h new file mode 100644 index 000000000..28e17d17f --- /dev/null +++ b/SignalUtilitiesKit/TSSocketManager.h @@ -0,0 +1,50 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSWebSocket.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TSRequest; + +@interface TSSocketManager : NSObject + +@property (class, readonly, nonatomic) TSSocketManager *shared; + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +// Returns the "best" state of any of the sockets. +// +// We surface the socket state in various places in the UI. +// We generally are trying to indicate/help resolve network +// connectivity issues. We want to show the "best" or "highest" +// socket state of the sockets. e.g. the UI should reflect +// "open" if any of the sockets is open. +- (OWSWebSocketState)highestSocketState; + +// If the app is in the foreground, we'll try to open the socket unless it's already +// open or connecting. +// +// If the app is in the background, we'll try to open the socket unless it's already +// open or connecting _and_ keep it open for at least N seconds. +// If the app is in the background and the socket is already open or connecting this +// might prolong how long we keep the socket open. +// +// This method can be called from any thread. +- (void)requestSocketOpen; + +// This can be used to force the socket to close and re-open, if it is open. +- (void)cycleSocket; + +#pragma mark - Message Sending + +- (BOOL)canMakeRequests; + +- (void)makeRequest:(TSRequest *)request + success:(TSSocketMessageSuccess)success + failure:(TSSocketMessageFailure)failure; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSSocketManager.m b/SignalUtilitiesKit/TSSocketManager.m new file mode 100644 index 000000000..4fee91411 --- /dev/null +++ b/SignalUtilitiesKit/TSSocketManager.m @@ -0,0 +1,80 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSSocketManager.h" +#import "SSKEnvironment.h" +#import "SSKAsserts.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TSSocketManager () + +@property (nonatomic) OWSWebSocket *websocket; + +@end + +#pragma mark - + +@implementation TSSocketManager + +- (instancetype)init +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSAssertIsOnMainThread(); + + _websocket = [[OWSWebSocket alloc] init]; + + OWSSingletonAssert(); + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + ++ (instancetype)shared +{ + OWSAssert(SSKEnvironment.shared.socketManager); + + return SSKEnvironment.shared.socketManager; +} + +- (BOOL)canMakeRequests +{ + return self.websocket.canMakeRequests; +} + +- (void)makeRequest:(TSRequest *)request + success:(TSSocketMessageSuccess)success + failure:(TSSocketMessageFailure)failure +{ + [self.websocket makeRequest:request success:success failure:failure]; +} + +- (void)requestSocketOpen +{ + [self.websocket requestSocketOpen]; +} + +- (void)cycleSocket +{ + [self.websocket cycleSocket]; +} + +- (OWSWebSocketState)highestSocketState +{ + return self.websocket.state; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSStorageHeaders.h b/SignalUtilitiesKit/TSStorageHeaders.h new file mode 100644 index 000000000..7f606aea9 --- /dev/null +++ b/SignalUtilitiesKit/TSStorageHeaders.h @@ -0,0 +1,14 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#ifndef Signal_TSStorageHeaders_h +#define Signal_TSStorageHeaders_h +#import "OWSIdentityManager.h" +#import "OWSPrimaryStorage+PreKeyStore.h" +#import "OWSPrimaryStorage+SessionStore.h" +#import "OWSPrimaryStorage+SignedPreKeyStore.h" +#import "OWSPrimaryStorage+keyFromIntLong.h" +#import "OWSPrimaryStorage.h" + +#endif diff --git a/SignalUtilitiesKit/TSStorageKeys.h b/SignalUtilitiesKit/TSStorageKeys.h new file mode 100644 index 000000000..89834b775 --- /dev/null +++ b/SignalUtilitiesKit/TSStorageKeys.h @@ -0,0 +1,32 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#ifndef TextSecureKit_TSStorageKeys_h +#define TextSecureKit_TSStorageKeys_h + +/** + * Preferences exposed to the user + */ + +#pragma mark User Preferences + +#define TSStorageUserPreferencesCollection @"TSStorageUserPreferencesCollection" + + +/** + * Internal settings of the application, not exposed to the user. + */ + +#pragma mark Internal Settings + +#define TSStorageInternalSettingsCollection @"TSStorageInternalSettingsCollection" +#define TSStorageInternalSettingsVersion @"TSLastLaunchedVersion" + +#endif + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSThread.h b/SignalUtilitiesKit/TSThread.h new file mode 100644 index 000000000..5af5ec9a5 --- /dev/null +++ b/SignalUtilitiesKit/TSThread.h @@ -0,0 +1,182 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" + +NS_ASSUME_NONNULL_BEGIN + +BOOL IsNoteToSelfEnabled(void); + +@class OWSDisappearingMessagesConfiguration; +@class TSInteraction; +@class TSInvalidIdentityKeyReceivingErrorMessage; + +typedef NSString *ConversationColorName NS_STRING_ENUM; + +extern ConversationColorName const ConversationColorNameCrimson; +extern ConversationColorName const ConversationColorNameVermilion; +extern ConversationColorName const ConversationColorNameBurlap; +extern ConversationColorName const ConversationColorNameForest; +extern ConversationColorName const ConversationColorNameWintergreen; +extern ConversationColorName const ConversationColorNameTeal; +extern ConversationColorName const ConversationColorNameBlue; +extern ConversationColorName const ConversationColorNameIndigo; +extern ConversationColorName const ConversationColorNameViolet; +extern ConversationColorName const ConversationColorNamePlum; +extern ConversationColorName const ConversationColorNameTaupe; +extern ConversationColorName const ConversationColorNameSteel; + +extern ConversationColorName const kConversationColorName_Default; + +/** + * TSThread is the superclass of TSContactThread and TSGroupThread + */ +@interface TSThread : TSYapDatabaseObject + +@property (nonatomic) BOOL shouldThreadBeVisible; +@property (nonatomic, readonly) NSDate *creationDate; +@property (nonatomic, readonly) BOOL isArchivedByLegacyTimestampForSorting; +@property (nonatomic, readonly) TSInteraction *lastInteraction; +@property (nonatomic, readonly) BOOL isSlaveThread; + +/** + * Whether the object is a group thread or not. + * + * @return YES if is a group thread, NO otherwise. + */ +- (BOOL)isGroupThread; + +/** + * Returns the name of the thread. + * + * @return The name of the thread. + */ +- (NSString *)name; + +@property (nonatomic, readonly) ConversationColorName conversationColorName; + +- (void)updateConversationColorName:(ConversationColorName)colorName + transaction:(YapDatabaseReadWriteTransaction *)transaction; ++ (ConversationColorName)stableColorNameForNewConversationWithString:(NSString *)colorSeed; +@property (class, nonatomic, readonly) NSArray *conversationColorNames; + +/** + * @returns + * Signal Id (e164) of the contact if it's a contact thread. + */ +- (nullable NSString *)contactIdentifier; + +/** + * @returns recipientId for each recipient in the thread + */ +@property (nonatomic, readonly) NSArray *recipientIdentifiers; + +- (BOOL)isNoteToSelf; + +#pragma mark Interactions + +- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction usingBlock:(void (^)(TSInteraction *interaction, YapDatabaseReadTransaction *transaction))block; + +- (void)enumerateInteractionsUsingBlock:(void (^)(TSInteraction *interaction))block; + +/** + * @return The number of interactions in this thread. + */ +- (NSUInteger)numberOfInteractions; + +/** + * Get all messages in the thread we weren't able to decrypt + */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (NSArray *)receivedMessagesForInvalidKey:(NSData *)key; +#pragma clang diagnostic pop + +- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(unreadMessageCount(transaction:)); + +- (BOOL)hasSafetyNumbers; + +- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +/** + * Returns the string that will be displayed typically in a conversations view as a preview of the last message + *received in this thread. + * + * @return Thread preview string. + */ +- (NSString *)lastMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(lastMessageText(transaction:)); + +- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(lastInteractionForInbox(transaction:)); + +/** + * Updates the thread's caches of the latest interaction. + * + * @param lastMessage Latest Interaction to take into consideration. + * @param transaction Database transaction. + */ +- (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction; + +#pragma mark Archival + +/** + * @return YES if no new messages have been sent or received since the thread was last archived. + */ +- (BOOL)isArchivedWithTransaction:(YapDatabaseReadTransaction *)transaction; + +/** + * Archives a thread + * + * @param transaction Database transaction. + */ +- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +/** + * Unarchives a thread + * + * @param transaction Database transaction. + */ +- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +- (TSInteraction *)getLastInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction; + +#pragma mark Disappearing Messages + +- (OWSDisappearingMessagesConfiguration *)disappearingMessagesConfigurationWithTransaction: + (YapDatabaseReadTransaction *)transaction; +- (uint32_t)disappearingMessagesDurationWithTransaction:(YapDatabaseReadTransaction *)transaction; + +#pragma mark Drafts + +/** + * Returns the last known draft for that thread. Always returns a string. Empty string if nil. + * + * @param transaction Database transaction. + * + * @return Last known draft for that thread. + */ +- (NSString *)currentDraftWithTransaction:(YapDatabaseReadTransaction *)transaction; + +/** + * Sets the draft of a thread. Typically called when leaving a conversation view. + * + * @param draftString Draft string to be saved. + * @param transaction Database transaction. + */ +- (void)setDraft:(NSString *)draftString transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@property (atomic, readonly) BOOL isMuted; +@property (atomic, readonly, nullable) NSDate *mutedUntilDate; + +#pragma mark - Update With... Methods + +- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate transaction:(YapDatabaseReadWriteTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSThread.m b/SignalUtilitiesKit/TSThread.m new file mode 100644 index 000000000..428987440 --- /dev/null +++ b/SignalUtilitiesKit/TSThread.m @@ -0,0 +1,740 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "TSThread.h" +#import "NSString+SSK.h" +#import "OWSDisappearingMessagesConfiguration.h" +#import "OWSPrimaryStorage.h" +#import "OWSReadTracking.h" +#import "SSKEnvironment.h" +#import "TSAccountManager.h" +#import "TSDatabaseView.h" +#import "TSIncomingMessage.h" +#import "TSInfoMessage.h" +#import "TSInteraction.h" +#import "TSInvalidIdentityKeyReceivingErrorMessage.h" +#import "TSOutgoingMessage.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +BOOL IsNoteToSelfEnabled(void) +{ + return YES; +} + +ConversationColorName const ConversationColorNameCrimson = @"red"; +ConversationColorName const ConversationColorNameVermilion = @"orange"; +ConversationColorName const ConversationColorNameBurlap = @"brown"; +ConversationColorName const ConversationColorNameForest = @"green"; +ConversationColorName const ConversationColorNameWintergreen = @"light_green"; +ConversationColorName const ConversationColorNameTeal = @"teal"; +ConversationColorName const ConversationColorNameBlue = @"blue"; +ConversationColorName const ConversationColorNameIndigo = @"indigo"; +ConversationColorName const ConversationColorNameViolet = @"purple"; +ConversationColorName const ConversationColorNamePlum = @"pink"; +ConversationColorName const ConversationColorNameTaupe = @"blue_grey"; +ConversationColorName const ConversationColorNameSteel = @"grey"; + +ConversationColorName const kConversationColorName_Default = ConversationColorNameSteel; + +@interface TSThread () + +@property (nonatomic) NSDate *creationDate; +@property (nonatomic) NSString *conversationColorName; +@property (nonatomic, nullable) NSNumber *archivedAsOfMessageSortId; +@property (nonatomic, copy, nullable) NSString *messageDraft; +@property (atomic, nullable) NSDate *mutedUntilDate; + +// DEPRECATED - not used since migrating to sortId +// but keeping these properties around to ease any pain in the back-forth +// migration while testing. Eventually we can safely delete these as they aren't used anywhere. +@property (nonatomic, nullable) NSDate *lastMessageDate DEPRECATED_ATTRIBUTE; +@property (nonatomic, nullable) NSDate *archivalDate DEPRECATED_ATTRIBUTE; + +@end + +#pragma mark - + +@implementation TSThread + +#pragma mark - Dependencies + +- (TSAccountManager *)tsAccountManager +{ + OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); + + return SSKEnvironment.shared.tsAccountManager; +} + +#pragma mark - + ++ (NSString *)collection { + return @"TSThread"; +} + +- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId +{ + self = [super initWithUniqueId:uniqueId]; + + if (self) { + _creationDate = [NSDate date]; + _messageDraft = nil; + + NSString *_Nullable contactId = self.contactIdentifier; + if (contactId.length > 0) { + // To be consistent with colors synced to desktop + _conversationColorName = [self.class stableColorNameForNewConversationWithString:contactId]; + } else { + _conversationColorName = [self.class stableColorNameForNewConversationWithString:self.uniqueId]; + } + } + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + // renamed `hasEverHadMessage` -> `shouldThreadBeVisible` + if (!_shouldThreadBeVisible) { + NSNumber *_Nullable legacy_hasEverHadMessage = [coder decodeObjectForKey:@"hasEverHadMessage"]; + + if (legacy_hasEverHadMessage != nil) { + _shouldThreadBeVisible = legacy_hasEverHadMessage.boolValue; + } + } + + if (_conversationColorName.length == 0) { + NSString *_Nullable colorSeed = self.contactIdentifier; + if (colorSeed.length > 0) { + // group threads + colorSeed = self.uniqueId; + } + + // To be consistent with colors synced to desktop + ConversationColorName colorName = [self.class stableColorNameForLegacyConversationWithString:colorSeed]; + OWSAssertDebug(colorName); + + _conversationColorName = colorName; + } else if (![[[self class] conversationColorNames] containsObject:_conversationColorName]) { + // If we'd persisted a non-mapped color name + ConversationColorName _Nullable mappedColorName = self.class.legacyConversationColorMap[_conversationColorName]; + + if (!mappedColorName) { + // We previously used the wrong values for the new colors, it's possible we persited them. + // map them to the proper value + mappedColorName = self.class.legacyFixupConversationColorMap[_conversationColorName]; + } + + if (!mappedColorName) { + OWSFailDebug(@"failure: unexpected unmappable conversationColorName: %@", _conversationColorName); + mappedColorName = kConversationColorName_Default; + } + + _conversationColorName = mappedColorName; + } + + NSDate *_Nullable lastMessageDate = [coder decodeObjectOfClass:NSDate.class forKey:@"lastMessageDate"]; + NSDate *_Nullable archivalDate = [coder decodeObjectOfClass:NSDate.class forKey:@"archivalDate"]; + _isArchivedByLegacyTimestampForSorting = + [self.class legacyIsArchivedWithLastMessageDate:lastMessageDate archivalDate:archivalDate]; + + return self; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [super saveWithTransaction:transaction]; + + [SSKPreferences setHasSavedThreadWithValue:YES transaction:transaction]; +} + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self removeAllThreadInteractionsWithTransaction:transaction]; + + [super removeWithTransaction:transaction]; +} + +- (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + // We can't safely delete interactions while enumerating them, so + // we collect and delete separately. + // + // We don't want to instantiate the interactions when collecting them + // or when deleting them. + NSMutableArray *interactionIds = [NSMutableArray new]; + YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName]; + OWSAssertDebug(interactionsByThread); + __block BOOL didDetectCorruption = NO; + [interactionsByThread enumerateKeysInGroup:self.uniqueId + usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) { + if (![key isKindOfClass:[NSString class]] || key.length < 1) { + OWSFailDebug( + @"invalid key in thread interactions: %@, %@.", key, [key class]); + didDetectCorruption = YES; + return; + } + [interactionIds addObject:key]; + }]; + + if (didDetectCorruption) { + OWSLogWarn(@"incrementing version of: %@", TSMessageDatabaseViewExtensionName); + [OWSPrimaryStorage incrementVersionOfDatabaseExtension:TSMessageDatabaseViewExtensionName]; + } + + for (NSString *interactionId in interactionIds) { + // We need to fetch each interaction, since [TSInteraction removeWithTransaction:] does important work. + TSInteraction *_Nullable interaction = + [TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction]; + if (!interaction) { + OWSFailDebug(@"couldn't load thread's interaction for deletion."); + continue; + } + [interaction removeWithTransaction:transaction]; + } +} + +- (BOOL)isNoteToSelf +{ + if (!IsNoteToSelfEnabled()) { + return NO; + } + return [LKSessionMetaProtocol isThreadNoteToSelf:self]; +} + +#pragma mark - To be subclassed. + +- (BOOL)isGroupThread { + OWSAbstractMethod(); + + return NO; +} + +// Override in ContactThread +- (nullable NSString *)contactIdentifier +{ + return nil; +} + +- (NSString *)name { + OWSAbstractMethod(); + + return nil; +} + +- (NSArray *)recipientIdentifiers +{ + OWSAbstractMethod(); + + return @[]; +} + +- (BOOL)hasSafetyNumbers +{ + return NO; +} + +#pragma mark - Interactions + +/** + * Iterate over this thread's interactions + */ +- (void)enumerateInteractionsWithTransaction:(YapDatabaseReadTransaction *)transaction + usingBlock:(void (^)(TSInteraction *interaction, + YapDatabaseReadTransaction *transaction))block +{ + YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName]; + [interactionsByThread + enumerateKeysAndObjectsInGroup:self.uniqueId + usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) { + TSInteraction *interaction = object; + block(interaction, transaction); + }]; +} + +/** + * Enumerates all the threads interactions. Note this will explode if you try to create a transaction in the block. + * If you need a transaction, use the sister method: `enumerateInteractionsWithTransaction:usingBlock` + */ +- (void)enumerateInteractionsUsingBlock:(void (^)(TSInteraction *interaction))block +{ + [self.dbReadWriteConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self enumerateInteractionsWithTransaction:transaction + usingBlock:^( + TSInteraction *interaction, YapDatabaseReadTransaction *transaction) { + + block(interaction); + }]; + }]; +} + +- (TSInteraction *)lastInteraction +{ + __block TSInteraction *interaction; + [self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + interaction = [self getLastInteractionWithTransaction:transaction]; + }]; + return interaction; +} + +- (TSInteraction *)getLastInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + YapDatabaseViewTransaction *interactions = [transaction ext:TSMessageDatabaseViewExtensionName]; + return [interactions lastObjectInGroup:self.uniqueId]; +} + +/** + * Useful for tests and debugging. In production use an enumeration method. + */ +- (NSArray *)allInteractions +{ + NSMutableArray *interactions = [NSMutableArray new]; + [self enumerateInteractionsUsingBlock:^(TSInteraction *interaction) { + [interactions addObject:interaction]; + }]; + + return [interactions copy]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (NSArray *)receivedMessagesForInvalidKey:(NSData *)key +{ + NSMutableArray *errorMessages = [NSMutableArray new]; + [self enumerateInteractionsUsingBlock:^(TSInteraction *interaction) { + if ([interaction isKindOfClass:[TSInvalidIdentityKeyReceivingErrorMessage class]]) { + TSInvalidIdentityKeyReceivingErrorMessage *error = (TSInvalidIdentityKeyReceivingErrorMessage *)interaction; + @try { + if ([[error throws_newIdentityKey] isEqualToData:key]) { + [errorMessages addObject:(TSInvalidIdentityKeyReceivingErrorMessage *)interaction]; + } + } @catch (NSException *exception) { + OWSFailDebug(@"exception: %@", exception); + } + } + }]; + + return [errorMessages copy]; +} +#pragma clang diagnostic pop + +- (NSUInteger)numberOfInteractions +{ + __block NSUInteger count; + [[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) { + YapDatabaseViewTransaction *interactionsByThread = [transaction ext:TSMessageDatabaseViewExtensionName]; + count = [interactionsByThread numberOfItemsInGroup:self.uniqueId]; + }]; + return count; +} + +- (NSArray> *)unseenMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + NSMutableArray> *messages = [NSMutableArray new]; + [[TSDatabaseView unseenDatabaseViewExtension:transaction] + enumerateKeysAndObjectsInGroup:self.uniqueId + usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) { + if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { + OWSFailDebug(@"Unexpected object in unseen messages: %@", [object class]); + return; + } + id unread = (id)object; + if (unread.read) { + [LKLogger print:@"Found an already read message in the * unseen * messages list."]; + return; + } + [messages addObject:unread]; + }]; + + return [messages copy]; +} + +- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + __block NSUInteger count = 0; + + YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName]; + [unreadMessages enumerateKeysAndObjectsInGroup:self.uniqueId + usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) { + if (![object conformsToProtocol:@protocol(OWSReadTracking)]) { + OWSFailDebug(@"Unexpected object in unread messages: %@", [object class]); + return; + } + id unread = (id)object; + if (unread.read) { + [LKLogger print:@"Found an already read message in the * unread * messages list."]; + return; + } + count += 1; + }]; + + return count; +} + +- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + for (id message in [self unseenMessagesWithTransaction:transaction]) { + [message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] sendReadReceipt:YES transaction:transaction]; + } + + // Just to be defensive, we'll also check for unread messages. + OWSAssertDebug([self unseenMessagesWithTransaction:transaction].count < 1); +} + +- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + OWSAssertDebug(transaction); + + __block NSUInteger missedCount = 0; + __block TSInteraction *last = nil; + [[transaction ext:TSMessageDatabaseViewExtensionName] + enumerateKeysAndObjectsInGroup:self.uniqueId + withOptions:NSEnumerationReverse + usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) { + OWSAssertDebug([object isKindOfClass:[TSInteraction class]]); + + missedCount++; + TSInteraction *interaction = (TSInteraction *)object; + + if ([TSThread shouldInteractionAppearInInbox:interaction]) { + last = interaction; + + // For long ignored threads, with lots of SN changes this can get really slow. + // I see this in development because I have a lot of long forgotten threads with + // members who's test devices are constantly reinstalled. We could add a + // purpose-built DB view, but I think in the real world this is rare to be a + // hotspot. + if (missedCount > 50) { + OWSLogWarn(@"found last interaction for inbox after skipping %lu items", + (unsigned long)missedCount); + } + *stop = YES; + } + }]; + return last; +} + +- (NSString *)lastMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + TSInteraction *interaction = [self lastInteractionForInboxWithTransaction:transaction]; + if ([interaction conformsToProtocol:@protocol(OWSPreviewText)]) { + id previewable = (id)interaction; + return [previewable previewTextWithTransaction:transaction].filterStringForDisplay; + } else { + return @""; + } +} + +// Returns YES IFF the interaction should show up in the inbox as the last message. ++ (BOOL)shouldInteractionAppearInInbox:(TSInteraction *)interaction +{ + OWSAssertDebug(interaction); + + if (interaction.isDynamicInteraction) { + return NO; + } + + if ([interaction isKindOfClass:[TSErrorMessage class]]) { + TSErrorMessage *errorMessage = (TSErrorMessage *)interaction; + if (errorMessage.errorType == TSErrorMessageNonBlockingIdentityChange) { + // Otherwise all group threads with the recipient will percolate to the top of the inbox, even though + // there was no meaningful interaction. + return NO; + } + } else if ([interaction isKindOfClass:[TSInfoMessage class]]) { + TSInfoMessage *infoMessage = (TSInfoMessage *)interaction; + if (infoMessage.messageType == TSInfoMessageVerificationStateChange) { + return NO; + } + } + + return YES; +} + +- (void)updateWithLastMessage:(TSInteraction *)lastMessage transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssertDebug(lastMessage); + OWSAssertDebug(transaction); + + if (![self.class shouldInteractionAppearInInbox:lastMessage]) { + return; + } + + if (!self.shouldThreadBeVisible) { + self.shouldThreadBeVisible = YES; + [self saveWithTransaction:transaction]; + } else { + [self touchWithTransaction:transaction]; + } +} + +#pragma mark - Disappearing Messages + +- (OWSDisappearingMessagesConfiguration *)disappearingMessagesConfigurationWithTransaction: + (YapDatabaseReadTransaction *)transaction +{ + return [OWSDisappearingMessagesConfiguration fetchOrBuildDefaultWithThreadId:self.uniqueId transaction:transaction]; +} + +- (uint32_t)disappearingMessagesDurationWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + + OWSDisappearingMessagesConfiguration *config = [self disappearingMessagesConfigurationWithTransaction:transaction]; + + if (!config.isEnabled) { + return 0; + } else { + return config.durationSeconds; + } +} + +#pragma mark - Archival + +- (BOOL)isArchivedWithTransaction:(YapDatabaseReadTransaction *)transaction; +{ + if (!self.archivedAsOfMessageSortId) { + return NO; + } + + TSInteraction *_Nullable latestInteraction = [self lastInteractionForInboxWithTransaction:transaction]; + uint64_t latestSortIdForInbox = latestInteraction ? latestInteraction.sortId : 0; + return self.archivedAsOfMessageSortId.unsignedLongLongValue >= latestSortIdForInbox; +} + ++ (BOOL)legacyIsArchivedWithLastMessageDate:(nullable NSDate *)lastMessageDate + archivalDate:(nullable NSDate *)archivalDate +{ + if (!archivalDate) { + return NO; + } + + if (!lastMessageDate) { + return YES; + } + + return [archivalDate compare:lastMessageDate] != NSOrderedAscending; +} + +- (void)archiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSThread *thread) { + uint64_t latestId = [SSKIncrementingIdFinder previousIdWithKey:TSInteraction.collection + transaction:transaction]; + thread.archivedAsOfMessageSortId = @(latestId); + }]; + + [self markAllAsReadWithTransaction:transaction]; +} + +- (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSThread *thread) { + thread.archivedAsOfMessageSortId = nil; + }]; +} + +#pragma mark - Drafts + +- (NSString *)currentDraftWithTransaction:(YapDatabaseReadTransaction *)transaction { + TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction]; + if (thread.messageDraft) { + return thread.messageDraft; + } else { + return @""; + } +} + +- (void)setDraft:(NSString *)draftString transaction:(YapDatabaseReadWriteTransaction *)transaction { + TSThread *thread = [TSThread fetchObjectWithUniqueID:self.uniqueId transaction:transaction]; + thread.messageDraft = draftString; + [thread saveWithTransaction:transaction]; +} + +#pragma mark - Muted + +- (BOOL)isMuted +{ + NSDate *mutedUntilDate = self.mutedUntilDate; + NSDate *now = [NSDate date]; + return (mutedUntilDate != nil && + [mutedUntilDate timeIntervalSinceDate:now] > 0); +} + +- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSThread *thread) { + [thread setMutedUntilDate:mutedUntilDate]; + }]; +} + +#pragma mark - Conversation Color + +- (ConversationColorName)conversationColorName +{ + OWSAssertDebug([self.class.conversationColorNames containsObject:_conversationColorName]); + return _conversationColorName; +} + ++ (NSArray *)colorNamesForNewConversation +{ + // all conversation colors except "steel" + return @[ + ConversationColorNameCrimson, + ConversationColorNameVermilion, + ConversationColorNameBurlap, + ConversationColorNameForest, + ConversationColorNameWintergreen, + ConversationColorNameTeal, + ConversationColorNameBlue, + ConversationColorNameIndigo, + ConversationColorNameViolet, + ConversationColorNamePlum, + ConversationColorNameTaupe, + ]; +} + ++ (NSArray *)conversationColorNames +{ + return [self.colorNamesForNewConversation arrayByAddingObject:kConversationColorName_Default]; +} + ++ (ConversationColorName)stableConversationColorNameForString:(NSString *)colorSeed + colorNames:(NSArray *)colorNames +{ + NSData *contactData = [colorSeed dataUsingEncoding:NSUTF8StringEncoding]; + + unsigned long long hash = 0; + NSUInteger hashingLength = sizeof(hash); + NSData *_Nullable hashData = [Cryptography computeSHA256Digest:contactData truncatedToBytes:hashingLength]; + if (hashData) { + [hashData getBytes:&hash length:hashingLength]; + } else { + OWSFailDebug(@"could not compute hash for color seed."); + } + + NSUInteger index = (hash % colorNames.count); + return [colorNames objectAtIndex:index]; +} + ++ (ConversationColorName)stableColorNameForNewConversationWithString:(NSString *)colorSeed +{ + return [self stableConversationColorNameForString:colorSeed colorNames:self.colorNamesForNewConversation]; +} + +// After introducing new conversation colors, we want to try to maintain as close as possible to the old color for an +// existing thread. ++ (ConversationColorName)stableColorNameForLegacyConversationWithString:(NSString *)colorSeed +{ + NSString *legacyColorName = + [self stableConversationColorNameForString:colorSeed colorNames:self.legacyConversationColorNames]; + ConversationColorName _Nullable mappedColorName = self.class.legacyConversationColorMap[legacyColorName]; + + if (!mappedColorName) { + OWSFailDebug(@"failure: unexpected unmappable legacyColorName: %@", legacyColorName); + return kConversationColorName_Default; + } + + return mappedColorName; +} + ++ (NSArray *)legacyConversationColorNames +{ + return @[ + @"red", + @"pink", + @"purple", + @"indigo", + @"blue", + @"cyan", + @"teal", + @"green", + @"deep_orange", + @"grey" + ]; +} + ++ (NSDictionary *)legacyConversationColorMap +{ + static NSDictionary *colorMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + colorMap = @{ + @"red" : ConversationColorNameCrimson, + @"deep_orange" : ConversationColorNameCrimson, + @"orange" : ConversationColorNameVermilion, + @"amber" : ConversationColorNameVermilion, + @"brown" : ConversationColorNameBurlap, + @"yellow" : ConversationColorNameBurlap, + @"pink" : ConversationColorNamePlum, + @"purple" : ConversationColorNameViolet, + @"deep_purple" : ConversationColorNameViolet, + @"indigo" : ConversationColorNameIndigo, + @"blue" : ConversationColorNameBlue, + @"light_blue" : ConversationColorNameBlue, + @"cyan" : ConversationColorNameTeal, + @"teal" : ConversationColorNameTeal, + @"green" : ConversationColorNameForest, + @"light_green" : ConversationColorNameWintergreen, + @"lime" : ConversationColorNameWintergreen, + @"blue_grey" : ConversationColorNameTaupe, + @"grey" : ConversationColorNameSteel, + }; + }); + + return colorMap; +} + +// we temporarily used the wrong value for the new color names. ++ (NSDictionary *)legacyFixupConversationColorMap +{ + static NSDictionary *colorMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + colorMap = @{ + @"crimson" : ConversationColorNameCrimson, + @"vermilion" : ConversationColorNameVermilion, + @"burlap" : ConversationColorNameBurlap, + @"forest" : ConversationColorNameForest, + @"wintergreen" : ConversationColorNameWintergreen, + @"teal" : ConversationColorNameTeal, + @"blue" : ConversationColorNameBlue, + @"indigo" : ConversationColorNameIndigo, + @"violet" : ConversationColorNameViolet, + @"plum" : ConversationColorNamePlum, + @"taupe" : ConversationColorNameTaupe, + @"steel" : ConversationColorNameSteel, + }; + }); + + return colorMap; +} + +- (void)updateConversationColorName:(ConversationColorName)colorName + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSThread *thread) { + thread.conversationColorName = colorName; + }]; +} + +- (BOOL)isSlaveThread +{ + return [LKMultiDeviceProtocol isSlaveThread:self]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSYapDatabaseObject.h b/SignalUtilitiesKit/TSYapDatabaseObject.h new file mode 100644 index 000000000..cb95b2ee4 --- /dev/null +++ b/SignalUtilitiesKit/TSYapDatabaseObject.h @@ -0,0 +1,166 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OWSPrimaryStorage; +@class YapDatabaseConnection; +@class YapDatabaseReadTransaction; +@class YapDatabaseReadWriteTransaction; + +@interface TSYapDatabaseObject : MTLModel + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Initializes a new database object with a unique identifier + * + * @param uniqueId Key used for the key-value store + * + * @return Initialized object + */ +- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId NS_DESIGNATED_INITIALIZER; + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +/** + * Returns the collection to which the object belongs. + * + * @return Key (string) identifying the collection + */ ++ (NSString *)collection; + +/** + * Get the number of keys in the models collection. Be aware that if there + * are multiple object types in this collection that the count will include + * the count of other objects in the same collection. + * + * @return The number of keys in the classes collection. + */ ++ (NSUInteger)numberOfKeysInCollection; ++ (NSUInteger)numberOfKeysInCollectionWithTransaction:(YapDatabaseReadTransaction *)transaction; + +/** + * Removes all objects in the classes collection. + */ ++ (void)removeAllObjectsInCollection; + +/** + * A memory intesive method to get all objects in the collection. You should prefer using enumeration over this method + * whenever feasible. See `enumerateObjectsInCollectionUsingBlock` + * + * @return All objects in the classes collection. + */ ++ (NSArray *)allObjectsInCollection; + +/** + * Enumerates all objects in collection. + */ ++ (void)enumerateCollectionObjectsUsingBlock:(void (^)(id obj, BOOL *stop))block; ++ (void)enumerateCollectionObjectsWithTransaction:(YapDatabaseReadTransaction *)transaction + usingBlock:(void (^)(id object, BOOL *stop))block; + +/** + * @return Shared database connections for reading and writing. + */ +- (YapDatabaseConnection *)dbReadConnection; ++ (YapDatabaseConnection *)dbReadConnection; +- (YapDatabaseConnection *)dbReadWriteConnection; ++ (YapDatabaseConnection *)dbReadWriteConnection; + +- (OWSPrimaryStorage *)primaryStorage; ++ (OWSPrimaryStorage *)primaryStorage; + +/** + * Fetches the object with the provided identifier + * + * @param uniqueID Unique identifier of the entry in a collection + * @param transaction Transaction used for fetching the object + * + * @return Instance of the object or nil if non-existent + */ ++ (nullable instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID + transaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(fetch(uniqueId:transaction:)); ++ (nullable instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID NS_SWIFT_NAME(fetch(uniqueId:)); + +/** + * Saves the object with the shared readWrite connection. + * + * This method will block if another readWrite transaction is open. + */ +- (void)save; + +/** + * Assign the latest persisted values from the database. + */ +- (void)reload; +- (void)reloadWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (void)reloadWithTransaction:(YapDatabaseReadTransaction *)transaction ignoreMissing:(BOOL)ignoreMissing; + +/** + * Saves the object with the shared readWrite connection - does not block. + * + * Be mindful that this method may clobber other changes persisted + * while waiting to open the readWrite transaction. + * + * @param completionBlock is called on the main thread + */ +- (void)saveAsyncWithCompletionBlock:(void (^_Nullable)(void))completionBlock; + +/** + * Saves the object with the provided transaction + * + * @param transaction Database transaction + */ +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +/** + * `touch` is a cheap way to fire a YapDatabaseModified notification to redraw anythign depending on the model. + */ +- (void)touch; +- (void)touchWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + +/** + * The unique identifier of the stored object + */ +@property (nonatomic, nullable) NSString *uniqueId; + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)remove; + +#pragma mark - Update With... + +// This method is used by "updateWith..." methods. +// +// This model may be updated from many threads. We don't want to save +// our local copy (this instance) since it may be out of date. We also +// want to avoid re-saving a model that has been deleted. Therefore, we +// use "updateWith..." methods to: +// +// a) Update a property of this instance. +// b) If a copy of this model exists in the database, load an up-to-date copy, +// and update and save that copy. +// b) If a copy of this model _DOES NOT_ exist in the database, do _NOT_ save +// this local instance. +// +// After "updateWith...": +// +// a) Any copy of this model in the database will have been updated. +// b) The local property on this instance will always have been updated. +// c) Other properties on this instance may be out of date. +// +// All mutable properties of this class have been made read-only to +// prevent accidentally modifying them directly. +// +// This isn't a perfect arrangement, but in practice this will prevent +// data loss and will resolve all known issues. +- (void)applyChangeToSelfAndLatestCopy:(YapDatabaseReadWriteTransaction *)transaction + changeBlock:(void (^)(id))changeBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TSYapDatabaseObject.m b/SignalUtilitiesKit/TSYapDatabaseObject.m new file mode 100644 index 000000000..21c313b61 --- /dev/null +++ b/SignalUtilitiesKit/TSYapDatabaseObject.m @@ -0,0 +1,250 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "TSYapDatabaseObject.h" +#import "OWSPrimaryStorage.h" +#import "SSKEnvironment.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation TSYapDatabaseObject + +- (instancetype)init +{ + return [self initWithUniqueId:[[NSUUID UUID] UUIDString]]; +} + +- (instancetype)initWithUniqueId:(NSString *_Nullable)aUniqueId +{ + self = [super init]; + if (!self) { + return self; + } + + _uniqueId = aUniqueId; + + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + return self; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [transaction setObject:self forKey:self.uniqueId inCollection:[[self class] collection]]; +} + +- (void)save +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self saveWithTransaction:transaction]; + }]; +} + +- (void)saveAsyncWithCompletionBlock:(void (^_Nullable)(void))completionBlock +{ + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self saveWithTransaction:transaction]; + } completion:completionBlock]; +} + +- (void)touchWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [transaction touchObjectForKey:self.uniqueId inCollection:[self.class collection]]; +} + +- (void)touch +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self touchWithTransaction:transaction]; + }]; +} + +- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [transaction removeObjectForKey:self.uniqueId inCollection:[[self class] collection]]; +} + +- (void)remove +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [self removeWithTransaction:transaction]; + }]; +} + +- (YapDatabaseConnection *)dbReadConnection +{ + return [[self class] dbReadConnection]; +} + +- (YapDatabaseConnection *)dbReadWriteConnection +{ + return [[self class] dbReadWriteConnection]; +} + +- (OWSPrimaryStorage *)primaryStorage +{ + return [[self class] primaryStorage]; +} + +#pragma mark Class Methods + ++ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey +{ + if ([propertyKey isEqualToString:@"TAG"]) { + return MTLPropertyStorageNone; + } else { + return [super storageBehaviorForPropertyWithKey:propertyKey]; + } +} + ++ (YapDatabaseConnection *)dbReadConnection +{ + OWSJanksUI(); + + // We use TSYapDatabaseObject's dbReadWriteConnection (not OWSPrimaryStorage's + // dbReadConnection) for consistency, since we tend to [TSYapDatabaseObject + // save] and want to write to the same connection we read from. To get true + // consistency, we'd want to update entities by reading & writing from within + // the same transaction, but that'll be a big refactor. + return self.dbReadWriteConnection; +} + ++ (YapDatabaseConnection *)dbReadWriteConnection +{ + OWSJanksUI(); + + return SSKEnvironment.shared.objectReadWriteConnection; +} + ++ (OWSPrimaryStorage *)primaryStorage +{ + return [OWSPrimaryStorage sharedManager]; +} + ++ (NSString *)collection +{ + return NSStringFromClass([self class]); +} + ++ (NSUInteger)numberOfKeysInCollection +{ + __block NSUInteger count; + [[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) { + count = [self numberOfKeysInCollectionWithTransaction:transaction]; + }]; + return count; +} + ++ (NSUInteger)numberOfKeysInCollectionWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + return [transaction numberOfKeysInCollection:[self collection]]; +} + ++ (NSArray *)allObjectsInCollection +{ + __block NSMutableArray *all = [[NSMutableArray alloc] initWithCapacity:[self numberOfKeysInCollection]]; + [self enumerateCollectionObjectsUsingBlock:^(id object, BOOL *stop) { + [all addObject:object]; + }]; + return [all copy]; +} + ++ (void)enumerateCollectionObjectsUsingBlock:(void (^)(id object, BOOL *stop))block +{ + [[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) { + [self enumerateCollectionObjectsWithTransaction:transaction usingBlock:block]; + }]; +} + ++ (void)enumerateCollectionObjectsWithTransaction:(YapDatabaseReadTransaction *)transaction + usingBlock:(void (^)(id object, BOOL *stop))block +{ + // Ignoring most of the YapDB parameters, and just passing through the ones we usually use. + void (^yapBlock)(NSString *key, id object, id metadata, BOOL *stop) + = ^void(NSString *key, id object, id metadata, BOOL *stop) { + block(object, stop); + }; + + [transaction enumerateRowsInCollection:[self collection] usingBlock:yapBlock]; +} + ++ (void)removeAllObjectsInCollection +{ + [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction removeAllObjectsInCollection:[self collection]]; + }]; +} + ++ (nullable instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID + transaction:(YapDatabaseReadTransaction *)transaction +{ + return [transaction objectForKey:uniqueID inCollection:[self collection]]; +} + ++ (nullable instancetype)fetchObjectWithUniqueID:(NSString *)uniqueID +{ + __block id _Nullable object = nil; + [[self dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *transaction) { + object = [transaction objectForKey:uniqueID inCollection:[self collection]]; + }]; + return object; +} + +#pragma mark - Update With... + +- (void)applyChangeToSelfAndLatestCopy:(YapDatabaseReadWriteTransaction *)transaction + changeBlock:(void (^)(id))changeBlock +{ + OWSAssertDebug(transaction); + + changeBlock(self); + + NSString *collection = [[self class] collection]; + id latestInstance = [transaction objectForKey:self.uniqueId inCollection:collection]; + if (latestInstance) { + changeBlock(latestInstance); + [latestInstance saveWithTransaction:transaction]; + } +} + +#pragma mark Reload + +- (void)reload +{ + [self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + [self reloadWithTransaction:transaction]; + }]; +} + +- (void)reloadWithTransaction:(YapDatabaseReadTransaction *)transaction +{ + [self reloadWithTransaction:transaction ignoreMissing:NO]; +} + +- (void)reloadWithTransaction:(YapDatabaseReadTransaction *)transaction ignoreMissing:(BOOL)ignoreMissing +{ + TSYapDatabaseObject *latest = [[self class] fetchObjectWithUniqueID:self.uniqueId transaction:transaction]; + if (!latest) { + if (!ignoreMissing) { + OWSFailDebug(@"`latest` was unexpectedly nil"); + } + return; + } + + [self setValuesForKeysWithDictionary:latest.dictionaryValue]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/TTLUtilities.swift b/SignalUtilitiesKit/TTLUtilities.swift new file mode 100644 index 000000000..ebe8219db --- /dev/null +++ b/SignalUtilitiesKit/TTLUtilities.swift @@ -0,0 +1,32 @@ + +@objc(LKTTLUtilities) +public final class TTLUtilities : NSObject { + + /// If a message type specifies an invalid TTL, this will be used. + public static let fallbackMessageTTL: UInt64 = 2 * kDayInMs + + @objc(LKMessageType) + public enum MessageType : Int { + // Unimportant control messages + case call, typingIndicator + // Somewhat important control messages + case linkDevice + // Important control messages + case closedGroupUpdate, disappearingMessagesConfiguration, ephemeral, profileKey, receipt, sessionRequest, sync, unlinkDevice + // Visible messages + case regular + } + + @objc public static func getTTL(for messageType: MessageType) -> UInt64 { + switch messageType { + // Unimportant control messages + case .call, .typingIndicator: return 1 * kMinuteInMs + // Somewhat important control messages + case .linkDevice: return 1 * kHourInMs + // Important control messages + case .closedGroupUpdate, .disappearingMessagesConfiguration, .ephemeral, .profileKey, .receipt, .sessionRequest, .sync, .unlinkDevice: return 2 * kDayInMs - 1 * kHourInMs + // Visible messages + case .regular: return 2 * kDayInMs + } + } +} diff --git a/SignalUtilitiesKit/TypingIndicatorMessage.swift b/SignalUtilitiesKit/TypingIndicatorMessage.swift new file mode 100644 index 000000000..1788289c1 --- /dev/null +++ b/SignalUtilitiesKit/TypingIndicatorMessage.swift @@ -0,0 +1,122 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc(OWSTypingIndicatorAction) +public enum TypingIndicatorAction: Int { + case started + case stopped +} + +@objc(OWSTypingIndicatorMessage) +public class TypingIndicatorMessage: TSOutgoingMessage { + private let action: TypingIndicatorAction + + // MARK: Initializers + + @objc + public init(thread: TSThread, + action: TypingIndicatorAction) { + self.action = action + + super.init(outgoingMessageWithTimestamp: NSDate.millisecondTimestamp(), + in: thread, + messageBody: nil, + attachmentIds: NSMutableArray(), + expiresInSeconds: 0, + expireStartedAt: 0, + isVoiceMessage: false, + groupMetaMessage: .unspecified, + quotedMessage: nil, + contactShare: nil, + linkPreview: nil) + } + + @objc + public required init!(coder: NSCoder) { + self.action = .started + super.init(coder: coder) + } + + @objc + public required init(dictionary dictionaryValue: [String: Any]!) throws { + self.action = .started + try super.init(dictionary: dictionaryValue) + } + + @objc + public override func shouldSyncTranscript() -> Bool { + return false + } + + @objc + public override var isSilent: Bool { + return true + } + + @objc + public override var isOnline: Bool { + return true + } + + private func protoAction(forAction action: TypingIndicatorAction) -> SSKProtoTypingMessage.SSKProtoTypingMessageAction { + switch action { + case .started: + return .started + case .stopped: + return .stopped + } + } + + @objc + public override func buildPlainTextData(_ recipient: SignalRecipient) -> Data? { + + let typingBuilder = SSKProtoTypingMessage.builder(timestamp: self.timestamp, + action: protoAction(forAction: action)) + + if let groupThread = self.thread as? TSGroupThread { + typingBuilder.setGroupID(groupThread.groupModel.groupId) + } + + let contentBuilder = SSKProtoContent.builder() + + do { + contentBuilder.setTypingMessage(try typingBuilder.build()) + + let data = try contentBuilder.buildSerializedData() + return data + } catch let error { + owsFailDebug("failed to build content: \(error)") + return nil + } + } + + // MARK: TSYapDatabaseObject overrides + + @objc + public override func shouldBeSaved() -> Bool { + return false + } + + @objc + public override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .typingIndicator)) } + + @objc + public override var debugDescription: String { + return "typingIndicatorMessage" + } + + // MARK: + + @objc(stringForTypingIndicatorAction:) + public class func string(forTypingIndicatorAction action: TypingIndicatorAction) -> String { + switch action { + case .started: + return "started" + case .stopped: + return "stopped" + } + } +} diff --git a/SignalUtilitiesKit/TypingIndicators.swift b/SignalUtilitiesKit/TypingIndicators.swift new file mode 100644 index 000000000..d0e7ebd73 --- /dev/null +++ b/SignalUtilitiesKit/TypingIndicators.swift @@ -0,0 +1,459 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc(OWSTypingIndicators) +public protocol TypingIndicators: class { + @objc + func didStartTypingOutgoingInput(inThread thread: TSThread) + + @objc + func didStopTypingOutgoingInput(inThread thread: TSThread) + + @objc + func didSendOutgoingMessage(inThread thread: TSThread) + + @objc + func didReceiveTypingStartedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) + + @objc + func didReceiveTypingStoppedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) + + @objc + func didReceiveIncomingMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) + + // Returns the recipient id of the user who should currently be shown typing for a given thread. + // + // If no one is typing in that thread, returns nil. + // If multiple users are typing in that thread, returns the user to show. + // + // TODO: Use this method. + @objc + func typingRecipientId(forThread thread: TSThread) -> String? + + @objc + func setTypingIndicatorsEnabled(value: Bool) + + @objc + func areTypingIndicatorsEnabled() -> Bool +} + +// MARK: - + +@objc(OWSTypingIndicatorsImpl) +public class TypingIndicatorsImpl: NSObject, TypingIndicators { + + @objc + public static let typingIndicatorStateDidChange = Notification.Name("typingIndicatorStateDidChange") + + private let kDatabaseCollection = "TypingIndicators" + private let kDatabaseKey_TypingIndicatorsEnabled = "kDatabaseKey_TypingIndicatorsEnabled" + + private var _areTypingIndicatorsEnabled = false + + public override init() { + super.init() + + AppReadiness.runNowOrWhenAppWillBecomeReady { + self.setup() + } + } + + private func setup() { + AssertIsOnMainThread() + + _areTypingIndicatorsEnabled = primaryStorage.dbReadConnection.bool(forKey: kDatabaseKey_TypingIndicatorsEnabled, inCollection: kDatabaseCollection, defaultValue: true) + } + + // MARK: - Dependencies + + private var primaryStorage: OWSPrimaryStorage { + return SSKEnvironment.shared.primaryStorage + } + + private var syncManager: OWSSyncManagerProtocol { + return SSKEnvironment.shared.syncManager + } + + // MARK: - + + @objc + public func setTypingIndicatorsEnabled(value: Bool) { + AssertIsOnMainThread() + Logger.info("\(_areTypingIndicatorsEnabled) -> \(value)") + _areTypingIndicatorsEnabled = value + + primaryStorage.dbReadWriteConnection.setBool(value, forKey: kDatabaseKey_TypingIndicatorsEnabled, inCollection: kDatabaseCollection) + + syncManager.sendConfigurationSyncMessage() + + NotificationCenter.default.postNotificationNameAsync(TypingIndicatorsImpl.typingIndicatorStateDidChange, object: nil) + } + + @objc + public func areTypingIndicatorsEnabled() -> Bool { + AssertIsOnMainThread() + + return _areTypingIndicatorsEnabled + } + + // MARK: - + + @objc + public func didStartTypingOutgoingInput(inThread thread: TSThread) { + AssertIsOnMainThread() + guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else { + owsFailDebug("Could not locate outgoing indicators state") + return + } + outgoingIndicators.didStartTypingOutgoingInput() + } + + @objc + public func didStopTypingOutgoingInput(inThread thread: TSThread) { + AssertIsOnMainThread() + guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else { + owsFailDebug("Could not locate outgoing indicators state") + return + } + outgoingIndicators.didStopTypingOutgoingInput() + } + + @objc + public func didSendOutgoingMessage(inThread thread: TSThread) { + AssertIsOnMainThread() + guard let outgoingIndicators = ensureOutgoingIndicators(forThread: thread) else { + owsFailDebug("Could not locate outgoing indicators state") + return + } + outgoingIndicators.didSendOutgoingMessage() + } + + @objc + public func didReceiveTypingStartedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) { + AssertIsOnMainThread() + Logger.info("") + let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId) + incomingIndicators.didReceiveTypingStartedMessage() + } + + @objc + public func didReceiveTypingStoppedMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) { + AssertIsOnMainThread() + Logger.info("") + let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId) + incomingIndicators.didReceiveTypingStoppedMessage() + } + + @objc + public func didReceiveIncomingMessage(inThread thread: TSThread, recipientId: String, deviceId: UInt) { + AssertIsOnMainThread() + Logger.info("") + let incomingIndicators = ensureIncomingIndicators(forThread: thread, recipientId: recipientId, deviceId: deviceId) + incomingIndicators.didReceiveIncomingMessage() + } + + @objc + public func typingRecipientId(forThread thread: TSThread) -> String? { + AssertIsOnMainThread() + + guard areTypingIndicatorsEnabled() else { + return nil + } + + var firstRecipientId: String? + var firstTimestamp: UInt64? + + let threadKey = incomingIndicatorsKey(forThread: thread) + guard let deviceMap = incomingIndicatorsMap[threadKey] else { + // No devices are typing in this thread. + return nil + } + for incomingIndicators in deviceMap.values { + guard incomingIndicators.isTyping else { + continue + } + guard let startedTypingTimestamp = incomingIndicators.startedTypingTimestamp else { + owsFailDebug("Typing device is missing start timestamp.") + continue + } + if let firstTimestamp = firstTimestamp, + firstTimestamp < startedTypingTimestamp { + // More than one recipient/device is typing in this conversation; + // prefer the one that started typing first. + continue + } + firstRecipientId = incomingIndicators.recipientId + firstTimestamp = startedTypingTimestamp + } + return firstRecipientId + } + + // MARK: - + + // Map of thread id-to-OutgoingIndicators. + private var outgoingIndicatorsMap = [String: OutgoingIndicators]() + + private func ensureOutgoingIndicators(forThread thread: TSThread) -> OutgoingIndicators? { + AssertIsOnMainThread() + + guard let threadId = thread.uniqueId else { + owsFailDebug("Thread missing id") + return nil + } + if let outgoingIndicators = outgoingIndicatorsMap[threadId] { + return outgoingIndicators + } + let outgoingIndicators = OutgoingIndicators(delegate: self, thread: thread) + outgoingIndicatorsMap[threadId] = outgoingIndicators + return outgoingIndicators + } + + // The sender maintains two timers per chat: + // + // A sendPause timer + // A sendRefresh timer + private class OutgoingIndicators { + private weak var delegate: TypingIndicators? + private let thread: TSThread + private var sendPauseTimer: Timer? + private var sendRefreshTimer: Timer? + + init(delegate: TypingIndicators, thread: TSThread) { + self.delegate = delegate + self.thread = thread + } + + // MARK: - Dependencies + + private var messageSender: MessageSender { + return SSKEnvironment.shared.messageSender + } + + // MARK: - + + func didStartTypingOutgoingInput() { + AssertIsOnMainThread() + + if sendRefreshTimer == nil { + // If the user types a character into the compose box, and the sendRefresh timer isn’t running: + + sendTypingMessageIfNecessary(forThread: thread, action: .started) + + sendRefreshTimer?.invalidate() + sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10, + target: self, + selector: #selector(OutgoingIndicators.sendRefreshTimerDidFire), + userInfo: nil, + repeats: false) + } else { + // If the user types a character into the compose box, and the sendRefresh timer is running: + } + + sendPauseTimer?.invalidate() + sendPauseTimer = Timer.weakScheduledTimer(withTimeInterval: 3, + target: self, + selector: #selector(OutgoingIndicators.sendPauseTimerDidFire), + userInfo: nil, + repeats: false) + } + + func didStopTypingOutgoingInput() { + AssertIsOnMainThread() + + sendTypingMessageIfNecessary(forThread: thread, action: .stopped) + + sendRefreshTimer?.invalidate() + sendRefreshTimer = nil + + sendPauseTimer?.invalidate() + sendPauseTimer = nil + } + + @objc + func sendPauseTimerDidFire() { + AssertIsOnMainThread() + + sendTypingMessageIfNecessary(forThread: thread, action: .stopped) + + sendRefreshTimer?.invalidate() + sendRefreshTimer = nil + + sendPauseTimer?.invalidate() + sendPauseTimer = nil + } + + @objc + func sendRefreshTimerDidFire() { + AssertIsOnMainThread() + + sendTypingMessageIfNecessary(forThread: thread, action: .started) + + sendRefreshTimer?.invalidate() + sendRefreshTimer = Timer.weakScheduledTimer(withTimeInterval: 10, + target: self, + selector: #selector(sendRefreshTimerDidFire), + userInfo: nil, + repeats: false) + } + + func didSendOutgoingMessage() { + AssertIsOnMainThread() + + sendRefreshTimer?.invalidate() + sendRefreshTimer = nil + + sendPauseTimer?.invalidate() + sendPauseTimer = nil + } + + private func sendTypingMessageIfNecessary(forThread thread: TSThread, action: TypingIndicatorAction) { + Logger.verbose("\(TypingIndicatorMessage.string(forTypingIndicatorAction: action))") + + guard let delegate = delegate else { + owsFailDebug("Missing delegate.") + return + } + // `areTypingIndicatorsEnabled` reflects the user-facing setting in the app preferences. + // If it's disabled we don't want to emit "typing indicator" messages + // or show typing indicators for other users. + guard delegate.areTypingIndicatorsEnabled() else { + return + } + + if !SessionMetaProtocol.shouldSendTypingIndicator(in: thread) { return } + + let message = TypingIndicatorMessage(thread: thread, action: action) + messageSender.sendPromise(message: message).retainUntilComplete() + } + } + + // MARK: - + + // Map of (thread id)-to-(recipient id and device id)-to-IncomingIndicators. + private var incomingIndicatorsMap = [String: [String: IncomingIndicators]]() + + private func incomingIndicatorsKey(forThread thread: TSThread) -> String { + return String(describing: thread.uniqueId) + } + + private func incomingIndicatorsKey(recipientId: String, deviceId: UInt) -> String { + return "\(recipientId) \(deviceId)" + } + + private func ensureIncomingIndicators(forThread thread: TSThread, recipientId: String, deviceId: UInt) -> IncomingIndicators { + AssertIsOnMainThread() + + let threadKey = incomingIndicatorsKey(forThread: thread) + let deviceKey = incomingIndicatorsKey(recipientId: recipientId, deviceId: deviceId) + guard let deviceMap = incomingIndicatorsMap[threadKey] else { + let incomingIndicators = IncomingIndicators(delegate: self, thread: thread, recipientId: recipientId, deviceId: deviceId) + incomingIndicatorsMap[threadKey] = [deviceKey: incomingIndicators] + return incomingIndicators + } + guard let incomingIndicators = deviceMap[deviceKey] else { + let incomingIndicators = IncomingIndicators(delegate: self, thread: thread, recipientId: recipientId, deviceId: deviceId) + var deviceMapCopy = deviceMap + deviceMapCopy[deviceKey] = incomingIndicators + incomingIndicatorsMap[threadKey] = deviceMapCopy + return incomingIndicators + } + return incomingIndicators + } + + // The receiver maintains one timer for each (sender, device) in a chat: + private class IncomingIndicators { + private weak var delegate: TypingIndicators? + private let thread: TSThread + fileprivate let recipientId: String + private let deviceId: UInt + private var displayTypingTimer: Timer? + fileprivate var startedTypingTimestamp: UInt64? + + var isTyping = false { + didSet { + AssertIsOnMainThread() + + let didChange = oldValue != isTyping + if didChange { + Logger.debug("isTyping changed: \(oldValue) -> \(self.isTyping)") + + notifyIfNecessary() + } + } + } + + init(delegate: TypingIndicators, thread: TSThread, + recipientId: String, deviceId: UInt) { + self.delegate = delegate + self.thread = thread + self.recipientId = recipientId + self.deviceId = deviceId + } + + func didReceiveTypingStartedMessage() { + AssertIsOnMainThread() + + displayTypingTimer?.invalidate() + displayTypingTimer = Timer.weakScheduledTimer(withTimeInterval: 5, + target: self, + selector: #selector(IncomingIndicators.displayTypingTimerDidFire), + userInfo: nil, + repeats: false) + if !isTyping { + startedTypingTimestamp = NSDate.ows_millisecondTimeStamp() + } + isTyping = true + } + + func didReceiveTypingStoppedMessage() { + AssertIsOnMainThread() + + clearTyping() + } + + @objc + func displayTypingTimerDidFire() { + AssertIsOnMainThread() + + clearTyping() + } + + func didReceiveIncomingMessage() { + AssertIsOnMainThread() + + clearTyping() + } + + private func clearTyping() { + AssertIsOnMainThread() + + displayTypingTimer?.invalidate() + displayTypingTimer = nil + startedTypingTimestamp = nil + isTyping = false + } + + private func notifyIfNecessary() { + Logger.verbose("") + + guard let delegate = delegate else { + owsFailDebug("Missing delegate.") + return + } + // `areTypingIndicatorsEnabled` reflects the user-facing setting in the app preferences. + // If it's disabled we don't want to emit "typing indicator" messages + // or show typing indicators for other users. + guard delegate.areTypingIndicatorsEnabled() else { + return + } + guard let threadId = thread.uniqueId else { + owsFailDebug("Thread is missing id.") + return + } + NotificationCenter.default.postNotificationNameAsync(TypingIndicatorsImpl.typingIndicatorStateDidChange, object: threadId) + } + } +} diff --git a/SignalUtilitiesKit/UIImage+OWS.h b/SignalUtilitiesKit/UIImage+OWS.h new file mode 100644 index 000000000..c193c9fd2 --- /dev/null +++ b/SignalUtilitiesKit/UIImage+OWS.h @@ -0,0 +1,23 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIImage (normalizeImage) + +- (UIImage *)normalizedImage; +- (UIImage *)resizedWithQuality:(CGInterpolationQuality)quality rate:(CGFloat)rate; + +- (nullable UIImage *)resizedWithMaxDimensionPoints:(CGFloat)maxDimensionPoints; +- (nullable UIImage *)resizedImageToSize:(CGSize)dstSize; +- (UIImage *)resizedImageToFillPixelSize:(CGSize)boundingSize; + ++ (UIImage *)imageWithColor:(UIColor *)color; ++ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/UIImage+OWS.m b/SignalUtilitiesKit/UIImage+OWS.m new file mode 100644 index 000000000..2c852100a --- /dev/null +++ b/SignalUtilitiesKit/UIImage+OWS.m @@ -0,0 +1,243 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "UIImage+OWS.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation UIImage (normalizeImage) + +- (UIImage *)normalizedImage +{ + if (self.imageOrientation == UIImageOrientationUp) { + return self; + } + + UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); + [self drawInRect:(CGRect){ { 0, 0 }, self.size }]; + UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return normalizedImage; +} + +- (UIImage *)resizedWithQuality:(CGInterpolationQuality)quality rate:(CGFloat)rate +{ + UIImage *resized = nil; + CGFloat width = self.size.width * rate; + CGFloat height = self.size.height * rate; + + UIGraphicsBeginImageContext(CGSizeMake(width, height)); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetInterpolationQuality(context, quality); + [self drawInRect:CGRectMake(0, 0, width, height)]; + resized = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return resized; +} + +- (nullable UIImage *)resizedWithMaxDimensionPoints:(CGFloat)maxDimensionPoints +{ + CGSize originalSize = self.size; + if (originalSize.width < 1 || originalSize.height < 1) { + OWSLogError(@"Invalid original size: %@", NSStringFromCGSize(originalSize)); + return nil; + } + + CGFloat maxOriginalDimensionPoints = MAX(originalSize.width, originalSize.height); + if (maxOriginalDimensionPoints < maxDimensionPoints) { + // Don't bother scaling an image that is already smaller than the max dimension. + return self; + } + + CGSize thumbnailSize = CGSizeZero; + if (originalSize.width > originalSize.height) { + thumbnailSize.width = maxDimensionPoints; + thumbnailSize.height = round(maxDimensionPoints * originalSize.height / originalSize.width); + } else { + thumbnailSize.width = round(maxDimensionPoints * originalSize.width / originalSize.height); + thumbnailSize.height = maxDimensionPoints; + } + if (thumbnailSize.width < 1 || thumbnailSize.height < 1) { + OWSLogError(@"Invalid thumbnail size: %@", NSStringFromCGSize(thumbnailSize)); + return nil; + } + + UIGraphicsBeginImageContext(CGSizeMake(thumbnailSize.width, thumbnailSize.height)); + CGContextRef _Nullable context = UIGraphicsGetCurrentContext(); + if (context == NULL) { + OWSLogError(@"Couldn't create context."); + return nil; + } + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); + [self drawInRect:CGRectMake(0, 0, thumbnailSize.width, thumbnailSize.height)]; + UIImage *_Nullable resized = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return resized; +} + +// Source: https://github.com/AliSoftware/UIImage-Resize + +- (nullable UIImage *)resizedImageToSize:(CGSize)dstSize +{ + CGImageRef imgRef = self.CGImage; + // the below values are regardless of orientation : for UIImages from Camera, width>height (landscape) + CGSize srcSize = CGSizeMake(CGImageGetWidth(imgRef), + CGImageGetHeight(imgRef)); // not equivalent to self.size (which is dependant on the imageOrientation)! + + /* Don't resize if we already meet the required destination size. */ + if (CGSizeEqualToSize(srcSize, dstSize)) { + return self; + } + + CGFloat scaleRatio = dstSize.width / srcSize.width; + UIImageOrientation orient = self.imageOrientation; + CGAffineTransform transform = CGAffineTransformIdentity; + switch (orient) { + case UIImageOrientationUp: // EXIF = 1 + transform = CGAffineTransformIdentity; + break; + + case UIImageOrientationUpMirrored: // EXIF = 2 + transform = CGAffineTransformMakeTranslation(srcSize.width, 0.0); + transform = CGAffineTransformScale(transform, -1.0, 1.0); + break; + + case UIImageOrientationDown: // EXIF = 3 + transform = CGAffineTransformMakeTranslation(srcSize.width, srcSize.height); + transform = CGAffineTransformRotate(transform, (CGFloat)M_PI); + break; + + case UIImageOrientationDownMirrored: // EXIF = 4 + transform = CGAffineTransformMakeTranslation(0.0, srcSize.height); + transform = CGAffineTransformScale(transform, 1.0, -1.0); + break; + + case UIImageOrientationLeftMirrored: // EXIF = 5 + dstSize = CGSizeMake(dstSize.height, dstSize.width); + transform = CGAffineTransformMakeTranslation(srcSize.height, srcSize.width); + transform = CGAffineTransformScale(transform, -1.0, 1.0); + transform = CGAffineTransformRotate(transform, (CGFloat)(3.0f * M_PI_2)); + break; + + case UIImageOrientationLeft: // EXIF = 6 + dstSize = CGSizeMake(dstSize.height, dstSize.width); + transform = CGAffineTransformMakeTranslation(0.0, srcSize.width); + transform = CGAffineTransformRotate(transform, (CGFloat)(3.0 * M_PI_2)); + break; + + case UIImageOrientationRightMirrored: // EXIF = 7 + dstSize = CGSizeMake(dstSize.height, dstSize.width); + transform = CGAffineTransformMakeScale(-1.0, 1.0); + transform = CGAffineTransformRotate(transform, (CGFloat)M_PI_2); + break; + + case UIImageOrientationRight: // EXIF = 8 + dstSize = CGSizeMake(dstSize.height, dstSize.width); + transform = CGAffineTransformMakeTranslation(srcSize.height, 0.0); + transform = CGAffineTransformRotate(transform, (CGFloat)M_PI_2); + break; + + default: + OWSFailDebug(@"Invalid image orientation"); + return nil; + } + + ///////////////////////////////////////////////////////////////////////////// + // The actual resize: draw the image on a new context, applying a transform matrix + UIGraphicsBeginImageContextWithOptions(dstSize, NO, self.scale); + + CGContextRef context = UIGraphicsGetCurrentContext(); + if (!context) { + return nil; + } + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); + + if (orient == UIImageOrientationRight || orient == UIImageOrientationLeft) { + CGContextScaleCTM(context, -scaleRatio, scaleRatio); + CGContextTranslateCTM(context, -srcSize.height, 0); + } else { + CGContextScaleCTM(context, scaleRatio, -scaleRatio); + CGContextTranslateCTM(context, 0, -srcSize.height); + } + + CGContextConcatCTM(context, transform); + + // we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a + // scaleRatio) + CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, srcSize.width, srcSize.height), imgRef); + UIImage *_Nullable resizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return resizedImage; +} + +- (UIImage *)resizedImageToFillPixelSize:(CGSize)dstSize +{ + OWSAssertDebug(dstSize.width > 0); + OWSAssertDebug(dstSize.height > 0); + + UIImage *normalized = [self normalizedImage]; + + // Get the size in pixels, not points. + CGSize srcSize = CGSizeMake(CGImageGetWidth(normalized.CGImage), CGImageGetHeight(normalized.CGImage)); + OWSAssertDebug(srcSize.width > 0); + OWSAssertDebug(srcSize.height > 0); + + CGFloat widthRatio = srcSize.width / dstSize.width; + CGFloat heightRatio = srcSize.height / dstSize.height; + CGRect drawRect = CGRectZero; + if (widthRatio > heightRatio) { + drawRect.origin.y = 0; + drawRect.size.height = dstSize.height; + drawRect.size.width = dstSize.height * srcSize.width / srcSize.height; + OWSAssertDebug(drawRect.size.width > dstSize.width); + drawRect.origin.x = (drawRect.size.width - dstSize.width) * -0.5f; + } else { + drawRect.origin.x = 0; + drawRect.size.width = dstSize.width; + drawRect.size.height = dstSize.width * srcSize.height / srcSize.width; + OWSAssertDebug(drawRect.size.height >= dstSize.height); + drawRect.origin.y = (drawRect.size.height - dstSize.height) * -0.5f; + } + + UIGraphicsBeginImageContextWithOptions(dstSize, NO, 1.f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); + [self drawInRect:drawRect]; + UIImage *dstImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return dstImage; +} + ++ (UIImage *)imageWithColor:(UIColor *)color +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(color); + + return [self imageWithColor:color size:CGSizeMake(1.f, 1.f)]; +} + ++ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size +{ + OWSAssertIsOnMainThread(); + OWSAssertDebug(color); + + CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height); + UIGraphicsBeginImageContextWithOptions(rect.size, NO, 1.f); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextClearRect(context, rect); + CGContextSetFillColorWithColor(context, [color CGColor]); + CGContextFillRect(context, rect); + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/WeakTimer.swift b/SignalUtilitiesKit/WeakTimer.swift new file mode 100644 index 000000000..f3553b6f4 --- /dev/null +++ b/SignalUtilitiesKit/WeakTimer.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +/** + * As of iOS10, the timer API's take a block, which makes it easy to reference weak self in Swift. This class offers a + * similar API that works pre iOS10. + * + * Solution modified from + * http://stackoverflow.com/questions/16821736/weak-reference-to-nstimer-target-to-prevent-retain-cycle/41003985#41003985 + */ +public final class WeakTimer { + + fileprivate weak var timer: Timer? + fileprivate weak var target: AnyObject? + fileprivate let action: (Timer) -> Void + + fileprivate init(timeInterval: TimeInterval, target: AnyObject, userInfo: Any?, repeats: Bool, action: @escaping (Timer) -> Void) { + self.target = target + self.action = action + self.timer = Timer.scheduledTimer(timeInterval: timeInterval, + target: self, + selector: #selector(fire), + userInfo: userInfo, + repeats: repeats) + } + + public class func scheduledTimer(timeInterval: TimeInterval, target: AnyObject, userInfo: Any?, repeats: Bool, action: @escaping (Timer) -> Void) -> Timer { + return WeakTimer(timeInterval: timeInterval, + target: target, + userInfo: userInfo, + repeats: repeats, + action: action).timer! + } + + @objc public func fire(timer: Timer) { + if target != nil { + action(timer) + } else { + timer.invalidate() + } + } +} diff --git a/SignalUtilitiesKit/YapDatabase+Promise.swift b/SignalUtilitiesKit/YapDatabase+Promise.swift new file mode 100644 index 000000000..49ad9fcc0 --- /dev/null +++ b/SignalUtilitiesKit/YapDatabase+Promise.swift @@ -0,0 +1,52 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +public extension YapDatabaseConnection { + + @objc + func readWritePromise(_ block: @escaping (YapDatabaseReadWriteTransaction) -> Void) -> AnyPromise { + return AnyPromise(readWritePromise(block) as Promise) + } + + func readWritePromise(_ block: @escaping (YapDatabaseReadWriteTransaction) -> Void) -> Promise { + return Promise { resolver in + self.asyncReadWrite(block, completionBlock: { resolver.fulfill(()) }) + } + } + + func read(_ block: @escaping (YapDatabaseReadTransaction) throws -> Void) throws { + var errorToRaise: Error? + + read { transaction in + do { + try block(transaction) + } catch { + errorToRaise = error + } + } + + if let errorToRaise = errorToRaise { + throw errorToRaise + } + } + + func readWrite(_ block: @escaping (YapDatabaseReadWriteTransaction) throws -> Void) throws { + var errorToRaise: Error? + + readWrite { transaction in + do { + try block(transaction) + } catch { + errorToRaise = error + } + } + + if let errorToRaise = errorToRaise { + throw errorToRaise + } + } +} diff --git a/SignalUtilitiesKit/YapDatabaseConnection+OWS.h b/SignalUtilitiesKit/YapDatabaseConnection+OWS.h new file mode 100644 index 000000000..96b27b277 --- /dev/null +++ b/SignalUtilitiesKit/YapDatabaseConnection+OWS.h @@ -0,0 +1,46 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +@class ECKeyPair; +@class PreKeyRecord; +@class SignedPreKeyRecord; +@class PreKeyBundle; + +NS_ASSUME_NONNULL_BEGIN + +@interface YapDatabaseConnection (OWS) + +- (BOOL)hasObjectForKey:(NSString *)key inCollection:(NSString *)collection; +- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue; +- (double)doubleForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(double)defaultValue; +- (int)intForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSDate *)dateForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSDictionary *)dictionaryForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSString *)stringForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable ECKeyPair *)keyPairForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable PreKeyRecord *)preKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable PreKeyBundle *)preKeyBundleForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable SignedPreKeyRecord *)signedPreKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection; + +- (NSUInteger)numberOfKeysInCollection:(NSString *)collection; + +#pragma mark - + +- (void)setObject:(id)object forKey:(NSString *)key inCollection:(NSString *)collection; +- (void)setBool:(BOOL)value forKey:(NSString *)key inCollection:(NSString *)collection; +- (void)setDouble:(double)value forKey:(NSString *)key inCollection:(NSString *)collection; +- (void)removeObjectForKey:(NSString *)string inCollection:(NSString *)collection; +- (void)setInt:(int)integer forKey:(NSString *)key inCollection:(NSString *)collection; +- (void)setDate:(NSDate *)value forKey:(NSString *)key inCollection:(NSString *)collection; +- (int)incrementIntForKey:(NSString *)key inCollection:(NSString *)collection; + +- (void)purgeCollection:(NSString *)collection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/YapDatabaseConnection+OWS.m b/SignalUtilitiesKit/YapDatabaseConnection+OWS.m new file mode 100644 index 000000000..27358c51c --- /dev/null +++ b/SignalUtilitiesKit/YapDatabaseConnection+OWS.m @@ -0,0 +1,192 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "YapDatabaseConnection+OWS.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation YapDatabaseConnection (OWS) + +- (BOOL)hasObjectForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + return nil != [self objectForKey:key inCollection:collection]; +} + +- (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + __block NSString *_Nullable object; + + [self readWithBlock:^(YapDatabaseReadTransaction *transaction) { + object = [transaction objectForKey:key inCollection:collection]; + }]; + + return object; +} + +- (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection ofExpectedType:(Class)class +{ + id _Nullable value = [self objectForKey:key inCollection:collection]; + OWSAssertDebug(!value || [value isKindOfClass:class]); + return value; +} + +- (nullable NSDictionary *)dictionaryForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:[NSDictionary class]]; +} + +- (nullable NSString *)stringForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:[NSString class]]; +} + +- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue +{ + NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + return value ? [value boolValue] : defaultValue; +} + +- (double)doubleForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(double)defaultValue +{ + NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + return value ? [value doubleValue] : defaultValue; +} + +- (nullable NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:[NSData class]]; +} + +- (nullable ECKeyPair *)keyPairForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:[ECKeyPair class]]; +} + +- (nullable PreKeyRecord *)preKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:[PreKeyRecord class]]; +} + +- (nullable PreKeyBundle *)preKeyBundleForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:PreKeyBundle.class]; +} + +- (nullable SignedPreKeyRecord *)signedPreKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:[SignedPreKeyRecord class]]; +} + +- (int)intForKey:(NSString *)key inCollection:(NSString *)collection +{ + NSNumber *_Nullable number = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + return [number intValue]; +} + +- (nullable NSDate *)dateForKey:(NSString *)key inCollection:(NSString *)collection +{ + NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + if (value) { + return [NSDate dateWithTimeIntervalSince1970:value.doubleValue]; + } else { + return nil; + } +} + +#pragma mark - + +- (NSUInteger)numberOfKeysInCollection:(NSString *)collection +{ + __block NSUInteger result; + [self readWithBlock:^(YapDatabaseReadTransaction *transaction) { + result = [transaction numberOfKeysInCollection:collection]; + }]; + return result; +} + +- (void)purgeCollection:(NSString *)collection +{ + [self readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction removeAllObjectsInCollection:collection]; + }]; +} + +- (void)setObject:(id)object forKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + [self readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction setObject:object forKey:key inCollection:collection]; + }]; +} + +- (void)setBool:(BOOL)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + NSNumber *_Nullable oldValue = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + if (oldValue && [@(value) isEqual:oldValue]) { + // Skip redundant writes. + return; + } + + [self setObject:@(value) forKey:key inCollection:collection]; +} + +- (void)setDouble:(double)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + [self setObject:@(value) forKey:key inCollection:collection]; +} + +- (void)removeObjectForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + [self readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction removeObjectForKey:key inCollection:collection]; + }]; +} + +- (void)setInt:(int)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + [self setObject:@(value) forKey:key inCollection:collection]; +} + +- (int)incrementIntForKey:(NSString *)key inCollection:(NSString *)collection +{ + __block int value = 0; + [self readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + value = [[transaction objectForKey:key inCollection:collection] intValue]; + value++; + [transaction setObject:@(value) forKey:key inCollection:collection]; + }]; + return value; +} + +- (void)setDate:(NSDate *)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + [self setObject:@(value.timeIntervalSince1970) forKey:key inCollection:collection]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/YapDatabaseTransaction+OWS.h b/SignalUtilitiesKit/YapDatabaseTransaction+OWS.h new file mode 100644 index 000000000..fb9f6524c --- /dev/null +++ b/SignalUtilitiesKit/YapDatabaseTransaction+OWS.h @@ -0,0 +1,45 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import + +@class ECKeyPair; +@class PreKeyRecord; +@class PreKeyBundle; +@class SignedPreKeyRecord; + +NS_ASSUME_NONNULL_BEGIN + +@interface YapDatabaseReadTransaction (OWS) + +- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue; +- (int)intForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSDate *)dateForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSDictionary *)dictionaryForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSString *)stringForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable ECKeyPair *)keyPairForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable PreKeyRecord *)preKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable PreKeyBundle *)preKeyBundleForKey:(NSString *)key inCollection:(NSString *)collection; +- (nullable SignedPreKeyRecord *)signedPreKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection; + +@end + +#pragma mark - + +@interface YapDatabaseReadWriteTransaction (OWS) + +#pragma mark - Debug + +#if DEBUG +- (void)snapshotCollection:(NSString *)collection snapshotFilePath:(NSString *)snapshotFilePath; +- (void)restoreSnapshotOfCollection:(NSString *)collection snapshotFilePath:(NSString *)snapshotFilePath; +#endif + +- (void)setBool:(BOOL)value forKey:(NSString *)key inCollection:(NSString *)collection; +- (void)setDate:(NSDate *)value forKey:(NSString *)key inCollection:(NSString *)collection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/YapDatabaseTransaction+OWS.m b/SignalUtilitiesKit/YapDatabaseTransaction+OWS.m new file mode 100644 index 000000000..9596f2841 --- /dev/null +++ b/SignalUtilitiesKit/YapDatabaseTransaction+OWS.m @@ -0,0 +1,169 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +#import "YapDatabaseTransaction+OWS.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation YapDatabaseReadTransaction (OWS) + +- (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection ofExpectedType:(Class) class { + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + id _Nullable value = [self objectForKey:key inCollection:collection]; + OWSAssertDebug(!value || [value isKindOfClass:class]); + return value; +} + + - (nullable NSDictionary *)dictionaryForKey : (NSString *)key inCollection : (NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + return [self objectForKey:key inCollection:collection ofExpectedType:[NSDictionary class]]; +} + +- (nullable NSString *)stringForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + return [self objectForKey:key inCollection:collection ofExpectedType:[NSString class]]; +} + +- (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + return value ? [value boolValue] : defaultValue; +} + +- (nullable NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + return [self objectForKey:key inCollection:collection ofExpectedType:[NSData class]]; +} + +- (nullable ECKeyPair *)keyPairForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + return [self objectForKey:key inCollection:collection ofExpectedType:[ECKeyPair class]]; +} + +- (nullable PreKeyRecord *)preKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + return [self objectForKey:key inCollection:collection ofExpectedType:[PreKeyRecord class]]; +} + +- (nullable PreKeyBundle *)preKeyBundleForKey:(NSString *)key inCollection:(NSString *)collection +{ + return [self objectForKey:key inCollection:collection ofExpectedType:PreKeyBundle.class]; +} + +- (nullable SignedPreKeyRecord *)signedPreKeyRecordForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + return [self objectForKey:key inCollection:collection ofExpectedType:[SignedPreKeyRecord class]]; +} + +- (int)intForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + NSNumber *_Nullable number = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + return [number intValue]; +} + +- (nullable NSDate *)dateForKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + if (value) { + return [NSDate dateWithTimeIntervalSince1970:value.doubleValue]; + } else { + return nil; + } +} + +@end + +#pragma mark - + +@implementation YapDatabaseReadWriteTransaction (OWS) + +#pragma mark - Debug + +#if DEBUG +- (void)snapshotCollection:(NSString *)collection snapshotFilePath:(NSString *)snapshotFilePath +{ + OWSAssertDebug(collection.length > 0); + OWSAssertDebug(snapshotFilePath.length > 0); + + NSMutableDictionary *snapshot = [NSMutableDictionary new]; + [self enumerateKeysAndObjectsInCollection:collection + usingBlock:^(NSString *_Nonnull key, id _Nonnull value, BOOL *_Nonnull stop) { + snapshot[key] = value; + }]; + NSData *_Nullable data = [NSKeyedArchiver archivedDataWithRootObject:snapshot]; + OWSAssertDebug(data); + BOOL success = [data writeToFile:snapshotFilePath atomically:YES]; + OWSAssertDebug(success); +} + +- (void)restoreSnapshotOfCollection:(NSString *)collection snapshotFilePath:(NSString *)snapshotFilePath +{ + OWSAssertDebug(collection.length > 0); + OWSAssertDebug(snapshotFilePath.length > 0); + + NSData *_Nullable data = [NSData dataWithContentsOfFile:snapshotFilePath]; + OWSAssertDebug(data); + NSMutableDictionary *_Nullable snapshot = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + OWSAssertDebug(snapshot); + + [self removeAllObjectsInCollection:collection]; + [snapshot enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, id _Nonnull value, BOOL *_Nonnull stop) { + [self setObject:value forKey:key inCollection:collection]; + }]; +} +#endif + +- (void)setBool:(BOOL)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssertDebug(key.length > 0); + OWSAssertDebug(collection.length > 0); + + NSNumber *_Nullable oldValue = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + if (oldValue && [@(value) isEqual:oldValue]) { + // Skip redundant writes. + return; + } + + [self setObject:@(value) forKey:key inCollection:collection]; +} + +- (void)setDate:(NSDate *)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + [self setObject:@(value.timeIntervalSince1970) forKey:key inCollection:collection]; +} + +@end + +NS_ASSUME_NONNULL_END