device_security 1.0.1 copy "device_security: ^1.0.1" to clipboard
device_security: ^1.0.1 copied to clipboard

Flutter plugin for device security detection: root/jailbreak, emulator, hooking framework, and virtual camera detection with confidence scoring.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:device_security/device_security.dart';

void main() => runApp(const SecurityDemoApp());

class SecurityDemoApp extends StatelessWidget {
  const SecurityDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Device Security Demo',
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

// ─────────────────────── Home ───────────────────────

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Security Check'),
          bottom: const TabBar(tabs: [
            Tab(text: 'Full Check'),
            Tab(text: 'Liveness'),
            Tab(text: 'Streaming'),
          ]),
        ),
        body: const TabBarView(children: [
          FullCheckTab(),
          LivenessTab(),
          StreamingTab(),
        ]),
      ),
    );
  }
}

// ─────────────────── Tab 1: Full Check ──────────────────

class FullCheckTab extends StatefulWidget {
  const FullCheckTab({super.key});

  @override
  State<FullCheckTab> createState() => _FullCheckTabState();
}

class _FullCheckTabState extends State<FullCheckTab>
    with AutomaticKeepAliveClientMixin {
  SecurityResult? _result;
  bool _loading = false;

  @override
  bool get wantKeepAlive => true;

  Future<void> _runCheck() async {
    setState(() => _loading = true);
    final result = await DeviceSecurityChecker.check();
    setState(() {
      _result = result;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    if (_loading) return const Center(child: CircularProgressIndicator());
    if (_result == null) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: FilledButton.icon(
            onPressed: _runCheck,
            icon: const Icon(Icons.security),
            label: const Text('Run full check'),
            style: FilledButton.styleFrom(
              minimumSize: const Size(double.infinity, 56),
            ),
          ),
        ),
      );
    }
    return _ResultListView(result: _result!, onRefresh: _runCheck);
  }
}

// ─────────────────── Tab 2: Liveness ──────────────────

class LivenessTab extends StatefulWidget {
  const LivenessTab({super.key});

  @override
  State<LivenessTab> createState() => _LivenessTabState();
}

class _LivenessTabState extends State<LivenessTab>
    with AutomaticKeepAliveClientMixin {
  LivenessCheckResult? _preResult;
  LivenessVerification? _verification;
  bool _loading = false;
  String? _status;

  @override
  bool get wantKeepAlive => true;

  Future<void> _runPreCheck() async {
    setState(() {
      _loading = true;
      _status = 'Running environment pre-check...';
      _verification = null;
    });

    final pre = await LivenessGuard.preCheck();

    setState(() {
      _preResult = pre;
      _loading = false;
      _status = pre.isEnvironmentSafe
          ? 'Environment safe. Perform liveness action, then tap Verify.'
          : 'Environment NOT safe — liveness should be blocked.';
    });
  }

  Future<void> _runVerify() async {
    if (_preResult == null) return;
    setState(() {
      _loading = true;
      _status = 'Capturing sensor data & verifying...';
    });

    final v = await LivenessGuard.verify(_preResult!);

    setState(() {
      _verification = v;
      _loading = false;
      _status = v.isPass ? 'PASSED — liveness verified.' : 'FAILED';
    });
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Text('LivenessGuard', style: Theme.of(context).textTheme.titleLarge),
        const SizedBox(height: 4),
        Text(
          'Step 1: preCheck() before showing liveness UI.\n'
          'Step 2: User performs action (nod / blink).\n'
          'Step 3: verify() to confirm it was real.',
          style: Theme.of(context).textTheme.bodySmall,
        ),
        const SizedBox(height: 16),

        // Step 1
        FilledButton.icon(
          onPressed: _loading ? null : _runPreCheck,
          icon: const Icon(Icons.shield),
          label: const Text('1. Pre-check'),
        ),

        if (_preResult != null) ...[
          const SizedBox(height: 8),
          _InfoTile(
            icon: _preResult!.isEnvironmentSafe
                ? Icons.check_circle
                : Icons.error,
            color: _preResult!.isEnvironmentSafe ? Colors.green : Colors.red,
            title: 'Environment',
            subtitle: _preResult!.isEnvironmentSafe ? 'Safe' : 'Unsafe',
          ),
          _InfoTile(
            icon: _preResult!.canCorrelateMotion
                ? Icons.check_circle
                : Icons.warning_amber,
            color: _preResult!.canCorrelateMotion
                ? Colors.green
                : Colors.orange,
            title: 'Motion sensors',
            subtitle: _preResult!.canCorrelateMotion
                ? 'Available'
                : 'Unavailable',
          ),
          if (_preResult!.cameraWarnings.isNotEmpty)
            ..._preResult!.cameraWarnings.map((w) => _InfoTile(
                  icon: Icons.warning_amber,
                  color: Colors.orange,
                  title: 'Camera',
                  subtitle: w,
                )),
        ],

        const SizedBox(height: 12),

        // Step 2 (placeholder)
        Card(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Text(
              'Liveness action happens here (nod, blink, turn head).',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
          ),
        ),

        const SizedBox(height: 12),

        // Step 3
        FilledButton.icon(
          onPressed:
              _loading || _preResult == null ? null : _runVerify,
          icon: const Icon(Icons.verified_user),
          label: const Text('3. Verify'),
        ),

        if (_verification != null) ...[
          const SizedBox(height: 8),
          _InfoTile(
            icon: _verification!.isPass
                ? Icons.check_circle
                : Icons.dangerous,
            color: _verification!.isPass ? Colors.green : Colors.red,
            title: 'Result',
            subtitle: _verification!.isPass ? 'PASSED' : 'FAILED',
          ),
          if (!_verification!.isPass)
            ..._verification!.failReasons.map((r) => _InfoTile(
                  icon: Icons.info_outline,
                  color: Colors.red,
                  title: 'Reason',
                  subtitle: r,
                )),
          _InfoTile(
            icon: Icons.sensors,
            color: Colors.blue,
            title: 'Device moved',
            subtitle:
                '${_verification!.sensorCorrelation.deviceMoved ? "YES" : "NO"} '
                '(accelDelta=${_verification!.sensorCorrelation.accelDelta.toStringAsFixed(4)}, '
                'gyroDelta=${_verification!.sensorCorrelation.gyroDelta.toStringAsFixed(4)})',
          ),
        ],

        if (_status != null) ...[
          const SizedBox(height: 16),
          Text(_status!, style: Theme.of(context).textTheme.bodySmall),
        ],
      ],
    );
  }
}

// ─────────────────── Tab 3: Streaming ──────────────────

class StreamingTab extends StatefulWidget {
  const StreamingTab({super.key});

  @override
  State<StreamingTab> createState() => _StreamingTabState();
}

