Skip to content

Commit 410d40e

Browse files
committed
feat: rewrite Async API
Signed-off-by: Denis Dobanda <[email protected]> s Signed-off-by: Denis Dobanda <[email protected]>
1 parent 8c7b9a5 commit 410d40e

10 files changed

+278
-155
lines changed

Sources/ThreatDetectionCenter.swift

Lines changed: 99 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,90 @@
11
import Foundation
2+
import Combine
23

34
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
475

576
/// Will check if jailbreak is present
677
///
7-
/// - Returns:
8-
/// `true`, if device is / was jailbroken;
9-
/// `false` otherwise
10-
///
1178
/// More about jailbreak: https://wikipedia.org/wiki/Jailbreak_%28iOS%29
1279
///
1380
/// > Should also detect jailbreak, even if the device is in a "safe" mode or
1481
/// 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()
1784
}
1885

1986
/// Will check for an injection tool like Frida
2087
///
21-
/// - Returns:
22-
/// `true`, if dynamic hooks are loaded at the time;
23-
/// `false` otherwise
24-
///
2588
/// More: https://fingerprint.com/blog/exploring-frida-dynamic-instrumentation-tool-kit/
2689
///
2790
/// > By the nature of dynamic hooks, this checks should be made on a regular
@@ -30,45 +93,30 @@ public final class ThreatDetectionCenter {
3093
///
3194
/// > Important: with a sufficient reverse engineering skills, this check can
3295
/// 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()
3598
}
3699

37100
/// 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()
44103
}
45104

46105
/// Will check, if the application is being traced by a debugger.
47106
///
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-
///
55107
/// A debugger is a tool that allows developers to inspect and modify the
56108
/// execution of a program in real-time, potentially exposing sensitive data
57109
/// or allowing unauthorized control.
58110
///
59111
/// > Please note that Apple itself may require a debugger for the app review
60112
/// process.
61-
public static var isDebuggerDetected: Bool? {
62-
DebuggerDetection.threatDetected()
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+
public static var devicePasscodeStatus: ThreatStatus {
119+
DevicePasscodeDetector.threatDetected()
72120
}
73121

74122
/// Will check, if current device has hardware protection layer
@@ -78,61 +126,34 @@ public final class ThreatDetectionCenter {
78126
///
79127
/// More: https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave
80128
///
81-
/// - Returns:
82-
/// `true`, if device has no hardware protection;
83-
/// `false` otherwise
84-
///
85129
/// > Should be evaluated on a real device. Should only be used as an
86130
/// indicator, if current device is capable of hardware protection. Does not
87131
/// automatically mean, that encryption operations (keys, certificates,
88132
/// keychain) are always backed by hardware. You should make sure, such
89133
/// 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()
92136
}
93-
94137

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)
105142
}
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+
}
126144

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
129156
}
130-
131-
if HardwareSecurityDetection.threatDetected() {
132-
continuation.yield(.hardwareProtectionUnavailable)
133-
}
134-
135-
continuation.finish()
136157
}
137158
}
138159
}

Sources/ThreatReport.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Foundation
2+
3+
/// An object describing latest detection status of every
4+
/// threat detectors
5+
public struct ThreatReport: Equatable, Hashable {
6+
public let rootPrivileges: ThreatStatus
7+
public let hooks: ThreatStatus
8+
public let simulator: ThreatStatus
9+
public let debugger: ThreatStatus
10+
public let devicePasscode: ThreatStatus
11+
public let hardwareCryptography: ThreatStatus
12+
13+
public init(
14+
rootPrivileges: ThreatStatus = .notChecked,
15+
hooks: ThreatStatus = .notChecked,
16+
simulator: ThreatStatus = .notChecked,
17+
debugger: ThreatStatus = .notChecked,
18+
devicePasscode: ThreatStatus = .notChecked,
19+
hardwareCryptography: ThreatStatus = .notChecked
20+
) {
21+
self.rootPrivileges = rootPrivileges
22+
self.hooks = hooks
23+
self.simulator = simulator
24+
self.debugger = debugger
25+
self.devicePasscode = devicePasscode
26+
self.hardwareCryptography = hardwareCryptography
27+
}
28+
29+
func copy(
30+
rootPrivileges: ThreatStatus? = nil,
31+
hooks: ThreatStatus? = nil,
32+
simulator: ThreatStatus? = nil,
33+
debugger: ThreatStatus? = nil,
34+
devicePasscode: ThreatStatus? = nil,
35+
hardwareCryptography: ThreatStatus? = nil
36+
) -> ThreatReport {
37+
return ThreatReport(
38+
rootPrivileges: rootPrivileges ?? self.rootPrivileges,
39+
hooks: hooks ?? self.hooks,
40+
simulator: simulator ?? self.simulator,
41+
debugger: debugger ?? self.debugger,
42+
devicePasscode: devicePasscode ?? self.devicePasscode,
43+
hardwareCryptography: hardwareCryptography ?? self.hardwareCryptography
44+
)
45+
}
46+
}
47+
48+
public enum ThreatStatus: Equatable, Hashable {
49+
case notChecked
50+
case notPresent
51+
case present
52+
case exception(ThreatDetectionException)
53+
54+
public static func ==(lhs: ThreatStatus, rhs: ThreatStatus) -> Bool {
55+
switch (lhs, rhs) {
56+
case (.notChecked, .notChecked):
57+
return true
58+
case (.notPresent, .notPresent):
59+
return true
60+
case (.present, .present):
61+
return true
62+
case (.exception(let s1), .exception(let s2)):
63+
return s1 == s2
64+
default:
65+
return false
66+
}
67+
}
68+
}
69+
70+
public enum ThreatDetectionException: Error, Equatable, Hashable {
71+
case checkNotPossible(String)
72+
}

Sources/internal/DebuggerDetection.swift

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Foundation
2+
3+
// MARK: - Internal
4+
internal final class DebuggerDetector {
5+
6+
static func threatDetected() -> ThreatStatus {
7+
do {
8+
let check = try hasTracerFlagSet()
9+
return check ? .present : .notPresent
10+
} catch let e {
11+
let ex = e as? ThreatDetectionException
12+
?? ThreatDetectionException.checkNotPossible(e.localizedDescription)
13+
return .exception(ex)
14+
}
15+
}
16+
}
17+
18+
// MARK: - Private
19+
fileprivate extension DebuggerDetector {
20+
21+
/// Check P_TRACED flag from Darwin Kernel
22+
/// if the process is traced
23+
private static func hasTracerFlagSet() throws -> Bool {
24+
var info = kinfo_proc()
25+
// Kernel info, process info, specific process by PID, get current process ID
26+
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
27+
var size = MemoryLayout.stride(ofValue: info)
28+
29+
let unixStatusCode = sysctl(&mib, u_int(mib.count), &info, &size, nil, 0)
30+
31+
if unixStatusCode != 0 {
32+
throw ThreatDetectionException.checkNotPossible("Unexpected unix status code")
33+
}
34+
35+
return (info.kp_proc.p_flag & P_TRACED) != 0
36+
}
37+
}

0 commit comments

Comments
 (0)