1
1
import Foundation
2
+ import Combine
2
3
3
4
public final class ThreatDetectionCenter {
5
+
6
+ private init ( ) { }
7
+
8
+ static private var delaySeconds = 10
9
+
10
+ // MARK: - Async Threat Detection
11
+
12
+ // Private publisher for sending temperature updates
13
+ static private let reportPublisher = CurrentValueSubject < ThreatReport , Never > ( ThreatReport ( ) )
14
+
15
+ static private var task : Task < ( ) , Never > ?
16
+
17
+ /// Time to wait for cycle recheck threads before running check again
18
+ /// > Should be positive and greater than 0
19
+ static public var threatReportsRegenerationDelaySeconds : Int {
20
+ get { delaySeconds }
21
+ set { if newValue >= 1 { delaySeconds = newValue } }
22
+ }
23
+
24
+ /// Use this API to get ThreatReports
25
+ ///
26
+ /// > First access will start Tasks and cache the result.
27
+ /// Next calls will not start any additional Tasks. Original result
28
+ /// will be cached and can be reused
29
+ static public var threatReports : AnyPublisher < ThreatReport , Never > {
30
+ if task != nil {
31
+ return reportPublisher. eraseToAnyPublisher ( )
32
+ }
33
+ task = Task {
34
+ Task {
35
+ repeat {
36
+ let status = JailbreakDetector . threatDetected ( )
37
+ reportPublisher. update { $0. copy ( rootPrivileges: status) }
38
+ await insertDelay ( )
39
+ } while !Task. isCancelled
40
+ }
41
+ Task {
42
+ repeat {
43
+ let status = HooksDetector . threatDetected ( )
44
+ reportPublisher. update { $0. copy ( hooks: status) }
45
+ await insertDelay ( )
46
+ } while !Task. isCancelled
47
+ }
48
+ Task {
49
+ let status = SimulatorDetector . threatDetected ( )
50
+ reportPublisher. update { $0. copy ( simulator: status) }
51
+ }
52
+ Task {
53
+ repeat {
54
+ let status = DebuggerDetector . threatDetected ( )
55
+ reportPublisher. update { $0. copy ( debugger: status) }
56
+ await insertDelay ( )
57
+ } while !Task. isCancelled
58
+ }
59
+ Task {
60
+ repeat {
61
+ let status = DevicePasscodeDetector . threatDetected ( )
62
+ reportPublisher. update { $0. copy ( devicePasscode: status) }
63
+ await insertDelay ( )
64
+ } while !Task. isCancelled
65
+ }
66
+ Task {
67
+ let status = HardwareSecurityDetector . threatDetected ( )
68
+ reportPublisher. update { $0. copy ( hardwareCryptography: status) }
69
+ }
70
+ }
71
+ return reportPublisher. eraseToAnyPublisher ( )
72
+ }
73
+
74
+ // MARK: - Sync API
4
75
5
76
/// Will check if jailbreak is present
6
77
///
7
- /// - Returns:
8
- /// `true`, if device is / was jailbroken;
9
- /// `false` otherwise
10
- ///
11
78
/// More about jailbreak: https://wikipedia.org/wiki/Jailbreak_%28iOS%29
12
79
///
13
80
/// > Should also detect jailbreak, even if the device is in a "safe" mode or
14
81
/// jailbreak mode is not active / was not properly removed
15
- public static var areRootPrivilegesDetected : Bool {
16
- JailbreakDetection . threatDetected ( )
82
+ public static var rootPrivilegesStatus : ThreatStatus {
83
+ JailbreakDetector . threatDetected ( )
17
84
}
18
85
19
86
/// Will check for an injection tool like Frida
20
87
///
21
- /// - Returns:
22
- /// `true`, if dynamic hooks are loaded at the time;
23
- /// `false` otherwise
24
- ///
25
88
/// More: https://fingerprint.com/blog/exploring-frida-dynamic-instrumentation-tool-kit/
26
89
///
27
90
/// > By the nature of dynamic hooks, this checks should be made on a regular
@@ -30,45 +93,30 @@ public final class ThreatDetectionCenter {
30
93
///
31
94
/// > Important: with a sufficient reverse engineering skills, this check can
32
95
/// be disabled. Use always in combination with another threats detections.
33
- public static var areHooksDetected : Bool {
34
- HooksDetection . threatDetected ( )
96
+ public static var hooksStatus : ThreatStatus {
97
+ HooksDetector . threatDetected ( )
35
98
}
36
99
37
100
/// 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 ( )
101
+ public static var simulatorStatus : ThreatStatus {
102
+ SimulatorDetector . threatDetected ( )
44
103
}
45
104
46
105
/// Will check, if the application is being traced by a debugger.
47
106
///
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
- ///
55
107
/// A debugger is a tool that allows developers to inspect and modify the
56
108
/// execution of a program in real-time, potentially exposing sensitive data
57
109
/// or allowing unauthorized control.
58
110
///
59
111
/// > Please note that Apple itself may require a debugger for the app review
60
112
/// process.
61
- public static var isDebuggerDetected : Bool ? {
62
- DebuggerDetection . threatDetected ( )
113
+ public static var debuggerStatus : ThreatStatus {
114
+ DebuggerDetector . threatDetected ( )
63
115
}
64
116
65
117
/// 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
+ public static var devicePasscodeStatus : ThreatStatus {
119
+ DevicePasscodeDetector . threatDetected ( )
72
120
}
73
121
74
122
/// Will check, if current device has hardware protection layer
@@ -78,61 +126,34 @@ public final class ThreatDetectionCenter {
78
126
///
79
127
/// More: https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave
80
128
///
81
- /// - Returns:
82
- /// `true`, if device has no hardware protection;
83
- /// `false` otherwise
84
- ///
85
129
/// > Should be evaluated on a real device. Should only be used as an
86
130
/// indicator, if current device is capable of hardware protection. Does not
87
131
/// automatically mean, that encryption operations (keys, certificates,
88
132
/// keychain) are always backed by hardware. You should make sure, such
89
133
/// operations are implemented correctly with hardware layer
90
- public static var isHardwareProtectionUnavailable : Bool {
91
- HardwareSecurityDetection . threatDetected ( )
134
+ public static var hardwareCryptographyStatus : ThreatStatus {
135
+ HardwareSecurityDetector . threatDetected ( )
92
136
}
93
-
94
137
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
138
+ // MARK: - Private API
139
+
140
+ static private func insertDelay( ) async {
141
+ try ? await Task . sleep ( nanoseconds: UInt64 ( delaySeconds) * NSEC_PER_SEC)
105
142
}
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
- }
143
+ }
126
144
127
- if DevicePasscodeDetection . threatDetected ( ) {
128
- continuation. yield ( . deviceWithoutPasscode)
145
+ fileprivate extension CurrentValueSubject where Output: Equatable {
146
+ /// Use this function to update a value in the publisher atomically
147
+ func update( _ callback: ( Output ) -> Output ) {
148
+ while true {
149
+ let value = self . value
150
+ let newValue = callback ( value)
151
+ if value == newValue {
152
+ return
153
+ } else if self . value == value {
154
+ self . value = newValue
155
+ return
129
156
}
130
-
131
- if HardwareSecurityDetection . threatDetected ( ) {
132
- continuation. yield ( . hardwareProtectionUnavailable)
133
- }
134
-
135
- continuation. finish ( )
136
157
}
137
158
}
138
159
}
0 commit comments