Flutter RASP

pub package License: MIT Platform

RASP (Runtime Application Self-Protection) for Flutter. Detects runtime tampering and ships every event to your own backend.

Features

  • Threat detection (16 threats including root/jailbreak, hooks, debugger, repackaging, untrusted install, emulator, VPN…).
  • Real-time monitoring with native-level termination when a configured threat fires.
  • Screen capture protection.
  • SSL certificate pinning (plain .pem, encrypted .enc, remote cert updates with secure storage).
  • Built-in security reporter — every detected threat and policy- triggered exit shipped to your HTTPS endpoint with HMAC + optional pinning. Flutter/Dart error capture is available but off by default (opt in via captureFlutterErrors / capturePlatformErrors).
Platform Minimum
Android API 24
iOS 13.0
dependencies:
  flutter_rasp: ^6.1.3

Threat detection

Threat Android iOS
Root / Jailbreak
Emulator / Simulator
Debugger
Hooks (Frida / Xposed)
Repackaging
Trusted install
VPN
Device passcode
Secure hardware
Screen capture block
Developer mode
ADB enabled
Obfuscation
Time spoofing
Location spoofing
Multi-instance

Initialize

Recommended order: set up SSL pinning first, then initialize RASP (and, optionally, the reporter). Doing pinning first resolves and caches the certificate, so the reporter can reuse it through pinnedCertPem — no second fetch, no duplicated cert handling.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 1. SSL pinning first.
  const pinningConfig = SslPinningConfig(
    certificateAssetPaths: ['assets/certs/api.pem'],
  );
  await SslPinningClient.createContext(pinningConfig);

  // 2. RASP monitoring (+ optional reporter).
  await FlutterRasp.instance.initialize(
    config: const RaspConfig(
      policy: ThreatPolicy.high,
      monitoringInterval: Duration(seconds: 10),
      androidConfig: AndroidRaspConfig(
        signingCertHashes: ['<sha256-from-Play-Console>'],
      ),
      iosConfig: IosRaspConfig(
        teamId: '<APPLE-TEAM-ID>',
        bundleIds: ['com.your.bundle'],
      ),
    ),
    onThreatDetected: (threats) => debugPrint('$threats'),
    // Optional — ship reports to your backend.
    reporter: ReporterConfig(
      endpoint: Uri.parse('https://your-backend.example.com/v1/ingest'),
      // Optional — pin the reporter's internal HTTP client too.
      // Omit it to fall back to system TLS validation (no pinning).
      pinnedCertPem: SslPinningClient.cachedPem(pinningConfig),
    ),
  );

  runApp(const MyApp());
}

Provide at least one of onThreatDetected or threatCallback. The reporter argument is optional — omit it if you don't need backend reporting. Pinning the reporter's HTTP client is also optional: pass pinnedCertPem to pin it, or leave it null for system TLS validation. Flutter/Dart error capture is off by default; see Security reporter.

signingCertHashes is the SHA-256 of the Play signing key (Play Console → App integrity → App signing). teamId is on developer.apple.com → Membership Details.

Threat policies

Policy Terminates on
ThreatPolicy.none nothing (report-only)
ThreatPolicy.low repackaging
ThreatPolicy.medium + root, hook, obfuscationIssues, multiInstance
ThreatPolicy.high + debug, devicePasscode, secureHardware, adbEnabled, locationSpoofing

Or roll your own:

const policy = ThreatPolicy(exitThreats: {Threat.root, Threat.vpn});

On-demand scan

final result = await FlutterRasp.instance.scanAll();
if (result.isCompromised) print(result.detectedThreats);

Or per-threat: isRooted(), isEmulator(), isHooked(), isVpnConnected(), etc. — one method per Threat value.

Screen capture protection

await FlutterRasp.instance.blockScreenCapture(true);

SSL certificate pinning

const config = SslPinningConfig(
  certificateAssetPaths: ['assets/certs/api.pem'],
);
final client = await SslPinningClient.createHttpClient(config);

Works with dart:io, Dio (IOHttpClientAdapter), package:http (IOClient).

Encrypted PEM — encrypt with the Certificate Encryptor web tool, pass passphrase:

const config = SslPinningConfig(
  certificateAssetPaths: ['assets/certs/api.enc'],
  passphrase: 'your_passphrase',
);

Remote updates — fetch a new cert at runtime; the plugin stores it in Keychain (iOS) / EncryptedSharedPreferences (Android) and falls back to the asset if the fetch fails:

final ctx = await SslPinningClient.createContext(
  config,
  onFetchRemote: () => yourApi.fetchCert(),
);

Resolution chain: memory → stored cert (background refresh) → remote fetch → asset.

SslPinningClient.cachedPem(config) returns the resolved PEM bytes synchronously after the first call — forward to the reporter's pinnedCertPem to reuse the same cert without a second fetch.


Security reporter

