11import Foundation
2+ import Combine
23
34public final class ThreatDetectionCenter {
4-
5+
6+ private init ( ) { }
7+
8+ /// **Seconds** to wait for cycle recheck threads before running check again
9+ /// > Should be positive and greater than 0, otherwise value is left unchanged
10+ static public var threatReportsUpdateInterval : Int {
11+ get { return _threatReportsUpdateInterval }
12+ set {
13+ if ( newValue > 0 ) {
14+ _threatReportsUpdateInterval = newValue
15+ }
16+ }
17+ }
18+
19+
20+ // MARK: - Async Threat Detection
21+
22+ // Private publisher for sending temperature updates
23+ static private let reportPublisher = CurrentValueSubject < ThreatReport , Never > ( ThreatReport ( ) )
24+
25+ static private var task : Task < ( ) , Never > ?
26+ static private var internalTasks : [ Task < ( ) , Never > ] = [ ]
27+ static private var _threatReportsUpdateInterval = 10
28+
29+ /// Use this API to get ThreatReports
30+ ///
31+ /// > First access will start all Tasks and cache the result.
32+ /// Next calls will not start any additional Tasks. Original result
33+ /// will be cached and can be reused
34+ static public var threatReports : AnyPublisher < ThreatReport , Never > {
35+ task = task ?? Task {
36+ internalTasks. append (
37+ Task {
38+ repeat {
39+ let status = JailbreakDetector . threatDetected ( )
40+ reportPublisher. update { $0. copy ( rootPrivileges: status) }
41+ await insertDelay ( )
42+ } while !Task. isCancelled
43+ }
44+ )
45+ internalTasks. append (
46+ Task {
47+ repeat {
48+ let status = HooksDetector . threatDetected ( )
49+ reportPublisher. update { $0. copy ( hooks: status) }
50+ await insertDelay ( )
51+ } while !Task. isCancelled
52+ }
53+ )
54+ internalTasks. append (
55+ Task {
56+ let status = SimulatorDetector . threatDetected ( )
57+ reportPublisher. update { $0. copy ( simulator: status) }
58+ }
59+ )
60+ internalTasks. append (
61+ Task {
62+ repeat {
63+ let status = DebuggerDetector . threatDetected ( )
64+ reportPublisher. update { $0. copy ( debugger: status) }
65+ await insertDelay ( )
66+ } while !Task. isCancelled
67+ }
68+ )
69+ internalTasks. append (
70+ Task {
71+ repeat {
72+ let status = DevicePasscodeDetector . threatDetected ( )
73+ reportPublisher. update { $0. copy ( devicePasscode: status) }
74+ await insertDelay ( )
75+ } while !Task. isCancelled
76+ }
77+ )
78+ internalTasks. append (
79+ Task {
80+ let status = HardwareSecurityDetector . threatDetected ( )
81+ reportPublisher. update { $0. copy ( hardwareCryptography: status) }
82+ }
83+ )
84+ }
85+ return reportPublisher. eraseToAnyPublisher ( )
86+ }
87+
88+ public static func close( ) {
89+ internalTasks. forEach { $0. cancel ( ) }
90+ task? . cancel ( )
91+ internalTasks. removeAll ( )
92+ task = nil
93+ }
94+
95+ // MARK: - Sync API
96+
597 /// Will check if jailbreak is present
698 ///
7- /// - Returns:
8- /// `true`, if device is / was jailbroken;
9- /// `false` otherwise
10- ///
1199 /// More about jailbreak: https://wikipedia.org/wiki/Jailbreak_%28iOS%29
12100 ///
13101 /// > Should also detect jailbreak, even if the device is in a "safe" mode or
14102 /// jailbreak mode is not active / was not properly removed
15- public static var areRootPrivilegesDetected : Bool {
16- JailbreakDetection . threatDetected ( )
103+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
104+ public static var rootPrivilegesStatus : ThreatStatus {
105+ JailbreakDetector . threatDetected ( )
17106 }
18-
107+
19108 /// Will check for an injection tool like Frida
20109 ///
21- /// - Returns:
22- /// `true`, if dynamic hooks are loaded at the time;
23- /// `false` otherwise
24- ///
25110 /// More: https://fingerprint.com/blog/exploring-frida-dynamic-instrumentation-tool-kit/
26111 ///
27112 /// > By the nature of dynamic hooks, this checks should be made on a regular
@@ -30,45 +115,34 @@ public final class ThreatDetectionCenter {
30115 ///
31116 /// > Important: with a sufficient reverse engineering skills, this check can
32117 /// be disabled. Use always in combination with another threats detections.
33- public static var areHooksDetected : Bool {
34- HooksDetection . threatDetected ( )
118+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
119+ public static var hooksStatus : ThreatStatus {
120+ HooksDetector . threatDetected ( )
35121 }
36-
122+
37123 /// Will check, if the app runs in a emulated / simulated environment
38- ///
39- /// - Returns:
40- /// `true`, if simulator environment is detected;
41- /// `false` otherwise
42- public static var isSimulatorDetected : Bool {
43- SimulatorDetection . threatDetected ( )
124+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
125+ public static var simulatorStatus : ThreatStatus {
126+ SimulatorDetector . threatDetected ( )
44127 }
45128
46129 /// Will check, if the application is being traced by a debugger.
47130 ///
48- /// - Returns:
49- /// `true`, if a debugger is detected;
50- /// `false`, if no debugger is detected;
51- /// `nil`, if the detection process did not produce a definitive result.
52- /// This could happen due to system limitations, lack of required
53- /// permissions, or other undefined conditions.
54- ///
55131 /// A debugger is a tool that allows developers to inspect and modify the
56132 /// execution of a program in real-time, potentially exposing sensitive data
57133 /// or allowing unauthorized control.
58134 ///
59135 /// > Please note that Apple itself may require a debugger for the app review
60136 /// process.
61- public static var isDebuggerDetected : Bool ? {
62- DebuggerDetection . threatDetected ( )
137+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
138+ public static var debuggerStatus : ThreatStatus {
139+ DebuggerDetector . threatDetected ( )
63140 }
64-
141+
65142 /// Will check, if current device is protected with at least a passcode
66- ///
67- /// - Returns:
68- /// `true`, if device is unprotected;
69- /// `false`, if device is protected with at least a passcode
70- public static var isDeviceWithoutPasscodeDetected : Bool {
71- DevicePasscodeDetection . threatDetected ( )
143+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
144+ public static var devicePasscodeStatus : ThreatStatus {
145+ DevicePasscodeDetector . threatDetected ( )
72146 }
73147
74148 /// Will check, if current device has hardware protection layer
@@ -78,61 +152,37 @@ public final class ThreatDetectionCenter {
78152 ///
79153 /// More: https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave
80154 ///
81- /// - Returns:
82- /// `true`, if device has no hardware protection;
83- /// `false` otherwise
84- ///
85155 /// > Should be evaluated on a real device. Should only be used as an
86156 /// indicator, if current device is capable of hardware protection. Does not
87157 /// automatically mean, that encryption operations (keys, certificates,
88158 /// keychain) are always backed by hardware. You should make sure, such
89159 /// operations are implemented correctly with hardware layer
90- public static var isHardwareProtectionUnavailable : Bool {
91- HardwareSecurityDetection . threatDetected ( )
160+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
161+ public static var hardwareCryptographyStatus : ThreatStatus {
162+ HardwareSecurityDetector . threatDetected ( )
92163 }
93164
94-
95- // MARK: - Async Threat Detection
96-
97- /// Defines all possible threats, that can be reported via the stream
98- public enum Threat : String {
99- case rootPrivileges
100- case hooks
101- case simulator
102- case debugger
103- case deviceWithoutPasscode
104- case hardwareProtectionUnavailable
165+ // MARK: - Private API
166+
167+ static private func insertDelay( ) async {
168+ try ? await Task . sleep ( nanoseconds: UInt64 ( _threatReportsUpdateInterval) * NSEC_PER_SEC)
105169 }
106-
107- /// Stream that contains possible threats that could be detected
108- public static var threats : AsyncStream < Threat > {
109- AsyncStream< Threat> { continuation in
110-
111- if JailbreakDetection . threatDetected ( ) {
112- continuation. yield ( . rootPrivileges)
113- }
114-
115- if HooksDetection . threatDetected ( ) {
116- continuation. yield ( . hooks)
117- }
118-
119- if SimulatorDetection . threatDetected ( ) {
120- continuation. yield ( . simulator)
121- }
122-
123- if DebuggerDetection . threatDetected ( ) ?? false {
124- continuation. yield ( . debugger)
125- }
170+ }
126171
127- if DevicePasscodeDetection . threatDetected ( ) {
128- continuation. yield ( . deviceWithoutPasscode)
172+ fileprivate extension CurrentValueSubject where Output: Equatable {
173+ /// Use this function to update a value in the publisher atomically
174+ func update( _ callback: ( Output ) -> Output ) {
175+ while true {
176+ let value = self . value
177+ let newValue = callback ( value)
178+ if ( Task . isCancelled) {
179+ return
180+ } else if value == newValue {
181+ return
182+ } else if self . value == value {
183+ self . value = newValue
184+ return
129185 }
130-
131- if HardwareSecurityDetection . threatDetected ( ) {
132- continuation. yield ( . hardwareProtectionUnavailable)
133- }
134-
135- continuation. finish ( )
136186 }
137187 }
138188}
0 commit comments