class _StreamingTabState extends State<StreamingTab>
    with AutomaticKeepAliveClientMixin {
  static const _checkInterval = Duration(seconds: 30);

  final LiveStreamGuard _guard = LiveStreamGuard();
  SecurityResult? _initialResult;
  final List<_CheckLog> _logs = [];
  bool _starting = false;

  // Countdown timer for showing seconds until next check
  Timer? _countdownTimer;
  int _secondsUntilNext = 0;

  @override
  bool get wantKeepAlive => true;

  Future<void> _start() async {
    setState(() {
      _starting = true;
      _logs.clear();
      _initialResult = null;
    });

    final initial = await _guard.start(
      interval: _checkInterval,
      onThreatDetected: (result) {
        if (!mounted) return;
        // Threat detected during a periodic check — in a real app you would
        // pause the stream, show a warning dialog, or kick the streamer.
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          backgroundColor: Colors.red,
          content: Text(
            'THREAT detected! '
            'vcam=${result.virtualCamera.detected}, '
            'replay=${result.replayAttack.detected}, '
            'hook=${result.hookingFramework.detected}',
          ),
          duration: const Duration(seconds: 5),
        ));
      },
      onPeriodicCheck: (result) {
        if (!mounted) return;
        // Every periodic check (threat or not) gets logged here.
        setState(() {
          _logs.add(_CheckLog(
            time: DateTime.now(),
            result: result,
            isThreat: result.isThreatDetected,
          ));
        });
        // Reset countdown
        _secondsUntilNext = _checkInterval.inSeconds;
      },
    );

    // Log the initial full check
    setState(() {
      _initialResult = initial;
      _starting = false;
      _logs.add(_CheckLog(
        time: DateTime.now(),
        result: initial,
        isThreat: initial.isThreatDetected,
        isInitial: true,
      ));
    });

    // Start a 1-second countdown display
    _secondsUntilNext = _checkInterval.inSeconds;
    _countdownTimer?.cancel();
    _countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) {
      if (!mounted || !_guard.isRunning) return;
      setState(() {
        _secondsUntilNext =
            (_secondsUntilNext - 1).clamp(0, _checkInterval.inSeconds);
      });
    });
  }

  void _stop() {
    _guard.stop();
    _countdownTimer?.cancel();
    _countdownTimer = null;
    setState(() {});
  }

  @override
  void dispose() {
    _guard.dispose();
    _countdownTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    final theme = Theme.of(context);

    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Text('LiveStreamGuard', style: theme.textTheme.titleLarge),
        const SizedBox(height: 4),
        Text(
          'Full check at start, then lightweight re-checks every '
          '${_checkInterval.inSeconds}s.\n'
          'Periodic checks only run: Hook + VirtualCamera + ReplayAttack.\n'
          'Root & Emulator results are carried forward from the initial check.',
          style: theme.textTheme.bodySmall,
        ),
        const SizedBox(height: 16),

        // ── Start / Stop buttons ──
        Row(children: [
          Expanded(
            child: FilledButton.icon(
              onPressed: _guard.isRunning || _starting ? null : _start,
              icon: const Icon(Icons.play_arrow),
              label: const Text('Start streaming'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: OutlinedButton.icon(
              onPressed: _guard.isRunning ? _stop : null,
              icon: const Icon(Icons.stop),
              label: const Text('Stop'),
            ),
          ),
        ]),

        if (_starting) ...[
          const SizedBox(height: 16),
          const Center(child: CircularProgressIndicator()),
          const SizedBox(height: 8),
          Center(
            child: Text('Running initial full check...',
                style: theme.textTheme.bodySmall),
          ),
        ],

        // ── Status summary card ──
        if (_initialResult != null) ...[
          const SizedBox(height: 16),
          Card(
            color: _guard.isRunning
                ? Colors.blue.withOpacity(0.05)
                : Colors.grey.withOpacity(0.05),
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(children: [
                    Icon(
                      _guard.isRunning ? Icons.sensors : Icons.sensors_off,
                      color: _guard.isRunning ? Colors.green : Colors.grey,
                      size: 20,
                    ),
                    const SizedBox(width: 8),
                    Text(
                      _guard.isRunning ? 'Monitoring active' : 'Stopped',
                      style: theme.textTheme.titleSmall,
                    ),
                    const Spacer(),
                    if (_guard.isRunning)
                      Container(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 8, vertical: 4),
                        decoration: BoxDecoration(
                          color: Colors.blue.withOpacity(0.1),
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          'Next check in ${_secondsUntilNext}s',
                          style: theme.textTheme.labelSmall,
                        ),
                      ),
                  ]),
                  const SizedBox(height: 12),
                  _statusRow(
                    'Safe for streaming',
                    _initialResult!.isSafeForLiveStreaming,
                  ),
                  _statusRow(
                    'Root / Jailbreak',
                    !_initialResult!.rootJailbreak.detected,
                    trueLabel: 'Clean',
                    falseLabel: 'Detected',
                  ),
                  _statusRow(
                    'Emulator',
                    !_initialResult!.emulator.detected,
                    trueLabel: 'Clean',
                    falseLabel: 'Detected',
                  ),
                  const Divider(height: 16),
                  Text(
                    'Latest periodic results:',
                    style: theme.textTheme.labelMedium,
                  ),
                  const SizedBox(height: 4),
                  _statusRow(
                    'Virtual Camera',
                    !(_guard.latestResult?.virtualCamera.detected ?? false),
                    trueLabel: 'Clean',
                    falseLabel: 'DETECTED',
                  ),
                  _statusRow(
                    'Replay Attack',
                    !(_guard.latestResult?.replayAttack.detected ?? false),
                    trueLabel: 'Clean',
                    falseLabel: 'DETECTED',
                  ),
                  _statusRow(
                    'Hooking Framework',
                    !(_guard.latestResult?.hookingFramework.detected ?? false),
                    trueLabel: 'Clean',
                    falseLabel: 'DETECTED',
                  ),
                ],
              ),
            ),
          ),
        ],

        // ── Check log ──
        if (_logs.isNotEmpty) ...[
          const SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text('Check Log (${_logs.length})',
                  style: theme.textTheme.titleMedium),
              if (_logs.length > 1)
                Text(
                  '${_logs.where((l) => l.isThreat).length} threats',
                  style: theme.textTheme.labelSmall?.copyWith(
                    color: _logs.any((l) => l.isThreat)
                        ? Colors.red
                        : Colors.green,
                  ),
                ),
            ],
          ),
          const SizedBox(height: 8),
          ..._logs.reversed.map((log) => _buildLogCard(log, theme)),
        ],
      ],
    );
  }

  Widget _statusRow(
    String label,
    bool isGood, {
    String trueLabel = 'Yes',
    String falseLabel = 'No',
  }) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(
            isGood ? trueLabel : falseLabel,
            style: TextStyle(
              fontWeight: FontWeight.w600,
              color: isGood ? Colors.green : Colors.red,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildLogCard(_CheckLog log, ThemeData theme) {
    final r = log.result;
    return Card(
      color: log.isThreat
          ? Colors.red.withOpacity(0.06)
          : Colors.green.withOpacity(0.04),
      child: ExpansionTile(
        leading: Icon(
          log.isThreat ? Icons.warning_amber_rounded : Icons.check_circle,
          color: log.isThreat ? Colors.red : Colors.green,
          size: 20,
        ),
        title: Text(
          log.isInitial ? 'Initial full check' : 'Periodic quick check',
          style: theme.textTheme.bodyMedium,
        ),
        subtitle: Text(
          '${_formatTime(log.time)}  '
          'score: ${r.overallRiskScore.toStringAsFixed(2)}  '
          '${log.isThreat ? "THREAT" : "clean"}',
          style: theme.textTheme.bodySmall,
        ),
        children: [
          if (log.isInitial) ...[
            _detectorTile('Root/Jailbreak', r.rootJailbreak),
            _detectorTile('Emulator', r.emulator),
          ],
          _detectorTile('Hook', r.hookingFramework),
          _detectorTile('Virtual Camera', r.virtualCamera),
          _detectorTile('Replay Attack', r.replayAttack),
          if (r.hookingFramework.reasons.isNotEmpty ||
              r.virtualCamera.reasons.isNotEmpty ||
              r.replayAttack.reasons.isNotEmpty) ...[
            const Divider(height: 1),
            Padding(
              padding: const EdgeInsets.all(12),
              child: Text(
                [
                  ...r.hookingFramework.reasons,
                  ...r.virtualCamera.reasons,
                  ...r.replayAttack.reasons,
                ].join('\n'),
                style: theme.textTheme.bodySmall
                    ?.copyWith(color: Colors.red.shade300),
              ),
            ),
          ],
        ],
      ),
    );
  }

  Widget _detectorTile(String name, DetectionDetail d) {
    return ListTile(
      dense: true,
      visualDensity: VisualDensity.compact,
      leading: Icon(
        d.detected ? Icons.error : Icons.check,
        size: 16,
        color: d.detected ? Colors.red : Colors.green,
      ),
      title: Text(name),
      trailing: Text(
        d.confidence.toStringAsFixed(2),
        style: TextStyle(
          color: d.detected ? Colors.red : Colors.green,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }

  String _formatTime(DateTime t) =>
      '${t.hour.toString().padLeft(2, '0')}:'
      '${t.minute.toString().padLeft(2, '0')}:'
      '${t.second.toString().padLeft(2, '0')}';
}

class _CheckLog {
  final DateTime time;
  final SecurityResult result;
  final bool isThreat;
  final bool isInitial;

  const _CheckLog({
    required this.time,
    required this.result,
    required this.isThreat,
    this.isInitial = false,
  });
}

// ─────────────────── Shared widgets ──────────────────

class _ResultListView extends StatelessWidget {
  final SecurityResult result;
  final VoidCallback onRefresh;

  const _ResultListView({required this.result, required this.onRefresh});

  @override
  Widget build(BuildContext context) {
    final r = result;
    final color = switch (r.riskLevel) {
      RiskLevel.safe => Colors.green,
      RiskLevel.low => Colors.lightGreen,
      RiskLevel.medium => Colors.orange,
      RiskLevel.high => Colors.deepOrange,
      RiskLevel.critical => Colors.red,
    };

    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Card(
          color: color.withOpacity(0.1),
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Column(children: [
              _row('Risk level', r.riskLevel.name,
                  bold: true, valueColor: color),
              _row('Overall score', r.overallRiskScore.toStringAsFixed(2),
                  bold: true),
              _row('Safe for liveness', r.isSafeForLiveness ? 'Yes' : 'No',
                  valueColor:
                      r.isSafeForLiveness ? Colors.green : Colors.red),
              _row('Safe for streaming',
                  r.isSafeForLiveStreaming ? 'Yes' : 'No',
                  valueColor:
                      r.isSafeForLiveStreaming ? Colors.green : Colors.red),
              _row('Threat detected', r.isThreatDetected ? 'Yes' : 'No',
                  valueColor:
                      r.isThreatDetected ? Colors.red : Colors.green),
            ]),
          ),
        ),
        const SizedBox(height: 12),
        _detailCard('Root / Jailbreak', r.rootJailbreak),
        _detailCard('Emulator', r.emulator),
        _detailCard('Hooking Framework', r.hookingFramework),
        _detailCard('Virtual Camera', r.virtualCamera),
        _detailCard('Replay Attack', r.replayAttack),
        const SizedBox(height: 12),
        Center(
          child: TextButton.icon(
            onPressed: onRefresh,
            icon: const Icon(Icons.refresh),
            label: const Text('Re-run'),
          ),
        ),
      ],
    );
  }

  Widget _row(String label, String value,
      {bool bold = false, Color? valueColor}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Text(value,
              style: TextStyle(
                fontWeight: bold ? FontWeight.bold : FontWeight.normal,
                color: valueColor,
              )),
        ],
      ),
    );
  }

  Widget _detailCard(String title, DetectionDetail d) {
    return Card(
      child: ExpansionTile(
        leading: Icon(
          d.detected ? Icons.warning_amber_rounded : Icons.check_circle,
          color: d.detected ? Colors.red : Colors.green,
        ),
        title: Text(title),
        subtitle: Text(
            'Confidence: ${d.confidence.toStringAsFixed(2)} — '
            '${d.detected ? "DETECTED" : "Clean"}'),
        children: d.reasons.isEmpty
            ? [const ListTile(title: Text('No issues found'))]
            : d.reasons.map((r) => ListTile(title: Text(r))).toList(),
      ),
    );
  }
}

class _InfoTile extends StatelessWidget {
  final IconData icon;
  final Color color;
  final String title;
  final String subtitle;

  const _InfoTile({
    required this.icon,
    required this.color,
    required this.title,
    required this.subtitle,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      dense: true,
      leading: Icon(icon, color: color, size: 20),
      title: Text(title),
      subtitle: Text(subtitle),
    );
  }
}
1
likes
150
points
130
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for device security detection: root/jailbreak, emulator, hooking framework, and virtual camera detection with confidence scoring.

Repository (GitHub)
View/report issues

Topics

#security #root-detection #jailbreak #emulator-detection #liveness

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on device_security

Packages that implement device_security