From f0c96abd05877c0d3f3d66519f72ac761430bb11 Mon Sep 17 00:00:00 2001 From: Dennis Paler Date: Wed, 4 Mar 2026 22:25:48 +0800 Subject: [PATCH 1/2] chore: bump version to 0.2.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2ac2e65..17b3439 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@think-grid-labs/react-native-shield", - "version": "0.1.0", + "version": "0.2.0", "description": "All-in-one security suite", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", @@ -160,4 +160,4 @@ ], "version": "0.57.0" } -} +} \ No newline at end of file From 2b499914598018f17e0b56ab2e11a8a544471f68 Mon Sep 17 00:00:00 2001 From: Dennis Paler Date: Thu, 5 Mar 2026 09:20:23 +0800 Subject: [PATCH 2/2] feat: add platform attestation, root reason codes, storage helpers, and biometric strength - requestIntegrityToken(nonce): Play Integrity API (Android) and DeviceCheck (iOS) for server-verifiable device attestation - getRootReasons(): returns reason codes (su_binary, jailbreak_files, sandbox_escape, dangerous_packages, mount_flags) instead of a plain boolean - getAllSecureKeys() / clearAllSecureStorage(): enumerate and bulk-wipe secure storage for key rotation and logout flows - getBiometricStrength(): returns strong | weak | none to distinguish hardware-backed biometrics from weak face unlock fix: updateSSLPins on iOS now rejects with SSL_PIN_UPDATE_UNSUPPORTED instead of silently resolving fix: remove duplicate appDidBecomeActive method in Shield.mm (compile error) deps: add play:integrity:1.4.0 (Android), link DeviceCheck.framework (iOS) docs: update README with use cases, API reference, and security architecture diagram --- README.md | 486 +++++++++++++----- Shield.podspec | 1 + android/build.gradle | 1 + .../src/main/java/com/shield/ShieldModule.kt | 93 ++++ ios/Shield.mm | 170 +++++- package.json | 2 +- src/NativeShield.ts | 13 + src/__tests__/index.test.tsx | 96 +++- src/index.tsx | 41 ++ 9 files changed, 765 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 0a439db..7408a2b 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ ## Features -- **βœ… Device Integrity**: Detect if a device is **Rooted** (Android) or **Jailbroken** (iOS). -- **πŸ•΅οΈ Anti-Tampering**: Detect if the app is running in an **Emulator/Simulator**, or if a **Debugger** is attached to the process. -- **πŸ”’ SSL Pinning**: Secure your network requests against Man-in-the-Middle (MitM) attacks by pinning your server's public key hash. -- **πŸ”‘ Secure Storage**: Store sensitive tokens and keys in the device's secure hardware (Keychain on iOS, EncryptedSharedPreferences on Android). -- **πŸ‘οΈ UI Privacy**: Prevent screenshots and screen recording. Automatically blur the app content when it goes into the background. -- **⚑ TurboModule API**: Built on the New Architecture (Fabric/TurboModules) for synchronous access and optimal performance. +- **βœ… Device Integrity** β€” Detect if a device is **Rooted** (Android) or **Jailbroken** (iOS), with detailed reason codes. +- **πŸ•΅οΈ Anti-Tampering** β€” Detect **Emulator/Simulator**, attached **Debugger**, hooking frameworks (Frida/Xposed), and developer mode. +- **πŸ” Platform Attestation** β€” Request a cryptographically signed **integrity token** from Google (Play Integrity API) or Apple (DeviceCheck) for server-side verification. +- **πŸ”’ SSL Pinning** β€” Secure your network requests against Man-in-the-Middle (MitM) attacks by pinning your server's public key hash. +- **πŸ”‘ Secure Storage** β€” Store sensitive tokens in the device's secure hardware (Keychain on iOS, EncryptedSharedPreferences on Android), with full key enumeration and bulk wipe. +- **🀝 Biometric Authentication** β€” Native biometric prompt (Face ID / Touch ID / Android Biometrics) with strength-level awareness. +- **πŸ‘οΈ UI Privacy** β€” Prevent screenshots and screen recording. Automatically blur app content when backgrounded. +- **⚑ TurboModule API** β€” Built on the New Architecture (TurboModules) for synchronous access and optimal performance. --- @@ -29,226 +31,462 @@ pnpm add @think-grid-labs/react-native-shield ``` ### iOS Setup -The iOS implementation relies on TrustKit for SSL Pinning. Make sure to install the CocoaPods dependencies: + +The iOS implementation relies on TrustKit for SSL pinning and links DeviceCheck for attestation. Install CocoaPods dependencies: + ```sh cd ios && pod install ``` +### Android Setup + +No extra steps needed. Play Integrity (`com.google.android.play:integrity`) is bundled automatically. + --- ## Modules & Usage -Import the library functions into your component or app startup logic: - ```typescript -import { - isRooted, +import { + isRooted, isEmulator, isDebuggerAttached, verifySignature, isHooked, isDeveloperModeEnabled, isVPNDetected, + getRootReasons, protectClipboard, authenticateWithBiometrics, - addSSLPinning, + getBiometricStrength, + addSSLPinning, updateSSLPins, - preventScreenshot, - setSecureString, - getSecureString, - removeSecureString + preventScreenshot, + setSecureString, + getSecureString, + removeSecureString, + getAllSecureKeys, + clearAllSecureStorage, + requestIntegrityToken, } from '@think-grid-labs/react-native-shield'; ``` +--- + ### 1. Device Integrity & Anti-Tampering -Check if the device environment is safe from root access, emulation, or debugging. These use synchronous TurboModule calls for immediate results. +Check if the device environment is safe. All checks are synchronous TurboModule calls for immediate, blocking results β€” safe to use before any sensitive operation. ```typescript const checkIntegrity = () => { if (isRooted()) { - console.warn("Security Alert: Device appears to be rooted or jailbroken."); - // Action to take: block sensitive actions, wipe data, or alert the user. + console.warn('Security Alert: Device is rooted or jailbroken.'); + // Block sensitive actions, wipe session tokens, or alert the user. } - + if (isEmulator()) { - console.warn("Security Alert: App is running in an emulator."); + console.warn('Security Alert: App is running in an emulator.'); + // Reject automated testing environments in production builds. } if (isDebuggerAttached()) { - console.warn("Security Alert: Debugger attached. Potential reverse-engineering attempt."); + console.warn('Security Alert: Debugger attached.'); + // Halt execution to prevent runtime inspection of secrets. } if (isDeveloperModeEnabled()) { - console.warn("Security Alert: ADB/Developer Mode is enabled (Android)."); + console.warn('Security Alert: ADB/Developer Mode is enabled (Android).'); } if (isHooked()) { - console.warn("Security Alert: Injection framework detected (Frida/Xposed)."); + console.warn('Security Alert: Injection framework detected (Frida/Xposed).'); + // Terminate the session β€” the runtime may be instrumented. } - // Supply your expected SHA-256 certificate hash (Android) - if (!verifySignature("YOUR_EXPECTED_HASH")) { - console.warn("Security Alert: App signature mismatch. Possible repackaging."); + if (!verifySignature('YOUR_EXPECTED_SHA256_HASH')) { + console.warn('Security Alert: App signature mismatch β€” possible repackaging.'); + // The APK may have been modified and redistributed. Refuse to run. } }; ``` +#### Root / Jailbreak Reason Codes + +Instead of a plain boolean, get a list of **why** the device is flagged β€” useful for risk scoring, logging, or tuning sensitivity per environment. + +```typescript +const reasons = getRootReasons(); +// Android example: ['su_binary', 'dangerous_packages'] +// iOS example: ['jailbreak_files', 'sandbox_escape'] + +if (reasons.length > 0) { + // Send reasons to your security analytics backend + reportThreat({ reasons, platform: Platform.OS }); +} +``` + +**Possible reason codes:** + +| Code | Platform | Meaning | +|---|---|---| +| `build_tags` | Android | `Build.TAGS` contains `test-keys` | +| `su_binary` | Android | `su` binary found in common paths | +| `su_command` | Android | `which su` command succeeded | +| `dangerous_packages` | Android | Known root manager apps installed (Magisk, SuperSU, etc.) | +| `mount_flags` | Android | `/system` partition mounted read-write | +| `jailbreak_files` | iOS | Cydia, Sileo, Zebra, MobileSubstrate, bash, sshd found | +| `sandbox_escape` | iOS | Write outside sandbox succeeded | +| `cydia_scheme` | iOS | `cydia://` URL scheme is openable | +| `substrate_loaded` | iOS | MobileSubstrate/Substrate dylib loaded into the process | + +**Use cases:** +- **Banking / fintech apps** β€” block transactions when `su_binary` or `sandbox_escape` is present. +- **DRM-protected content** β€” deny playback on devices with `dangerous_packages`. +- **Risk-adaptive UX** β€” show a security warning without fully blocking when only `build_tags` is set (mild signal). + +--- + **Implementation Details:** -- **Root Detection:** - - *Android:* Looks for `test-keys` in build tags, `su` binary execution, and commonly known root directories. - - *iOS:* Checks for common jailbreak files, verifies sandbox write limits, and checks for unauthorized URL schemes. -- **Emulator Detection:** - - *Android:* Checks `Build.FINGERPRINT`, `Build.MODEL`, `Build.HARDWARE`, and `Build.PRODUCT` against known emulator signatures. - - *iOS:* Checks the `TARGET_OS_SIMULATOR` macro at runtime. -- **Debugger Detection:** - - *Android:* Queries `android.os.Debug.isDebuggerConnected()`. - - *iOS:* Queries the kernel via `sysctl` to check if the `P_TRACED` flag is set on the current process. -- **App Signature Verification:** - - *Android:* Hashes the `PackageManager` signing certificates (SHA-256) and compares them against the provided hash string. - - *iOS:* Returns false if `embedded.mobileprovision` is present (meaning not tied to App Store), as deeper programmatic verification is heavily restrictive on iOS. -- **Hooking Detection:** - - *Android:* Probes the classloader for Xposed and Substrate classes, and checks file paths for `frida-server`. - - *iOS:* Scans registered `dyld` dynamic libraries for names like `Frida`, `Substrate`, `cycript`, or `SSLKillSwitch`. -- **Developer Mode Check:** - - *Android:* Looks at `Settings.Global.DEVELOPMENT_SETTINGS_ENABLED` and `ADB_ENABLED`. - - *iOS:* Not supported natively (always returns `false`). - -### 2. App Environment Security - -Protecting user interaction states and network paths. + +| Check | Android | iOS | +|---|---|---| +| Root | `Build.TAGS`, su binary scan, `which su`, known root package names, `/proc/mounts` flags | Jailbreak files, sandbox write test, Cydia URL scheme, dyld substrate scan | +| Emulator | `Build.FINGERPRINT/MODEL/HARDWARE/PRODUCT` patterns | `TARGET_OS_SIMULATOR` compile-time macro | +| Debugger | `android.os.Debug.isDebuggerConnected()` | `sysctl` `P_TRACED` flag | +| Signature | SHA-256 of `PackageManager` signing certs | Presence of `embedded.mobileprovision` | +| Hooking | ClassLoader probe (Xposed/Substrate), frida-server file | `dyld` image name scan (Frida, Substrate, cycript, SSLKillSwitch) | +| Dev mode | `Settings.Global.DEVELOPMENT_SETTINGS_ENABLED`, `ADB_ENABLED` | Not supported (always `false`) | + +--- + +### 2. Platform Attestation + +Request a **cryptographically signed integrity token** from Google or Apple that your server can verify. This is the only mechanism that provides hardware-backed, unforgeable proof of device and app integrity β€” something no local detection heuristic can match. + +```typescript +// Call from your login or session-start flow. +// The nonce must be generated server-side (unique, unpredictable). +const verifyDeviceWithServer = async () => { + try { + const nonce = await fetchNonceFromServer(); // your API + const token = await requestIntegrityToken(nonce); + + // Send token to your backend for verification + await sendToBackend({ token, platform: Platform.OS }); + // Android: verify via Google Play Integrity API + // iOS: verify via Apple DeviceCheck API + } catch (e) { + // 'INTEGRITY_NOT_SUPPORTED' β€” emulator or unsigned build + // 'INTEGRITY_ERROR' β€” network or Play Services unavailable + console.error('Attestation failed:', e); + } +}; +``` + +**Platform behavior:** +- **Android** β€” Uses [Play Integrity API](https://developer.android.com/google/play/integrity). The returned token encodes: `appIntegrity` (cert match + install source), `deviceIntegrity` (hardware attestation), and `accountDetails` (licensed via Play). Verify server-side with `https://playintegrity.googleapis.com`. +- **iOS** β€” Uses [DeviceCheck](https://developer.apple.com/documentation/devicecheck). The returned token is a base64-encoded device attestation verifiable via Apple's DeviceCheck servers. + +**Use cases:** +- **Prevent API abuse** β€” only honor requests from genuine, unmodified app installs. +- **Anti-bot for login flows** β€” rate-limit or block attestation failures. +- **High-value transactions** β€” require a fresh attestation token before wire transfers or crypto withdrawals. +- **License enforcement** β€” confirm the app was installed from the official store, not sideloaded. + +> **Note:** The nonce should be a server-generated, single-use random value (minimum 16 bytes, base64-encoded). Reusing nonces defeats replay protection. + +--- + +### 3. App Environment Security ```typescript const protectAppEnvironment = () => { - // Clear the clipboard string when the app goes into the background + if (isVPNDetected()) { + console.warn('Network path is routed through a VPN or proxy.'); + // Optionally warn users that certificate pinning may behave differently, + // or block access to sensitive features in high-security contexts. + } + + // Auto-clear clipboard whenever the app backgrounds protectClipboard(true); +}; +``` - if (isVPNDetected()) { - console.warn("Network path is utilizing a VPN or proxy."); +**Use cases:** +- **Compliance apps** β€” some regulated environments prohibit VPN usage on managed devices. +- **Clipboard protection** β€” prevent a copied password or OTP from persisting after the user leaves the app (banking, password managers). + +--- + +### 4. Biometric Authentication + +#### Authenticate the user + +```typescript +const loginWithBiometrics = async () => { + const success = await authenticateWithBiometrics('Authenticate to continue'); + if (success) { + // Unlock secure vault, proceed to dashboard, etc. + } else { + // User cancelled or biometric not matched β€” show fallback UI } }; ``` -### 3. Biometric Authentication +#### Check biometric strength before gating features -Launch the platform's native biometric prompt (FaceID/TouchID/Android Biometrics) directly. This method uses a zero-dependency local implementation avoiding additional module bloat. +Different biometric sensors have very different security properties. Face Unlock on Android is classified as "weak" (2D camera, spoofable with a photo). Face ID on iOS and fingerprint readers are "strong" (secure enclave, spoof-resistant hardware). ```typescript -const loginWithFaceID = async () => { - const success = await authenticateWithBiometrics("Please authenticate to unlock secure data"); - if (success) { - // Proceed to unlock - } else { - // Authentication failed or canceled - } +const enforceStrongBiometrics = async () => { + const strength = await getBiometricStrength(); + // Returns: "strong" | "weak" | "none" + + if (strength === 'strong') { + // Unlock cryptographic keys, allow high-value operations + } else if (strength === 'weak') { + // Allow login but require step-up auth for sensitive actions + console.warn('Biometric strength is weak β€” consider requiring a PIN for payments.'); + } else { + // No biometrics enrolled β€” fall back to password/PIN + } }; ``` -### 4. SSL/Certificate Pinning +**Use cases:** +- **Tiered access control** β€” only allow `"strong"` biometrics to authorize payments or PII access; `"weak"` for general app unlock. +- **FIDO2 / passkey flows** β€” gate passkey creation on `"strong"` hardware. +- **Adaptive security UI** β€” surface a warning banner if only weak biometrics are available. + +--- + +### 5. SSL / Certificate Pinning -Prevent MitM attacks by verifying that the server's certificate public key matches your predefined pins. +Prevent MitM attacks by verifying that your server's certificate public key matches your predefined pins. ```typescript -// Call this EARLY in your app's lifecycle (e.g., in App.tsx or index.js) +// Call EARLY in your app lifecycle (e.g., App.tsx root useEffect or index.js). useEffect(() => { - const configureSecurity = async () => { + const configurePinning = async () => { try { - // Provide an array of base64 encoded SHA-256 public key hashes await addSSLPinning('api.yourdomain.com', [ - 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Primary Pin - 'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' // Backup Pin - ]); - console.log('SSL Pinning enabled for api.yourdomain.com'); - - // If pins rotate dynamically, pass new arrays - await updateSSLPins('api.yourdomain.com', [ - 'sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=' + 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Primary pin + 'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' // Backup pin ]); } catch (e) { - console.error('Failed to configure SSL Pinning', e); + console.error('SSL pinning failed:', e); } }; - - configureSecurity(); + configurePinning(); }, []); ``` -**How to get your Pin:** -You must provide the SubjectPublicKeyInfo hash. You can extract this using OpenSSL: +**Rotating pins (Android only):** + +On Android, OkHttp allows the client factory to be replaced at runtime, so `updateSSLPins` takes effect immediately. On iOS, TrustKit locks its configuration at startup β€” `updateSSLPins` will **reject** with `SSL_PIN_UPDATE_UNSUPPORTED`. To rotate iOS pins, update `addSSLPinning` arguments and ship an app update (or restart). + +```typescript +// Android: works at runtime +// iOS: throws SSL_PIN_UPDATE_UNSUPPORTED β€” catch it! +try { + await updateSSLPins('api.yourdomain.com', [ + 'sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=' + ]); +} catch (e: any) { + if (e.code === 'SSL_PIN_UPDATE_UNSUPPORTED') { + // Expected on iOS β€” handled. New pins take effect after next app launch. + } +} +``` + +**How to extract your pin hash:** + ```sh -openssl s_client -servername api.yourdomain.com -connect api.yourdomain.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 +openssl s_client -servername api.yourdomain.com -connect api.yourdomain.com:443 \ + | openssl x509 -pubkey -noout \ + | openssl pkey -pubin -outform der \ + | openssl dgst -sha256 -binary \ + | openssl enc -base64 ``` -### 3. Secure Storage +**Use cases:** +- **Fintech / healthcare** β€” mandatory in PCI-DSS and HIPAA environments to prevent intercepted API calls. +- **Auth token endpoints** β€” pin only your `/token` and `/refresh` endpoints to limit blast radius. +- **Always provide a backup pin** β€” a single pin + certificate expiry = production outage. Always include at least one backup. + +--- + +### 6. Secure Storage -Store sensitive data like authentication tokens or API keys securely using native encrypted abstractions. +Store secrets (tokens, API keys, encryption keys) in the device's hardware-backed secure enclave. ```typescript -const manageSecrets = async () => { - // Save a secret - const success = await setSecureString('user_token', 'xyz-123-abc'); - - // Retrieve a secret - const token = await getSecureString('user_token'); // Returns 'xyz-123-abc' or null - - // Delete a secret - await removeSecureString('user_token'); +// Save +await setSecureString('access_token', 'eyJhbGci...'); + +// Read +const token = await getSecureString('access_token'); // string | null + +// Delete one key +await removeSecureString('access_token'); +``` + +#### Enumerate and audit stored keys + +```typescript +const keys = await getAllSecureKeys(); +// e.g. ['access_token', 'refresh_token', 'user_id'] + +// Useful for: migration scripts, key rotation, debugging storage state +console.log('Stored keys:', keys); +``` + +#### Bulk wipe on logout or account deletion + +```typescript +const handleLogout = async () => { + await clearAllSecureStorage(); + // All keys under this app's bundle ID are gone. + // Safe to call even if storage is already empty. }; ``` **Implementation Details:** -- **Android:** Powered by `androidx.security.crypto.EncryptedSharedPreferences`, utilizing the Android Keystore Master Key system (AES256_GCM). -- **iOS:** Powered by **Keychain Services** (`SecItemAdd`, `SecItemCopyMatching`) to store data securely inside the iOS secure enclave. +- **Android** β€” `EncryptedSharedPreferences` with AES256-GCM, backed by Android Keystore master key. +- **iOS** β€” Keychain Services (`SecItemAdd` / `SecItemCopyMatching`) scoped to the app's bundle ID. -### 6. UI Privacy (Screenshot & Screen Recording Prevention) +**Use cases:** +- `setSecureString` / `getSecureString` β€” persist OAuth tokens, refresh tokens, or biometric-bound keys. +- `getAllSecureKeys` β€” audit what's in storage before a migration; compare against an expected key manifest. +- `clearAllSecureStorage` β€” GDPR "right to erasure" flows, account deletion, forced logout on session invalidation. -Protect sensitive data from being captured by other apps, recordings, or the user. +--- + +### 7. UI Privacy + +Prevent sensitive UI from being captured in screenshots, screen recordings, or the OS app switcher preview. ```typescript -// Enable Privacy Mode +// Enable β€” call once at app startup or when entering a sensitive screen await preventScreenshot(true); -// Disable Privacy Mode +// Disable β€” call when leaving the sensitive context await preventScreenshot(false); ``` -**Platform Behavior & Limitations:** -- **Android:** Sets `WindowManager.LayoutParams.FLAG_SECURE`. - - Prevents physical screenshots and screen recording. - - Automatically hides app content in the "Recent Apps" OS switcher (shows a black or white screen). -- **iOS:** Uses the `UITextField` secureTextEntry hack and background blurs. - - **Screen Recording:** Masks the content using a hidden secure field layer. The content appears hidden in recordings or AirPlay mirroring. - - **App Switcher:** Injects a blur effect when the app resigns active (goes to the background), preventing data leaks in the multitasking view. - - ⚠️ **Known Limitation:** True hardware screenshot prevention (pressing Home+Power) is not officially supported by iOS. This module primarily blocks programmatic recording and hides data in the app switcher. - - ⚠️ **Known Bug:** In the current implementation, calling `preventScreenshot(false)` removes background blurs but does *not* completely dismantle the screen-recording protection layer in the visual hierarchy. Re-rendering might be required. +**Use cases:** +- **Banking / wallet apps** β€” enable on balance screens, transaction history, and transfer confirmations. +- **Password managers** β€” enable globally; users won't be able to screenshot their vault. +- **Healthcare** β€” prevent patient data from appearing in OS recent-apps thumbnails. +- **Chat apps with disappearing messages** β€” enable on message screens to block screen recording. + +**Platform Behavior:** + +| | Android | iOS | +|---|---|---| +| Screenshot | Blocked via `FLAG_SECURE` | Not blockable (OS limitation) | +| Screen recording | Blocked via `FLAG_SECURE` | Content masked via `UITextField.secureTextEntry` layer hack | +| App switcher preview | Replaced with blank screen | Blur overlay injected on `WillResignActive` | + +> ⚠️ **iOS limitation:** Hardware screenshots (Home + Power) cannot be blocked by any third-party app on iOS. `preventScreenshot` primarily protects against screen recording, AirPlay mirroring, and the app switcher preview. --- ## API Reference -| Method | Type | Description | -| :--- | :--- | :--- | -| `isRooted()` | `() => boolean` | Synchronously returns `true` if the device is compromised (rooted/jailbroken). | -| `isEmulator()` | `() => boolean` | Synchronously returns `true` if running in a Simulator/Emulator. | -| `isDebuggerAttached()` | `() => boolean` | Synchronously returns `true` if a debugger is actively attached to the process. | -| `isDeveloperModeEnabled()` | `() => boolean` | Synchronously checks if Developer Options/ADB are enabled (Android only). | -| `isHooked()` | `() => boolean` | Synchronously checks for hooked frameworks (Frida, Xposed, Substrate). | -| `verifySignature(hash)` | `(hash: string) => boolean` | Verifies the app's signing cert matches `hash` (Android) or is valid (iOS). | -| `isVPNDetected()` | `() => boolean` | Synchronously returns `true` if traffic is routed via VPN interfaces. | -| `protectClipboard(protect)`| `(protect: boolean) => Promise` | Toggles auto-clearing the clipboard when UI enters the background. | -| `authenticateWithBiometrics(prompt)`| `(prompt: string) => Promise` | Starts FaceID/TouchID/Android Biometrics prompt. Returns `true` on success. | -| `addSSLPinning(domain, hashes)`| `(domain: string, hashes: string[]) => Promise` | Enforces strict validation of the public key for the specific domain natively. | -| `updateSSLPins(domain, hashes)`| `(domain: string, hashes: string[]) => Promise` | Rotates hashes dynamically (*iOS TrustKit requires app restart to apply*). | -| `preventScreenshot(prevent)` | `(prevent: boolean) => Promise` | Toggles UI protection features (recording prevention, background blur) on/off. | -| `setSecureString(key, value)` | `(key: string, value: string) => Promise` | Encrypts and securely saves a string using Keystore/Keychain. | -| `getSecureString(key)` | `(key: string) => Promise` | Decrypts and retrieves a string from secure storage. | -| `removeSecureString(key)` | `(key: string) => Promise` | Deletes a string securely from storage. | +### Device Integrity + +| Method | Returns | Description | +|---|---|---| +| `isRooted()` | `boolean` | `true` if device is rooted (Android) or jailbroken (iOS) | +| `isEmulator()` | `boolean` | `true` if running in a simulator or emulator | +| `isDebuggerAttached()` | `boolean` | `true` if a debugger is attached to the process | +| `isDeveloperModeEnabled()` | `boolean` | `true` if ADB/developer options are active (Android only) | +| `isHooked()` | `boolean` | `true` if Frida, Xposed, Substrate, or similar is detected | +| `verifySignature(hash)` | `boolean` | `true` if the app's signing cert matches the expected hash | +| `getRootReasons()` | `string[]` | Array of reason codes explaining why the device is flagged | + +### Network & Environment + +| Method | Returns | Description | +|---|---|---| +| `isVPNDetected()` | `boolean` | `true` if traffic is routed through a VPN interface | +| `protectClipboard(protect)` | `Promise` | Toggle auto-clear clipboard on app background | + +### Platform Attestation + +| Method | Returns | Description | +|---|---|---| +| `requestIntegrityToken(nonce)` | `Promise` | Play Integrity token (Android) or DeviceCheck token (iOS) for server-side verification | + +### Biometrics + +| Method | Returns | Description | +|---|---|---| +| `authenticateWithBiometrics(prompt)` | `Promise` | Launches native biometric prompt; resolves `true` on success | +| `getBiometricStrength()` | `Promise` | `"strong"`, `"weak"`, or `"none"` based on enrolled biometric hardware | + +### SSL Pinning + +| Method | Returns | Description | +|---|---|---| +| `addSSLPinning(domain, hashes)` | `Promise` | Enable certificate pinning for a domain | +| `updateSSLPins(domain, hashes)` | `Promise` | Update pins at runtime (Android only; rejects with `SSL_PIN_UPDATE_UNSUPPORTED` on iOS) | + +### Secure Storage + +| Method | Returns | Description | +|---|---|---| +| `setSecureString(key, value)` | `Promise` | Encrypt and store a string | +| `getSecureString(key)` | `Promise` | Decrypt and retrieve a stored string | +| `removeSecureString(key)` | `Promise` | Delete a single key from secure storage | +| `getAllSecureKeys()` | `Promise` | List all keys currently in secure storage | +| `clearAllSecureStorage()` | `Promise` | Delete all keys from secure storage | + +### UI Privacy + +| Method | Returns | Description | +|---|---|---| +| `preventScreenshot(prevent)` | `Promise` | Toggle screenshot/recording prevention and background blur | + +--- + +## Security Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ react-native-shield β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Device Check β”‚ Network Layer β”‚ Data Layer β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ isRooted() β”‚ addSSLPinning() β”‚ setSecureString() β”‚ +β”‚ isEmulator() β”‚ updateSSLPins() β”‚ getSecureString() β”‚ +β”‚ isHooked() β”‚ isVPNDetected() β”‚ removeSecureString() β”‚ +β”‚ getRootReasonsβ”‚ β”‚ getAllSecureKeys() β”‚ +β”‚ β”‚ Attestation β”‚ clearAllSecureStorageβ”‚ +β”‚ isDebugger β”‚ β”‚ β”‚ +β”‚ Attached() β”‚ requestIntegrity β”‚ UI Layer β”‚ +β”‚ verifySigna- β”‚ Token() β”‚ β”‚ +β”‚ ture() β”‚ β”‚ preventScreenshot() β”‚ +β”‚ β”‚ Biometrics β”‚ protectClipboard() β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ authenticateWith β”‚ β”‚ +β”‚ β”‚ Biometrics() β”‚ β”‚ +β”‚ β”‚ getBiometric β”‚ β”‚ +β”‚ β”‚ Strength() β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + ↓ ↓ ↓ + Android/iOS OkHttp / Keystore / + System APIs TrustKit Keychain + Play Integrity / + DeviceCheck +``` --- ## Contributing -See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and set up the development workflow seamlessly. +See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. ## License diff --git a/Shield.podspec b/Shield.podspec index e026a64..2f1c6b8 100644 --- a/Shield.podspec +++ b/Shield.podspec @@ -17,5 +17,6 @@ Pod::Spec.new do |s| s.private_header_files = "ios/**/*.h" s.dependency "TrustKit" + s.frameworks = "DeviceCheck" install_modules_dependencies(s) end diff --git a/android/build.gradle b/android/build.gradle index a531a9b..b13538b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -67,4 +67,5 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:4.9.2" implementation "androidx.security:security-crypto:1.1.0-alpha06" implementation "androidx.biometric:biometric:1.2.0-alpha05" + implementation "com.google.android.play:integrity:1.4.0" } diff --git a/android/src/main/java/com/shield/ShieldModule.kt b/android/src/main/java/com/shield/ShieldModule.kt index edae3d4..46106ff 100644 --- a/android/src/main/java/com/shield/ShieldModule.kt +++ b/android/src/main/java/com/shield/ShieldModule.kt @@ -1,6 +1,7 @@ package com.shield import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.Arguments import java.io.File import java.io.BufferedReader import java.io.InputStreamReader @@ -14,9 +15,12 @@ import android.content.ClipboardManager import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities +import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import com.facebook.react.bridge.UiThreadUtil +import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.IntegrityTokenRequest class ShieldModule(reactContext: ReactApplicationContext) : NativeShieldSpec(reactContext) { @@ -267,6 +271,62 @@ class ShieldModule(reactContext: ReactApplicationContext) : } } + override fun getAllSecureKeys(promise: com.facebook.react.bridge.Promise) { + try { + val keys = Arguments.createArray() + for (key in getEncryptedSharedPreferences().all.keys) { + keys.pushString(key) + } + promise.resolve(keys) + } catch (e: Exception) { + promise.reject("SECURE_STORAGE_ERROR", e) + } + } + + override fun clearAllSecureStorage(promise: com.facebook.react.bridge.Promise) { + try { + getEncryptedSharedPreferences().edit().clear().apply() + promise.resolve(true) + } catch (e: Exception) { + promise.reject("SECURE_STORAGE_ERROR", e) + } + } + + override fun getBiometricStrength(promise: com.facebook.react.bridge.Promise) { + val manager = BiometricManager.from(reactApplicationContext) + when { + manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS -> + promise.resolve("strong") + manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS -> + promise.resolve("weak") + else -> promise.resolve("none") + } + } + + override fun getRootReasons(): com.facebook.react.bridge.WritableArray { + val reasons = Arguments.createArray() + if (checkRootMethod1()) reasons.pushString("build_tags") + if (checkRootMethod2()) reasons.pushString("su_binary") + if (checkRootMethod3()) reasons.pushString("su_command") + if (checkDangerousPackages()) reasons.pushString("dangerous_packages") + if (checkSystemMountFlags()) reasons.pushString("mount_flags") + return reasons + } + + override fun requestIntegrityToken(nonce: String, promise: com.facebook.react.bridge.Promise) { + try { + val integrityManager = IntegrityManagerFactory.create(reactApplicationContext) + val request = IntegrityTokenRequest.builder() + .setNonce(nonce) + .build() + integrityManager.requestIntegrityToken(request) + .addOnSuccessListener { response -> promise.resolve(response.token()) } + .addOnFailureListener { e -> promise.reject("INTEGRITY_ERROR", e.message, e) } + } catch (e: Exception) { + promise.reject("INTEGRITY_ERROR", e.message, e) + } + } + private fun checkRootMethod1(): Boolean { val buildTags = android.os.Build.TAGS return buildTags != null && buildTags.contains("test-keys") @@ -304,6 +364,39 @@ class ShieldModule(reactContext: ReactApplicationContext) : } } + private fun checkDangerousPackages(): Boolean { + val suspiciousPackages = arrayOf( + "com.topjohnwu.magisk", + "eu.chainfire.supersu", + "com.koushikdutta.superuser", + "com.noshufou.android.su", + "com.thirdparty.superuser", + "com.yellowes.su", + "com.zachspong.temprootremovejb", + "com.ramdroid.appquarantine" + ) + val pm = reactApplicationContext.packageManager + for (pkg in suspiciousPackages) { + try { + pm.getPackageInfo(pkg, 0) + return true + } catch (_: PackageManager.NameNotFoundException) {} + } + return false + } + + private fun checkSystemMountFlags(): Boolean { + try { + val reader = BufferedReader(InputStreamReader(File("/proc/mounts").inputStream())) + var line: String? + while (reader.readLine().also { line = it } != null) { + val l = line ?: continue + if (l.contains("/system") && l.contains(" rw,")) return true + } + } catch (_: Exception) {} + return false + } + companion object { const val NAME = "Shield" } diff --git a/ios/Shield.mm b/ios/Shield.mm index 564f310..b8ddb4c 100644 --- a/ios/Shield.mm +++ b/ios/Shield.mm @@ -1,4 +1,5 @@ #import "Shield.h" +#import #import #import #import @@ -218,11 +219,13 @@ - (void)updateSSLPins:(NSString *)domain publicKeyHashes:(NSArray *)publicKeyHashes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - // TrustKit does not support runtime reconfiguration without exception. - // In a fully dynamic scenario, developers should store pins in JS and pass - // them to addSSLPinning on app launch. This method serves as a stub - // to align with the Android API where OkHttp allows factory overrides. - resolve(nil); + // TrustKit does not support runtime reconfiguration after initialization. + // To update pins, store the new hashes and call addSSLPinning on next app + // launch. On Android, OkHttp factory overrides allow runtime updates. + reject(@"SSL_PIN_UPDATE_UNSUPPORTED", + @"TrustKit pins cannot be updated at runtime on iOS. " + @"Store updated hashes and call addSSLPinning on the next app launch.", + nil); } - (void)preventScreenshot:(BOOL)prevent @@ -323,12 +326,6 @@ - (void)appDidBecomeActive { } } -- (void)appDidBecomeActive { - UIWindow *window = [UIApplication sharedApplication].keyWindow; - UIView *blurView = [window viewWithTag:12345]; - [blurView removeFromSuperview]; -} - // Secure Storage Implementation (Keychain) - (void)setSecureString:(NSString *)key @@ -420,6 +417,157 @@ - (void)removeSecureString:(NSString *)key } } +// ─── Secure Storage Helpers ─────────────────────────────────────────────────── + +- (void)getAllSecureKeys:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + NSString *service = [[NSBundle mainBundle] bundleIdentifier]; + NSDictionary *query = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : service, + (__bridge id)kSecReturnAttributes : @YES, + (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitAll + }; + + CFTypeRef result = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); + + if (status == errSecSuccess) { + NSArray *items = (__bridge_transfer NSArray *)result; + NSMutableArray *keys = [NSMutableArray array]; + for (NSDictionary *item in items) { + NSString *account = item[(__bridge id)kSecAttrAccount]; + if (account) [keys addObject:account]; + } + resolve(keys); + } else if (status == errSecItemNotFound) { + resolve(@[]); + } else { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:status + userInfo:nil]; + reject(@"SECURE_STORAGE_ERROR", @"Failed to enumerate keys", error); + } +} + +- (void)clearAllSecureStorage:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + NSString *service = [[NSBundle mainBundle] bundleIdentifier]; + NSDictionary *query = @{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService : service + }; + + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); + if (status == errSecSuccess || status == errSecItemNotFound) { + resolve(@(YES)); + } else { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain + code:status + userInfo:nil]; + reject(@"SECURE_STORAGE_ERROR", @"Failed to clear storage", error); + } +} + +// ─── Biometric Strength ─────────────────────────────────────────────────────── + +- (void)getBiometricStrength:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + LAContext *context = [[LAContext alloc] init]; + NSError *error = nil; + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:&error]) { + // On iOS, Face ID and Touch ID are both classified as strong biometrics. + resolve(@"strong"); + } else { + resolve(@"none"); + } +} + +// ─── Jailbreak Reason Codes ────────────────────────────────────────────────── + +- (NSArray *)collectJailbreakReasons { + NSMutableArray *reasons = [NSMutableArray array]; + + NSArray *jailbreakPaths = @[ + @"/Applications/Cydia.app", + @"/Applications/Sileo.app", + @"/Applications/Zebra.app", + @"/Library/MobileSubstrate/MobileSubstrate.dylib", + @"/bin/bash", + @"/usr/sbin/sshd", + @"/etc/apt", + @"/private/var/lib/apt/", + @"/usr/bin/ssh", + ]; + for (NSString *path in jailbreakPaths) { + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + [reasons addObject:@"jailbreak_files"]; + break; + } + } + + NSError *writeError; + [@"check" writeToFile:@"/private/jailbreak_rw_test.txt" + atomically:YES + encoding:NSUTF8StringEncoding + error:&writeError]; + if (!writeError) { + [reasons addObject:@"sandbox_escape"]; + [[NSFileManager defaultManager] removeItemAtPath:@"/private/jailbreak_rw_test.txt" error:nil]; + } + + if ([[UIApplication sharedApplication] + canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]) { + [reasons addObject:@"cydia_scheme"]; + } + + uint32_t count = _dyld_image_count(); + for (uint32_t i = 0; i < count; i++) { + const char *name = _dyld_get_image_name(i); + if (name) { + NSString *imageName = [NSString stringWithUTF8String:name]; + if ([imageName containsString:@"MobileSubstrate"] || + [imageName containsString:@"Substrate"]) { + [reasons addObject:@"substrate_loaded"]; + break; + } + } + } + + return [reasons copy]; +} + +- (NSArray *)getRootReasons { + return [self collectJailbreakReasons]; +} + +// ─── Platform Integrity Attestation ───────────────────────────────────────── + +- (void)requestIntegrityToken:(NSString *)nonce + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + if (@available(iOS 11.0, *)) { + DCDevice *device = [DCDevice currentDevice]; + if (![device isSupported]) { + reject(@"INTEGRITY_NOT_SUPPORTED", + @"DeviceCheck is not supported on this device", nil); + return; + } + [device generateTokenWithCompletionHandler:^(NSData *token, NSError *error) { + if (error) { + reject(@"INTEGRITY_ERROR", error.localizedDescription, error); + } else { + resolve([token base64EncodedStringWithOptions:0]); + } + }]; + } else { + reject(@"INTEGRITY_NOT_SUPPORTED", @"DeviceCheck requires iOS 11+", nil); + } +} + +// ───────────────────────────────────────────────────────────────────────────── + - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); diff --git a/package.json b/package.json index 17b3439..01c12c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@think-grid-labs/react-native-shield", - "version": "0.2.0", + "version": "0.2.1", "description": "All-in-one security suite", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", diff --git a/src/NativeShield.ts b/src/NativeShield.ts index c6f5280..88ba3fa 100644 --- a/src/NativeShield.ts +++ b/src/NativeShield.ts @@ -16,6 +16,19 @@ export interface Spec extends TurboModule { setSecureString(key: string, value: string): Promise; getSecureString(key: string): Promise; removeSecureString(key: string): Promise; + + // Enhanced integrity detection + getRootReasons(): string[]; + + // Secure storage helpers + getAllSecureKeys(): Promise; + clearAllSecureStorage(): Promise; + + // Biometric capability + getBiometricStrength(): Promise; + + // Platform attestation + requestIntegrityToken(nonce: string): Promise; } export default TurboModuleRegistry.getEnforcing('Shield'); diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index 701f0e3..7a29095 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -5,10 +5,14 @@ import { setSecureString, getSecureString, removeSecureString, + getRootReasons, + getAllSecureKeys, + clearAllSecureStorage, + getBiometricStrength, + requestIntegrityToken, } from '../index'; import NativeShield from '../NativeShield'; -// Prepare the mock jest.mock('../NativeShield', () => { return { __esModule: true, @@ -19,6 +23,11 @@ jest.mock('../NativeShield', () => { setSecureString: jest.fn(), getSecureString: jest.fn(), removeSecureString: jest.fn(), + getRootReasons: jest.fn(), + getAllSecureKeys: jest.fn(), + clearAllSecureStorage: jest.fn(), + getBiometricStrength: jest.fn(), + requestIntegrityToken: jest.fn(), }, }; }); @@ -34,6 +43,21 @@ describe('react-native-shield', () => { expect(isRooted()).toBe(true); expect(NativeShield.isRooted).toHaveBeenCalledTimes(1); }); + + it('getRootReasons returns array of reason strings', () => { + (NativeShield.getRootReasons as jest.Mock).mockReturnValue([ + 'su_binary', + 'dangerous_packages', + ]); + const reasons = getRootReasons(); + expect(reasons).toEqual(['su_binary', 'dangerous_packages']); + expect(NativeShield.getRootReasons).toHaveBeenCalledTimes(1); + }); + + it('getRootReasons returns empty array on clean device', () => { + (NativeShield.getRootReasons as jest.Mock).mockReturnValue([]); + expect(getRootReasons()).toEqual([]); + }); }); describe('SSL Pinning', () => { @@ -67,7 +91,7 @@ describe('react-native-shield', () => { }); describe('Secure Storage', () => { - it('setSecureString pass correct arguments', async () => { + it('setSecureString passes correct arguments', async () => { (NativeShield.setSecureString as jest.Mock).mockResolvedValue(true); const result = await setSecureString('key', 'value'); expect(result).toBe(true); @@ -87,5 +111,73 @@ describe('react-native-shield', () => { expect(result).toBe(true); expect(NativeShield.removeSecureString).toHaveBeenCalledWith('key'); }); + + it('getAllSecureKeys returns array of keys', async () => { + (NativeShield.getAllSecureKeys as jest.Mock).mockResolvedValue([ + 'token', + 'refresh_token', + ]); + const keys = await getAllSecureKeys(); + expect(keys).toEqual(['token', 'refresh_token']); + expect(NativeShield.getAllSecureKeys).toHaveBeenCalledTimes(1); + }); + + it('getAllSecureKeys returns empty array when storage is empty', async () => { + (NativeShield.getAllSecureKeys as jest.Mock).mockResolvedValue([]); + expect(await getAllSecureKeys()).toEqual([]); + }); + + it('clearAllSecureStorage resolves true', async () => { + (NativeShield.clearAllSecureStorage as jest.Mock).mockResolvedValue(true); + const result = await clearAllSecureStorage(); + expect(result).toBe(true); + expect(NativeShield.clearAllSecureStorage).toHaveBeenCalledTimes(1); + }); + }); + + describe('Biometrics', () => { + it('getBiometricStrength returns "strong"', async () => { + (NativeShield.getBiometricStrength as jest.Mock).mockResolvedValue( + 'strong' + ); + expect(await getBiometricStrength()).toBe('strong'); + }); + + it('getBiometricStrength returns "weak"', async () => { + (NativeShield.getBiometricStrength as jest.Mock).mockResolvedValue( + 'weak' + ); + expect(await getBiometricStrength()).toBe('weak'); + }); + + it('getBiometricStrength returns "none" when unavailable', async () => { + (NativeShield.getBiometricStrength as jest.Mock).mockResolvedValue( + 'none' + ); + expect(await getBiometricStrength()).toBe('none'); + }); + }); + + describe('Platform Attestation', () => { + it('requestIntegrityToken passes nonce and returns token string', async () => { + const fakeToken = 'base64encodedtoken=='; + (NativeShield.requestIntegrityToken as jest.Mock).mockResolvedValue( + fakeToken + ); + const result = await requestIntegrityToken('server-nonce-xyz'); + expect(result).toBe(fakeToken); + expect(NativeShield.requestIntegrityToken).toHaveBeenCalledWith( + 'server-nonce-xyz' + ); + }); + + it('requestIntegrityToken rejects when unsupported', async () => { + (NativeShield.requestIntegrityToken as jest.Mock).mockRejectedValue( + new Error('INTEGRITY_NOT_SUPPORTED') + ); + await expect(requestIntegrityToken('nonce')).rejects.toThrow( + 'INTEGRITY_NOT_SUPPORTED' + ); + }); }); }); diff --git a/src/index.tsx b/src/index.tsx index 585bc95..5cb61c6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -67,3 +67,44 @@ export function getSecureString(key: string): Promise { export function removeSecureString(key: string): Promise { return Shield.removeSecureString(key); } + +/** + * Returns an array of reason strings for why the device is considered rooted + * or jailbroken (e.g. "su_binary", "build_tags", "jailbreak_files"). + * Returns an empty array if no indicators are found. + */ +export function getRootReasons(): string[] { + return Shield.getRootReasons(); +} + +/** + * Returns all keys currently stored in secure storage. + */ +export function getAllSecureKeys(): Promise { + return Shield.getAllSecureKeys(); +} + +/** + * Deletes all entries from secure storage. + */ +export function clearAllSecureStorage(): Promise { + return Shield.clearAllSecureStorage(); +} + +/** + * Returns the strongest biometric authentication level available on the device: + * "strong" (fingerprint / Face ID / iris), "weak" (face unlock), or "none". + */ +export function getBiometricStrength(): Promise { + return Shield.getBiometricStrength(); +} + +/** + * Requests a platform integrity token. + * - Android: Play Integrity API token (verify server-side with Google) + * - iOS: DeviceCheck token (verify server-side with Apple) + * @param nonce An unpredictable, server-generated value to bind the token. + */ +export function requestIntegrityToken(nonce: string): Promise { + return Shield.requestIntegrityToken(nonce); +}