Update: 2026-05-08 06:19:56
This commit is contained in:
237
musadaq-app/lib/core/services/device_security_service.dart
Normal file
237
musadaq-app/lib/core/services/device_security_service.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:freerasp/freerasp.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
/// Device Security Service
|
||||
/// Enterprise-grade jailbreak/root/tamper detection for Musadaq using freeRASP.
|
||||
///
|
||||
/// Detects: root, jailbreak, debugger, emulator, hooking frameworks,
|
||||
/// tampered apps, unofficial stores, missing passcode.
|
||||
///
|
||||
/// Policy: On detection → block app with security warning (no data access).
|
||||
class DeviceSecurityService extends GetxService {
|
||||
final isCompromised = false.obs;
|
||||
final threatDetails = <String>[].obs;
|
||||
final securityChecked = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initSecurity();
|
||||
}
|
||||
|
||||
/// Initialize freeRASP
|
||||
Future<void> _initSecurity() async {
|
||||
try {
|
||||
await _initFreeRASP();
|
||||
} catch (e) {
|
||||
debugPrint('[Security] freeRASP init failed: $e');
|
||||
}
|
||||
securityChecked.value = true;
|
||||
}
|
||||
|
||||
/// Initialize freeRASP with full threat callbacks
|
||||
Future<void> _initFreeRASP() async {
|
||||
final callback = ThreatCallback(
|
||||
onAppIntegrity: () => _onThreatDetected('تلاعب بالتطبيق'),
|
||||
onObfuscationIssues: () => _onThreatDetected('عدم وجود تشفير الكود'),
|
||||
onDebug: () => _onThreatDetected('وضع التصحيح مفعّل'),
|
||||
onDeviceBinding: () => _onThreatDetected('تغيير جهاز غير مصرح'),
|
||||
onDeviceID: () => _onThreatDetected('معرّف جهاز مزوّر'),
|
||||
onHooks: () => _onThreatDetected('أدوات اختراق (Hooking)'),
|
||||
onPasscode: () => _onThreatDetected('الجهاز بدون قفل شاشة'),
|
||||
onPrivilegedAccess: () => _onThreatDetected('جهاز مروّت (Root/Jailbreak)'),
|
||||
onSecureHardwareNotAvailable: () =>
|
||||
_onThreatDetected('عدم توفر عتاد آمن'),
|
||||
onSimulator: () => _onThreatDetected('محاكي (Emulator)'),
|
||||
onUnofficialStore: () => _onThreatDetected('تثبيت من متجر غير رسمي'),
|
||||
);
|
||||
|
||||
// Configure talsec
|
||||
final config = TalsecConfig(
|
||||
androidConfig: AndroidConfig(
|
||||
packageName: 'com.musadaq.app',
|
||||
signingCertHashes: ['4F:10:B4:E9:29:E0:5E:0A:3E:AE:B0:31:4C:C1:3A:DB:CB:E6:FF:DF:6A:F5:85:FC:68:FE:C1:E4:B6:29:6B:6F'],
|
||||
supportedStores: ['com.android.vending'], // Google Play only
|
||||
),
|
||||
iosConfig: IOSConfig(
|
||||
bundleIds: ['com.musadaq.app'],
|
||||
teamId: 'REPLACE_WITH_YOUR_TEAM_ID',
|
||||
),
|
||||
watcherMail: 'security@musadaq.jo',
|
||||
isProd: true,
|
||||
);
|
||||
|
||||
await Talsec.instance.start(config);
|
||||
Talsec.instance.attachListener(callback);
|
||||
debugPrint('[Security] freeRASP initialized');
|
||||
}
|
||||
|
||||
/// Handle threat detection
|
||||
void _onThreatDetected(String threat) {
|
||||
debugPrint('[Security] ⚠️ THREAT: $threat');
|
||||
threatDetails.add(threat);
|
||||
isCompromised.value = true;
|
||||
|
||||
// Block the app immediately
|
||||
_showSecurityBlock();
|
||||
}
|
||||
|
||||
/// Show a non-dismissible security warning that blocks the app
|
||||
void _showSecurityBlock() {
|
||||
// Only show once
|
||||
if (Get.isDialogOpen == true) return;
|
||||
|
||||
Get.dialog(
|
||||
PopScope(
|
||||
canPop: false,
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFF1A1A2E),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Shield icon
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.red.withValues(alpha: 0.15),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shield_rounded,
|
||||
size: 56,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Title
|
||||
const Text(
|
||||
'تحذير أمني',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'El Messiri',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Message
|
||||
const Text(
|
||||
'تم اكتشاف أن هذا الجهاز غير آمن.\n'
|
||||
'لحماية بياناتك المالية والضريبية،\n'
|
||||
'لا يمكن تشغيل مُصادَق على هذا الجهاز.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 15,
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Threat details
|
||||
Obx(() => Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.red.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'التهديدات المكتشفة:',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...threatDetails.map((t) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning_amber,
|
||||
color: Colors.red, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(t,
|
||||
style: const TextStyle(
|
||||
color: Colors.white60,
|
||||
fontSize: 13)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Close button (exits the app)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => SystemNavigator.pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
icon: const Icon(Icons.exit_to_app,
|
||||
color: Colors.white),
|
||||
label: const Text(
|
||||
'إغلاق التطبيق',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Help text
|
||||
const Text(
|
||||
'إذا كنت تعتقد أن هذا خطأ، تواصل مع\nsupport@musadaq.jo',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white38, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Quick check if device is safe (for use in API calls)
|
||||
bool get isSafe => !isCompromised.value;
|
||||
|
||||
/// Get security status for display
|
||||
Map<String, dynamic> getSecurityStatus() {
|
||||
return {
|
||||
'is_safe': isSafe,
|
||||
'checked': securityChecked.value,
|
||||
'threats': threatDetails.toList(),
|
||||
'threat_count': threatDetails.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
147
musadaq-app/lib/core/services/shorebird_update_service.dart
Normal file
147
musadaq-app/lib/core/services/shorebird_update_service.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shorebird_code_push/shorebird_code_push.dart';
|
||||
|
||||
/// Shorebird Code Push Service
|
||||
/// Handles OTA (Over-The-Air) updates for Dart code.
|
||||
/// Checks for patches on app launch and notifies users when updates are available.
|
||||
class ShorebirdUpdateService extends GetxService {
|
||||
final _updater = ShorebirdUpdater();
|
||||
|
||||
final isUpdateAvailable = false.obs;
|
||||
final isDownloading = false.obs;
|
||||
final currentPatchNumber = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_checkForUpdate();
|
||||
}
|
||||
|
||||
/// Check if Shorebird is available (only works with shorebird release builds)
|
||||
bool get isShorebirdAvailable => _updater.isAvailable;
|
||||
|
||||
/// Get current patch number
|
||||
Future<void> loadCurrentPatch() async {
|
||||
if (!isShorebirdAvailable) return;
|
||||
try {
|
||||
final patch = await _updater.readCurrentPatch();
|
||||
currentPatchNumber.value = patch?.number ?? 0;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Check for updates on launch
|
||||
Future<void> _checkForUpdate() async {
|
||||
if (!isShorebirdAvailable) {
|
||||
debugPrint('[Shorebird] Not available (debug build)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await loadCurrentPatch();
|
||||
debugPrint('[Shorebird] Current patch: ${currentPatchNumber.value}');
|
||||
|
||||
final status = await _updater.checkForUpdate();
|
||||
if (status == UpdateStatus.outdated) {
|
||||
isUpdateAvailable.value = true;
|
||||
debugPrint('[Shorebird] Update available!');
|
||||
_showUpdateDialog();
|
||||
} else {
|
||||
debugPrint('[Shorebird] App is up to date');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[Shorebird] Error checking for update: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Download and apply the update
|
||||
Future<void> downloadUpdate() async {
|
||||
if (!isShorebirdAvailable) return;
|
||||
|
||||
isDownloading.value = true;
|
||||
try {
|
||||
await _updater.update();
|
||||
isDownloading.value = false;
|
||||
isUpdateAvailable.value = false;
|
||||
|
||||
// Show restart prompt
|
||||
Get.defaultDialog(
|
||||
title: '✅ تم التحديث',
|
||||
middleText: 'تم تحميل التحديث بنجاح.\nأعد تشغيل التطبيق لتطبيق التغييرات.',
|
||||
textConfirm: 'حسناً',
|
||||
confirmTextColor: Colors.white,
|
||||
buttonColor: const Color(0xFF0F4C81),
|
||||
onConfirm: () => Get.back(),
|
||||
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
radius: 14,
|
||||
);
|
||||
} catch (e) {
|
||||
isDownloading.value = false;
|
||||
debugPrint('[Shorebird] Download failed: $e');
|
||||
Get.snackbar(
|
||||
'خطأ',
|
||||
'فشل تحميل التحديث',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show update available dialog
|
||||
void _showUpdateDialog() {
|
||||
Get.defaultDialog(
|
||||
title: '🔄 تحديث متاح',
|
||||
middleText: 'يتوفر تحديث جديد لمُصادَق.\nهل تريد تحميله الآن؟',
|
||||
textConfirm: 'تحديث',
|
||||
textCancel: 'لاحقاً',
|
||||
confirmTextColor: Colors.white,
|
||||
buttonColor: const Color(0xFF0F4C81),
|
||||
onConfirm: () {
|
||||
Get.back();
|
||||
downloadUpdate();
|
||||
},
|
||||
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
radius: 14,
|
||||
);
|
||||
}
|
||||
|
||||
/// Manual check (can be called from settings)
|
||||
Future<void> manualCheckForUpdate() async {
|
||||
if (!isShorebirdAvailable) {
|
||||
Get.snackbar(
|
||||
'تنبيه',
|
||||
'التحديث التلقائي متاح فقط في نسخة الإصدار',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Get.snackbar(
|
||||
'جارٍ الفحص...',
|
||||
'يتم البحث عن تحديثات',
|
||||
backgroundColor: const Color(0xFF0F4C81),
|
||||
colorText: Colors.white,
|
||||
showProgressIndicator: true,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
try {
|
||||
final status = await _updater.checkForUpdate();
|
||||
if (status == UpdateStatus.outdated) {
|
||||
isUpdateAvailable.value = true;
|
||||
_showUpdateDialog();
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'✅ محدّث',
|
||||
'التطبيق يعمل بأحدث إصدار (Patch ${currentPatchNumber.value})',
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('خطأ', 'فشل البحث عن تحديثات',
|
||||
backgroundColor: Colors.red, colorText: Colors.white);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user