Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

225 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🛡️ DeviceSecurityKit

DeviceSecurityKit

Lightweight iOS & visionOS Security Detection Framework

Detect jailbreaks, debuggers, emulators, Frida, runtime hooks, SSL pinning bypasses, VPN/proxy usage and more.

Swift iOS visionOS SPM License


🚀 Features

Category Detection
🔓 Jailbreak Files, sandbox escape, fork capability, URL schemes, symlinks, environment variables
🐞 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
📺 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 Extension Detection Flags third-party keyboards active while a sensitive field (password, OTP, payment) is being edited
🎯 Concurrent Threat Tracking currentThreats exposes every threat active right now, not just the most severe one
🌍 Localization All user-facing strings (threat/status/severity descriptions, reports) ship via a String Catalog and adapt to the device's locale
🔏 Signed Reports Tamper-evident SignedSecurityReport with Secure Enclave-backed ECDSA signing — backends can verify reports weren't forged or replayed client-side

📦 Installation

Swift Package Manager

Xcode

  1. File → Add Package Dependencies
  2. Enter:
https://github.com/galahador/DeviceSecurityKit.git
  1. Select:
from: "0.40.0"
  1. Add Package

Package.swift

dependencies: [
    .package(
        url: "https://github.com/galahador/DeviceSecurityKit.git",
        from: "0.40.0"
    )
]

⚡ Quick Start

##
import DeviceSecurityKit

DSK.shared
    .configure(.production)
    .onThreatDetected { threat in
        print("Threat: \(threat.description)")
    }
    .start()

🎯 Usage

Configure

DSK.shared
    .configure(.production)
    .start()

Threat Monitoring

DSK.shared
    .onThreatDetected { threat in
        print(threat.description)
    }
    .start()

Security Status Monitoring

DSK.shared
    .onStatusChange { status in
        print(status)
    }
    .start()

One-Shot Security Check

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.

Async API

let result = await DSK.shared.performCheckAsync()
let secure = await DSK.shared.isSecureAsync()

App Attest

let attestation = try await DSK.shared.attest(challengeHash: serverChallenge)
// Send `attestation` to your backend for verification

AsyncStream

Subscribe 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.

There's an equivalent stream for status transitions:

Task {
    for await status in DSK.shared.statusUpdates {
        print("Status changed to \(status)")
    }
}

Signed Threat Reports

DSKReportSigner produces cryptographically signed SignedSecurityReport values that your backend can verify were generated by a legitimate, unmodified app — not forged or replayed client-side.

On physical devices the signing key lives in the Secure Enclave — the raw private key bytes never leave the chip. On the iOS Simulator a software P256 key is used instead.

At install time — register the public key with your backend

// Call once after first launch and send the key to your server.
let pubKey = try DSKReportSigner.shared.publicKeyData() // 65-byte x963 P256 point
uploadPublicKey(pubKey, forInstallID: installID)

On each check — produce and send a signed report

// Convenience — runs performCheck() and signs the result in one call.
let report = try DSK.shared.signedCheck()

// Or sign an existing result:
let result = DSK.shared.performCheck()
let report = try DSKReportSigner.shared.sign(result)

// report contains:
//   .payload    — canonical JSON (SecurityResult + nonce + timestamp)
//   .signature  — DER-encoded ECDSA-SHA256 signature over payload
//   .publicKey  — uncompressed P256 public key (65 bytes)
//   .generatedAt — UTC timestamp

uploadToBackend(report) // send as JSON; all fields are Codable

The payload embeds a fresh UUID nonce on every call — even identical SecurityResult values produce a different payload each time. Store seen nonces on the backend and reject any report that reuses one.

Client-side integrity check (optional)

if report.isSignatureValid() {
    // payload hasn't been tampered with after signing
    // note: this does NOT prove the key is trusted — that's the backend's job
}

Backend verification (pseudocode)

1. Assert report.publicKey == pinned key for this installID
2. Verify ECDSA-SHA256(report.signature, report.payload) using report.publicKey
3. Decode report.payload as JSON, extract `nonce`
4. Reject if nonce has been seen before (replay protection)
5. Read `result.threats`, `result.riskScore`, etc.

Key rotation

Call deleteKey() to force a new key pair on the next sign. Remember to re-register the new public key with your backend after rotation.

try DSKReportSigner.shared.deleteKey()

Custom signer injection

Inject any DSKReportSigning conformer — useful for tests, App Attest-backed signing, or other custom schemes:

let report = try DSK.shared.signedCheck(using: myCustomSigner)

Threat History

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()

Threat History Persistence

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.

SwiftUI

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)")
            }
        }
    }
}

🚨 Responding To Threats

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()

⚙️ Configuration

Presets

.configure(.default)
.configure(.production)
.configure(.jailbreakOnly)
.configure(.disabled)

Custom Configuration

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()

Clipboard Monitoring

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()

External Display Detection

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. Not applicable on visionOS, which has no discrete external-display concept — isExternalDisplayConnected() always returns false there.

Keyboard Extension Detection

When withKeyboardExtensionDetection(true) is enabled, mark sensitive fields as they become active so DSK can flag a third-party keyboard typing into them:

textField.becomeFirstResponder()
KeyboardExtensionMonitor.markSensitiveFieldActive(textField)

// When editing ends:
KeyboardExtensionMonitor.markSensitiveFieldInactive()

If the active input mode isn't one of Apple's own (com.apple.*) while a sensitive field is marked active, DSK reports SecurityThreat.thirdPartyKeyboardActive. The detection is time-boxed by KeyboardExtensionMonitor.detectionWindowSeconds (default: 10s).

Concurrent Threats

