Skip to content

Commit f82606e

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

16 files changed

+361
-164
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: 105 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,87 @@
11
import Foundation
2+
import Combine
23

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

Comments
 (0)