Skip to content

Commit adb5d70

Browse files
committed
feat: rewrite Async API
Signed-off-by: Denis Dobanda <[email protected]>
1 parent cf8e04e commit adb5d70

16 files changed

+388
-168
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1620"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "SecurityToolkit"
19+
BuildableName = "SecurityToolkit"
20+
BlueprintName = "SecurityToolkit"
21+
ReferencedContainer = "container:">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
</TestAction>
33+
<LaunchAction
34+
buildConfiguration = "Debug"
35+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
36+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37+
launchStyle = "0"
38+
useCustomWorkingDirectory = "NO"
39+
ignoresPersistentStateOnLaunch = "NO"
40+
debugDocumentVersioning = "YES"
41+
debugServiceExtension = "internal"
42+
allowLocationSimulation = "YES">
43+
</LaunchAction>
44+
<ProfileAction
45+
buildConfiguration = "Release"
46+
shouldUseLaunchSchemeArgsEnv = "YES"
47+
savedToolIdentifier = ""
48+
useCustomWorkingDirectory = "NO"
49+
debugDocumentVersioning = "YES">
50+
<MacroExpansion>
51+
<BuildableReference
52+
BuildableIdentifier = "primary"
53+
BlueprintIdentifier = "SecurityToolkit"
54+
BuildableName = "SecurityToolkit"
55+
BlueprintName = "SecurityToolkit"
56+
ReferencedContainer = "container:">
57+
</BuildableReference>
58+
</MacroExpansion>
59+
</ProfileAction>
60+
<AnalyzeAction
61+
buildConfiguration = "Debug">
62+
</AnalyzeAction>
63+
<ArchiveAction
64+
buildConfiguration = "Release"
65+
revealArchiveInOrganizer = "YES">
66+
</ArchiveAction>
67+
</Scheme>

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
iOS Mobile Security Toolkit Library Changelog
22
===========================
33

4+
# 2.0.0
5+
* Introduced async API: threatReports
6+
7+
48
# 1.1.1
59
* Fixed podspec deployment
610

CONTRIBUTING.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,6 @@ and reference the old one instead.
5555

5656
## Development
5757

58-
This project contains a private code, not available for public reading or
59-
editing. Please unterstand this measurement to prevent security risks.
60-
Developing new features or fixing bugs can be done in both private and public
61-
parts of this project. You are welcome to participate in public part!
62-
63-
6458
### Setup
6559

6660
Please use the latest Xcode Version. Use the provided example project to test

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 2024 Exxeta
1+
Copyright 2025 Exxeta
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy of
44
this software and associated documentation files (the “Software”), to deal in

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
![License](https://img.shields.io/github/license/EXXETA/Android-Security-Toolkit.svg?style=flat-square)
44
![Release](https://img.shields.io/github/release/EXXETA/Android-Security-Toolkit.svg?style=flat-square)
5+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FEXXETA%2FiOS-Security-Toolkit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/EXXETA/iOS-Security-Toolkit)
6+
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FEXXETA%2FiOS-Security-Toolkit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/EXXETA/iOS-Security-Toolkit)
7+
58

69
<img src="./docs/1.png" width=300 alt="screenshot"/>
710

@@ -35,7 +38,7 @@ Swift Package Manager
3538
### SPM
3639

3740
`.package(url: "https://github.com/EXXETA/iOS-Security-Toolkit.git", from:
38-
"1.1.1")`
41+
"2.0.0")`
3942

4043
## CocoaPods
4144

SecurityToolkit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'SecurityToolkit'
3-
s.version = '1.1.1'
3+
s.version = '2.0.0'
44
s.summary = 'Simple and easy security threat detector in Swift'
55
s.homepage = 'https://github.com/EXXETA/iOS-Security-Toolkit'
66
s.license = { :type => 'MIT', :file => 'LICENSE.md' }
Lines changed: 132 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,112 @@
11
import Foundation
2+
import Combine
23

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

Comments
 (0)