11import Foundation
2+ import Combine
23
34public final class ThreatDetectionCenter {
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 default value is used
10+ static public var threatReportsUpdateInterval = 10
11+
12+ // MARK: - Async Threat Detection
13+
14+ // Private publisher for sending temperature updates
15+ static private let reportPublisher = CurrentValueSubject < ThreatReport , Never > ( ThreatReport ( ) )
16+
17+ static private var task : Task < ( ) , Never > ?
18+
19+ /// Use this API to get ThreatReports
20+ ///
21+ /// > First access will start all Tasks and cache the result.
22+ /// Next calls will not start any additional Tasks. Original result
23+ /// will be cached and can be reused
24+ static public var threatReports : AnyPublisher < ThreatReport , Never > {
25+ task = task ?? Task {
26+ Task {
27+ repeat {
28+ let status = JailbreakDetector . threatDetected ( )
29+ reportPublisher. update { $0. copy ( rootPrivileges: status) }
30+ await insertDelay ( )
31+ } while !Task. isCancelled
32+ }
33+ Task {
34+ repeat {
35+ let status = HooksDetector . threatDetected ( )
36+ reportPublisher. update { $0. copy ( hooks: status) }
37+ await insertDelay ( )
38+ } while !Task. isCancelled
39+ }
40+ Task {
41+ let status = SimulatorDetector . threatDetected ( )
42+ reportPublisher. update { $0. copy ( simulator: status) }
43+ }
44+ Task {
45+ repeat {
46+ let status = DebuggerDetector . threatDetected ( )
47+ reportPublisher. update { $0. copy ( debugger: status) }
48+ await insertDelay ( )
49+ } while !Task. isCancelled
50+ }
51+ Task {
52+ repeat {
53+ let status = DevicePasscodeDetector . threatDetected ( )
54+ reportPublisher. update { $0. copy ( devicePasscode: status) }
55+ await insertDelay ( )
56+ } while !Task. isCancelled
57+ }
58+ Task {
59+ let status = HardwareSecurityDetector . threatDetected ( )
60+ reportPublisher. update { $0. copy ( hardwareCryptography: status) }
61+ }
62+ }
63+ return reportPublisher. eraseToAnyPublisher ( )
64+ }
65+
66+ public static func close( ) {
67+ task? . cancel ( )
68+ }
69+
70+ // MARK: - Sync API
471
572 /// Will check if jailbreak is present
673 ///
7- /// - Returns:
8- /// `true`, if device is / was jailbroken;
9- /// `false` otherwise
10- ///
1174 /// More about jailbreak: https://wikipedia.org/wiki/Jailbreak_%28iOS%29
1275 ///
1376 /// > Should also detect jailbreak, even if the device is in a "safe" mode or
1477 /// jailbreak mode is not active / was not properly removed
15- public static var areRootPrivilegesDetected : Bool {
16- JailbreakDetection . threatDetected ( )
78+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
79+ public static var rootPrivilegesStatus : ThreatStatus {
80+ JailbreakDetector . threatDetected ( )
1781 }
1882
1983 /// Will check for an injection tool like Frida
2084 ///
21- /// - Returns:
22- /// `true`, if dynamic hooks are loaded at the time;
23- /// `false` otherwise
24- ///
2585 /// More: https://fingerprint.com/blog/exploring-frida-dynamic-instrumentation-tool-kit/
2686 ///
2787 /// > By the nature of dynamic hooks, this checks should be made on a regular
@@ -30,45 +90,34 @@ public final class ThreatDetectionCenter {
3090 ///
3191 /// > Important: with a sufficient reverse engineering skills, this check can
3292 /// be disabled. Use always in combination with another threats detections.
33- public static var areHooksDetected : Bool {
34- HooksDetection . threatDetected ( )
93+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
94+ public static var hooksStatus : ThreatStatus {
95+ HooksDetector . threatDetected ( )
3596 }
3697
3798 /// 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 ( )
99+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
100+ public static var simulatorStatus : ThreatStatus {
101+ SimulatorDetector . threatDetected ( )
44102 }
45103
46104 /// Will check, if the application is being traced by a debugger.
47105 ///
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- ///
55106 /// A debugger is a tool that allows developers to inspect and modify the
56107 /// execution of a program in real-time, potentially exposing sensitive data
57108 /// or allowing unauthorized control.
58109 ///
59110 /// > Please note that Apple itself may require a debugger for the app review
60111 /// process.
61- public static var isDebuggerDetected : Bool ? {
62- DebuggerDetection . threatDetected ( )
112+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
113+ public static var debuggerStatus : ThreatStatus {
114+ DebuggerDetector . threatDetected ( )
63115 }
64116
65117 /// 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 ( )
118+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
119+ public static var devicePasscodeStatus : ThreatStatus {
120+ DevicePasscodeDetector . threatDetected ( )
72121 }
73122
74123 /// Will check, if current device has hardware protection layer
@@ -78,61 +127,39 @@ public final class ThreatDetectionCenter {
78127 ///
79128 /// More: https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave
80129 ///
81- /// - Returns:
82- /// `true`, if device has no hardware protection;
83- /// `false` otherwise
84- ///
85130 /// > Should be evaluated on a real device. Should only be used as an
86131 /// indicator, if current device is capable of hardware protection. Does not
87132 /// automatically mean, that encryption operations (keys, certificates,
88133 /// keychain) are always backed by hardware. You should make sure, such
89134 /// operations are implemented correctly with hardware layer
90- public static var isHardwareProtectionUnavailable : Bool {
91- HardwareSecurityDetection . threatDetected ( )
135+ @available ( * , deprecated, message: " Migrate to use new threatReports async API " )
136+ public static var hardwareCryptographyStatus : ThreatStatus {
137+ HardwareSecurityDetector . threatDetected ( )
92138 }
93-
94-
95- // MARK: - Async Threat Detection
96139
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
140+ // MARK: - Private API
141+
142+ static private func insertDelay ( ) async {
143+ if ( threatReportsUpdateInterval > 0 ) {
144+ try ? await Task . sleep ( nanoseconds : UInt64 ( threatReportsUpdateInterval ) * NSEC_PER_SEC )
145+ } else {
146+ try ? await Task . sleep ( nanoseconds : 10 * NSEC_PER_SEC )
147+ }
105148 }
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- }
149+ }
126150
127- if DevicePasscodeDetection . threatDetected ( ) {
128- continuation. yield ( . deviceWithoutPasscode)
151+ fileprivate extension CurrentValueSubject where Output: Equatable {
152+ /// Use this function to update a value in the publisher atomically
153+ func update( _ callback: ( Output ) -> Output ) {
154+ while true {
155+ let value = self . value
156+ let newValue = callback ( value)
157+ if value == newValue {
158+ return
159+ } else if self . value == value {
160+ self . value = newValue
161+ return
129162 }
130-
131- if HardwareSecurityDetection . threatDetected ( ) {
132- continuation. yield ( . hardwareProtectionUnavailable)
133- }
134-
135- continuation. finish ( )
136163 }
137164 }
138165}
0 commit comments