Pass a ReporterConfig to FlutterRasp.initialize(...) and the plugin ships every detected threat and policy-triggered exit to your backend.

Flutter/Dart errors are not captured by default. In development they can be extremely noisy, so captureFlutterErrors and capturePlatformErrors both default to false. Set them to true when you want framework / uncaught Dart errors shipped too.

await FlutterRasp.instance.initialize(
  config: const RaspConfig(policy: ThreatPolicy.high, /* ... */),
  onThreatDetected: (threats) => debugPrint('$threats'),
  reporter: ReporterConfig(
    endpoint: Uri.parse('https://your-backend.example.com/v1/ingest'),
    headers: const {'X-Project-Id': 'my-app'},
    hmacKey: const String.fromEnvironment('RASP_HMAC_KEY'),
    pinnedCertPem: SslPinningClient.cachedPem(apiConfig), // optional
  ),
);

Configuration

Field Default Purpose
captureFlutterErrors false Hook FlutterError.onError. Disabled by default — opt in.
capturePlatformErrors false Hook PlatformDispatcher.onError. Disabled by default — opt in.
captureExitThreats true Ship a report synchronously before RASP kills the process.
captureDetectedThreats true Auto-ship when a new threat is observed (deduped per session).
maxBreadcrumbs 50 FIFO cap.
maxPendingReports 50 On-disk queue cap.
exitTimeout 1.5 s Max wait for the exit report before termination.
httpTimeout 1.2 s Per-attempt timeout.
retryBackoffs [3 s, 9 s, 27 s] Backoff schedule.
userId null Optional user.id.

Wire format

{
  "schemaVersion": 1,
  "reportId": "<uuid-hex>",
  "timestamp": "2026-05-27T10:30:00.000Z",
  "sessionId": "<uuid-hex>",
  "event": "enforcedExit",
  "crashThreat": "root",
  "detectedThreats": ["root", "hook"],
  "message": "enforcedExit triggered by: root",
  "breadcrumbs": [
    {
      "ts": "...",
      "category": "rasp",
      "level": "warning",
      "message": "threats detected"
    }
  ],
  "device": {
    "id": "<sha256-hex>",
    "platform": "android",
    "model": "Pixel 7",
    "manufacturer": "Google",
    "osVersion": "14",
    "apiLevel": 34,
    "locale": "es-ES",
    "country": "CO",
    "timezone": "America/Bogota",
    "isPhysicalDevice": true
  },
  "app": {
    "packageName": "com.example.app",
    "version": "1.2.3",
    "build": "456",
    "installer": "com.android.vending",
    "firstInstallMs": 1747000000000,
    "lastUpdateMs": 1747500000000,
    "buildType": "release",
    "abi": "arm64-v8a"
  },
  "network": {
    "type": "wifi",
    "carrier": "Claro",
    "mcc": "732",
    "mnc": "101"
  },
  "policy": { "exitThreats": ["root", "hook", "repackaging"] },
  "user": { "id": "<optional>" },
  "extras": { "source": "native" }
}
Field Description
event threatsDetected · enforcedExit · flutterError · dartError · manualCapture.
crashThreat Set only on enforcedExit — the threat that triggered termination.
detectedThreats All threats observed in this event.
policy.exitThreats The exact list the integrator configured as exitThreats.
app.installer Android: package name (Play, Amazon…) or sideload. iOS: appStore / testFlight / simulator / dev / enterprise / sideload / unknown.
network.type wifi / cellular / ethernet / vpn / none / unknown. Carrier + MCC/MNC on Android with a SIM.
extras.source enforcedExit ships "native" (built and shipped from the native exit path).

When hmacKey is set, every body is signed with HMAC-SHA256 and forwarded as the X-Rasp-Signature header.

Reliability

  • Pending reports are persisted encrypted at rest. If delivery fails or the process dies, the report ships on next launch.
  • enforcedExit reports are shipped synchronously before the process is killed, on a bounded time budget you control with exitTimeout.
  • 4xx → permanent rejection (dropped). 5xx / network errors → retried with backoff.

What we don't collect

Source IP (your backend already has it), precise location, advertising id, IMEI, MAC, SIM serial.

Try it locally

The example/ app bundles a Dart mock backend that renders every report it receives. See example/README.md for the three-step setup.

mock backend dashboard


Architecture

Flutter App
    ▼
Dart (public API): FlutterRasp · RaspReporter · SslPinning
    ▼  Pigeon (typed bridge)
flutter_rasp plugin (Kotlin / Swift)
    ▼  in-process
flutter_rasp_core (precompiled AAR / XCFramework)
  detectors · reporter · screen capture

Detectors and the reporter ship compiled and obfuscated. The Dart layer is intentionally thin.


License

MIT. See LICENSE.

Libraries

flutter_rasp
A comprehensive RASP (Runtime Application Self-Protection) plugin for Flutter.