status collapses to the single worst-severity threat, but DSK actually tracks every threat that's active at once:

let active = DSK.shared.currentThreats // Set<SecurityThreat>

if active.contains(.jailbreak) && active.contains(.screenRecording) {
    // both conditions are true concurrently, not just the highest-severity one
}

In SwiftUI, DSKObservable.activeThreats publishes the same set reactively.

🔒 Anti-Repackaging

Obtain Your Certificate Hash

#if DEBUG
print(
    RepackagingDetector.currentCertificateHash()
)
#endif

Configure

.withAntiRepackagingCheck(
    true,
    expectedCertificateHash:
    "your-hash-here"
)

🔄 Signature Updates

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.

🌐 VPN Allowlist

.withVPNProxyDetection(
    true,
    allowedBundleIDs: [
        "com.cisco.anyconnect",
        "com.microsoft.intune.tunnel"
    ]
)

⏱️ Monitoring Interval

DSK.shared
    .monitoringInterval(30)
    .start()

Default: 60 seconds

Adaptive Monitoring

DSK uses exponential backoff to balance responsiveness with efficiency:

  • Threat detected — interval snaps to minMonitoringInterval for 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.currentMonitoringInterval

Check Coalescing

Rapid or concurrent calls to performCheck() / isSecure (e.g. from several call sites within milliseconds of each other) are coalesced into a single detector sweep instead of each triggering a full ~20-detector pass:

DSK.shared
    .checkCoalescingWindow(0.5) // default: 0.5s
    .start()

Background Monitoring (BGTaskScheduler)

DSK can run a security check while the app is suspended, via BGAppRefreshTask:

let identifier = "com.example.app.dsk-refresh"

DSK.shared
    .registerBackgroundTask(identifier: identifier)

let submitted = 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().

scheduleBackgroundCheck(identifier:earliestBeginDate:) returns @discardableResult Boolfalse if BGTaskScheduler rejected the submission (e.g. too many pending requests), which is also logged via SecurityLogger.

Add the identifier to your Info.plist:

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.example.app.dsk-refresh</string>
</array>

🎯 Countermeasures

Countermeasures are automatic actions that fire when a threat is detected.

Any Threat

DSK.shared
    .countermeasure(throttled: false) { threat in
        Analytics.log("dsk_threat", ["type": threat.rawValue])
    }

Specific Threat

DSK.shared
    .countermeasure(for: .jailbreak, throttled: true) { _ in
        AuthManager.shared.clearTokens()
    }

Severity-Based

DSK.shared
    .countermeasure(forMinimumSeverity: .critical, throttled: true) { threat in
        KeychainManager.shared.wipe()
        exit(0)
    }

Custom Countermeasure Object

let cm = Countermeasure(
    trigger: .threat(.fridaDetected),
    throttled: true
) { _ in
    exit(0)
}

DSK.shared.addCountermeasure(cm)

Remove Countermeasures

DSK.shared.removeCountermeasure(cm)
DSK.shared.removeAllCountermeasures()

Throttled countermeasures execute once every 300 seconds per threat type. Adjust with .threatCallbackThrottleInterval(_:).

🔔 Event Sinks

For observers that outlive a single closure (e.g. an analytics or logging object), conform to SecurityEventSink instead of using the callback-based handlers:

final class SecurityAuditLogger: SecurityEventSink {
    func threatDetected(_ event: ThreatEvent) {
        AuditLog.record(event)
    }

    func statusChanged(to status: SecurityStatus) {
        AuditLog.record(status)
    }

    func checkCompleted(_ result: SecurityResult) {
        AuditLog.record(result.riskScore, result.riskLevel)
    }
}

let sink = SecurityAuditLogger()
DSK.shared.addEventSink(sink)

// Later:
DSK.shared.removeEventSink(sink)
DSK.shared.removeAllEventSinks()

statusChanged(to:) and checkCompleted(_:) have default no-op implementations, so a sink only needs to implement threatDetected(_:).

Detector Diagnostics

Inspect per-detector timing and timeout information from the most recent check:

for (name, diagnostic) in DSK.shared.lastDetectorDiagnostics {
    print("\(name): \(diagnostic.duration)s, timedOut: \(diagnostic.timedOut)")
}

Custom Screen Recording Provider

isScreenBeingRecorded() is backed by UIScreen.main.isCaptured on iOS by default (always false on visionOS, which has no UIScreen). Inject your own ScreenRecordingProvider conformer for testing or custom logic:

DSK.shared
    .screenRecordingProvider(myCustomProvider)
    .start()

📊 Threat Severity

Severity Meaning
🟢 Normal No threat detected
🔵 Low Informational
🟡 Medium Potential risk
🟠 High Dangerous environment
🔴 Critical Immediate action recommended

📚 API Reference

SecurityMonitor

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

🚩 Supported Threats

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

🌍 Localization

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.


📋 Info.plist

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

📱 Requirements

Requirement Version
iOS 15.0+
visionOS 1.0+
Swift 5.9+
Xcode 15.0+

visionOS support omits detectors tied to concepts that don't exist there: ExternalDisplayDetector.isExternalDisplayConnected() and the default ScreenRecordingProvider both return false (no UIScreen/discrete-display concept). Everything else — jailbreak, debugger, Frida, hook, integrity, attestation, clipboard, keyboard-extension, etc. — behaves the same as on iOS.


Limitations

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.

🤝 Contributing

Issues and pull requests are welcome.

For major changes, please open an issue first.


📄 License

MIT License

Created by @galahador


🛡️ Security First • Zero Dependencies • Open Source Forever

About

A lightweight iOS security detection library for Swift that identifies compromised runtime conditions in real time.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages