Lightweight iOS Security Detection Framework
Detect jailbreaks, debuggers, emulators, Frida, runtime hooks, SSL pinning bypasses, VPN/proxy usage and more.
| Category | Detection |
|---|---|
| π Jailbreak | Files, sandbox escape, fork capability, URL schemes, symlinks, environment variables, TrollStore markers, and anti-detection tweak signatures (Shadow, Liberty Lite, A-Bypass) |
| π Debugger | sysctl, ptrace, parent process, timing analysis, breakpoint instructions |
| π± Emulator | Hardware mismatch, simulator artifacts, DeviceCheck validation |
| 𧬠Reverse Engineering | Frida, Substrate, libhooker, runtime tampering |
| π App Integrity | Code signature validation, Team ID verification, CodeResources hash validation |
| πͺ Hook Detection | Runtime function hook detection via ARM64 prologue inspection |
| π Swizzling Detection | Objective-C IMP redirection validation, including biometric (LAContext) method hooking |
| πΎ Frida Detection | Libraries, symbols, process checks, multi-port scanning, Frida Gadget dylib signatures, frida-server filesystem artifacts |
| πΊ Screen Recording | Active recording and mirroring detection |
| πΈ Screenshot Detection | Real-time screenshot notifications |
| π Pinning Bypass Detection | Detects bypass tools (SSLKillSwitch, ssl-proxy, etc.) and proxy configurations β not a substitute for implementing certificate/public-key pinning in your networking stack |
| π VPN / Proxy Detection | VPN interfaces and proxy configuration detection |
| π App Attest | Apple App Attest validation |
| π¦ Anti-Repackaging | Signing certificate verification |
| π‘οΈ DSK Integrity | Runtime validation of DSK internals |
| β±οΈ Monitoring | Continuous background security monitoring, BGTaskScheduler integration |
| π Signature Updates | Ed25519-verified remote updates to detection lists |
| π’ MDM Detection | Flags devices running under an enterprise Managed App Configuration |
| π Clipboard Monitoring | Detects unexpected pasteboard changes after the app copies sensitive data |
| π₯οΈ External Display Detection | Flags AirPlay screen mirroring or external/wired monitor connections |
| β¨οΈ Keyboard Detection | Flags third-party keyboard extensions active on sensitive input fields |
| π Localization | All user-facing strings (threat/status/severity descriptions, reports) ship via a String Catalog and adapt to the device's locale |
| π οΈ CI Scanning | dsk-scan CLI runs static integrity/repackaging checks against a built .ipa/.app for CI pre-submission gates |
import DeviceSecurityKit
DSK.shared
.configure(.production)
.onThreatDetected { threat in
print("Threat: \(threat.description)")
}
.start()- File β Add Package Dependencies
- Enter:
https://github.com/galahador/DeviceSecurityKit.git
- Select:
from: "0.40.0"
- Add Package
dependencies: [
.package(
url: "https://github.com/galahador/DeviceSecurityKit.git",
from: "0.40.0"
)
]DSK.shared
.configure(.production)
.start()DSK.shared
.onThreatDetected { threat in
print(threat.description)
}
.start()DSK.shared
.onStatusChange { status in
print(status)
}
.start()let result = DSK.shared.performCheck()
if result.isSecure {
print("Secure")
} else {
print(result.threats)
}
performCheck()runs all enabled detectors synchronously and may take several seconds. Call it off the main thread, or use the async variant below.
let result = await DSK.shared.performCheckAsync()
let secure = await DSK.shared.isSecureAsync()let attestation = try await DSK.shared.attest(challengeHash: serverChallenge)
// Send `attestation` to your backend for verificationSubscribe to a live stream of threat events:
Task {
for await event in DSK.shared.threatEvents {
print("\(event.threat) at \(event.detectedAt)")
print("Evidence: \(event.evidence)")
}
}Multiple consumers can subscribe independently. The stream ends when stop() is called or the consuming Task is cancelled.
DSK keeps a ring buffer of recent ThreatEvents so you can inspect detections after the fact:
let history = DSK.shared.threatHistory
for event in history {
print("\(event.threat) β \(event.detectedAt)")
}Configure the buffer size (default: 100) and clear it:
DSK.shared
.threatHistoryMaxSize(200)
.start()
// Later:
DSK.shared.clearThreatHistory()Opt in to persist threatHistory to the Keychain so a tampering event survives an app relaunch or kill β useful for forensics:
let config = DeviceSecurityConfiguration.default
.withThreatHistoryPersistence(true)
DSK.shared
.configure(config)
.start()History is rehydrated from the Keychain on init when enabled, and clearThreatHistory() also wipes the persisted copy. Disabled by default.
DSKObservable wraps DSK as an ObservableObject, publishing status and threatHistory for use directly in SwiftUI views:
struct ContentView: View {
@StateObject private var dsk = DSKObservable()
var body: some View {
VStack {
Text("Status: \(dsk.status.rawValue)")
List(dsk.threatHistory) { event in
Text("\(event.threat) β \(event.detectedAt)")
}
}
}
}DSK.shared
.onThreatDetected { threat in
switch threat.severity {
case .critical:
AuthManager.shared.clearTokens()
KeychainManager.shared.wipe()
Analytics.log(
"security_threat",
["type": threat.rawValue]
)
exit(0)
case .high:
showSecurityAlert()
default:
break
}
}
.start().configure(.default)
.configure(.production)
.configure(.jailbreakOnly)
.configure(.disabled)let config = DeviceSecurityConfiguration.default
.withJailbreakCheck(true)
.withDebuggerCheck(true)
.withEmulatorCheck(false)
.withReverseEngineeringCheck(true)
.withScreenRecordingCheck(true)
.withScreenshotDetection(true)
.withHookDetection(true)
.withPinningBypassDetection(true)
.withSwizzlingDetection(true)
.withFridaDetection(true)
.withAttestationCheck(true)
.withVPNProxyDetection(
true,
allowedBundleIDs: [
"com.example.corporate-vpn"
]
)
.withAppIntegrityCheck(
true,
expectedTeamID: "ABCDE12345"
)
.withAntiRepackagingCheck(
true,
expectedCertificateHash: "a1b2c3..."
)
.withMDMDetection(true)
.withClipboardMonitoring(true)
.withExternalDisplayDetection(true)
.withKeyboardExtensionDetection(true)
DSK.shared
.configure(config)
.start()iOS does not let an app detect when another process reads the pasteboard. As a
practical proxy, call ClipboardMonitor.markSensitiveCopy() right after copying
sensitive data (passwords, OTPs, tokens) to the clipboard. This baselines
UIPasteboard.general.changeCount; if the pasteboard changes again β by this app or
another process β without another markSensitiveCopy() call, DSK reports
SecurityThreat.clipboardExfiltration.
UIPasteboard.general.string = oneTimePasscode
ClipboardMonitor.markSensitiveCopy()When withExternalDisplayDetection(true) is enabled, DSK checks
UIScreen.screens.count > 1 to detect AirPlay screen mirroring or a connected
external/wired monitor. If a second screen is present, DSK reports
SecurityThreat.externalDisplayConnected β useful for hiding sensitive content
(e.g. with secureScreen(dsk:)) while the device's screen is being mirrored.
When withKeyboardExtensionDetection(true) is enabled, mark sensitive fields
(password, OTP, payment, etc.) as they become active so DSK can flag a
third-party keyboard extension being used to type into them:
func textFieldDidBeginEditing(_ textField: UITextField) {
KeyboardExtensionMonitor.markSensitiveFieldActive(textField)
}
func textFieldDidEndEditing(_ textField: UITextField) {
KeyboardExtensionMonitor.markSensitiveFieldInactive()
}If the active input mode isn't an Apple system keyboard while a sensitive
field is marked active, DSK reports SecurityThreat.thirdPartyKeyboardActive
within a configurable detection window (KeyboardExtensionMonitor.detectionWindowSeconds, default 10s).
#if DEBUG
print(
RepackagingDetector.currentCertificateHash()
)
#endif.withAntiRepackagingCheck(
true,
expectedCertificateHash:
"your-hash-here"
)SignatureUpdateManager extends DSK's built-in detection lists (jailbreak paths, debugger process names, reverse-engineering libraries, etc.) with additional entries from a remotely-distributed, Ed25519-signed manifest. Detectors append these entries to their static lists, so you can react to newly-discovered jailbreak tools or hooking frameworks without shipping an app update.
SignatureUpdateManager.shared
.configure(publicKey: yourEd25519PublicKey)
let manifest = try await SignatureUpdateManager.shared.update(
from: manifestURL
)
print(manifest.version)
print(SignatureUpdateManager.shared.entries(for: .jailbreakPaths))The manifest is a signed envelope (payload + signature); update(from:) verifies the signature against the configured public key before applying or caching it. An invalid signature throws SignatureUpdateError.invalidSignature, and calling update(from:) before configure(publicKey:) throws .notConfigured. The most recently verified manifest is cached on disk and reloaded automatically the next time configure(publicKey:) is called.
dsk-scan is a standalone CLI executable shipped alongside the package. It runs the same
static checks as AppIntegrityDetector/RepackagingDetector β but offline, against a built
.ipa or .app β so you can gate CI before submitting to the App Store.
swift run dsk-scan Build/MyApp.ipa \
--expected-certificate-hash a1b2c3... \
--expected-team-id ABCDE12345.withVPNProxyDetection(
true,
allowedBundleIDs: [
"com.cisco.anyconnect",
"com.microsoft.intune.tunnel"
]
)DSK.shared
.monitoringInterval(30)
.start()Default: 60 seconds
DSK uses exponential backoff to balance responsiveness with efficiency:
- Threat detected β interval snaps to
minMonitoringIntervalfor rapid re-checking. - Consecutive clean cycles β interval doubles each cycle:
base Γ 2^cleanCycles, clamped to[min, max].
DSK.shared
.monitoringInterval(60) // base interval
.minMonitoringInterval(10) // fastest re-check
.maxMonitoringInterval(600) // slowest backoff
.start()Query the current adaptive interval at any time:
let current = DSK.shared.currentMonitoringIntervalDSK can run a security check while the app is suspended, via BGAppRefreshTask:
let identifier = "com.example.app.dsk-refresh"
DSK.shared
.registerBackgroundTask(identifier: identifier)
DSK.shared.scheduleBackgroundCheck(identifier: identifier)registerBackgroundTask(identifier:) should be called during app launch (before applicationDidFinishLaunching returns). Each run automatically reschedules the next check and calls performCheckAsync().
Add the identifier to your Info.plist:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.example.app.dsk-refresh</string>
</array>Countermeasures are automatic actions that fire when a threat is detected.
DSK.shared
.countermeasure(throttled: false) { threat in
Analytics.log("dsk_threat", ["type": threat.rawValue])
}DSK.shared
.countermeasure(for: .jailbreak, throttled: true) { _ in
AuthManager.shared.clearTokens()
}DSK.shared
.countermeasure(forMinimumSeverity: .critical, throttled: true) { threat in
KeychainManager.shared.wipe()
exit(0)
}let cm = Countermeasure(
trigger: .threat(.fridaDetected),
throttled: true
) { _ in
exit(0)
}
DSK.shared.addCountermeasure(cm)DSK.shared.removeCountermeasure(cm)
DSK.shared.removeAllCountermeasures()Throttled countermeasures execute once every 300 seconds per threat type. Adjust with
.threatCallbackThrottleInterval(_:).
| Severity | Meaning |
|---|---|
| π’ Normal | No threat detected |
| π΅ Low | Informational |
| π‘ Medium | Potential risk |
| π High | Dangerous environment |
| π΄ Critical | Immediate action recommended |
| Method | Description |
|---|---|
| performCheck() | Run all configured checks |
| isSecure() | Quick security status |
| startMonitoring() | Begin monitoring |
| stopMonitoring() | Stop monitoring |
| configure() | Update configuration |
| onStatusChange() | Status callback |
| onThreatDetected() | Threat callback |
| Threat | Severity |
|---|---|
| Jailbreak | π΄ Critical |
| Reverse Engineering | π΄ Critical |
| App Integrity Failure | π΄ Critical |
| Hook Detection | π΄ Critical |
| Method Swizzling | π΄ Critical |
| Pinning Bypass | π΄ Critical |
| Frida | π΄ Critical |
| Attestation Failure | π΄ Critical |
| DSK Tampering | π΄ Critical |
| Repackaging | π΄ Critical |
| Debugger | π High |
| Screen Recording | π High |
| Emulator | π‘ Medium |
| VPN / Proxy | π‘ Medium |
| Screenshot | π‘ Medium |
| MDM / Enterprise Management | π’ Low |
| Clipboard Exfiltration | π‘ Medium |
| External Display Connected | π‘ Medium |
| Third-Party Keyboard Active | π‘ Medium |
All user-facing strings β SecurityThreat.description, ThreatSeverity.description, SecurityStatus.description, RiskLevel.description, and SecurityResult.generateReport(...) β are sourced from a String Catalog (Localizable.xcstrings) bundled with DSK. They automatically follow the host app's locale; no extra setup is required. Contribute additional translations by adding languages to the catalog.
<key>LSApplicationQueriesSchemes</key>
<array>
<string>cydia</string>
<string>sileo</string>
<string>zbra</string>
<string>filza</string>
<string>undecimus</string>
<string>checkra1n</string>
<string>taurine</string>
<string>odyssey</string>
<string>dopamine</string>
</array>| Requirement | Version |
|---|---|
| iOS | 15.0+ |
| Swift | 5.9+ |
| Xcode | 15.0+ |
DeviceSecurityKit is a client-side detection library. All checks run within the app process on the user's device, which means:
- Bypassable by a determined attacker. Anyone with full control of the device (root access, custom kernel, instrumentation frameworks) can intercept, patch, or suppress any check. No client-side security library can prevent this.
- Best used as a signal, not a gate. Treat detection results as one input into a broader risk-assessment pipeline. Combine them with server-side validation (App Attest, device posture APIs, backend anomaly detection) for defence in depth.
- False positives are possible. Some legitimate developer tools, accessibility software, enterprise MDM profiles, or VPN configurations may trigger detections. Test thoroughly with your user base and use the configuration API to disable checks that don't apply.
- Simulator environment. Several detectors are automatically disabled in the iOS Simulator (
#if targetEnvironment(simulator)) because they would always trigger. Test security-critical flows on a real device.
Issues and pull requests are welcome.
For major changes, please open an issue first.
MIT License
Created by @galahador
π‘οΈ Security First β’ Zero Dependencies β’ Open Source Forever
