device_security 1.0.1
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),
);
}
}