diff --git a/.gitignore b/.gitignore index 96af125..a9c5388 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,5 @@ package-lock.json yarn.lock SETUP_GITHUB.md FINAL_REPORT.md +FINAL_IMPLEMENTATION_SUMMARY.md +CORRECTIONS_SUMMARY.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 44654a1..800cdf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2024-12-27 + +### Added +- **GPS Fake Detector** (`EngineGpsFakeDetector`) - Detecta aplicativos de GPS falso e manipulação de localização + - Verifica configurações de mock location no Android + - Detecta mais de 25 apps conhecidos de GPS fake instalados + - Analisa confiabilidade da fonte de GPS (precisão suspeita, valores impossíveis) + - Verifica consistência do GPS ao longo do tempo (detecta "teletransporte") + - Monitora permissões de localização e serviços GPS + - Nível de severidade: 7 (alto) + - Confiança de detecção: 90% + - Suporte para Android e iOS + +### Dependencies +- Adicionado `geolocator: ^13.0.1` para análise de localização GPS +- Adicionado `location: ^7.0.0` para serviços de localização +- Adicionado `permission_handler: ^11.3.1` para verificação de permissões + +### Enhanced +- Expandido enum `SecurityThreatType` com novo tipo `gpsFake` +- Implementado código nativo Android para detecção de mock location +- Adicionados métodos estáticos para verificações específicas: + - `EngineGpsFakeDetector.checkMockLocationEnabled()` + - `EngineGpsFakeDetector.getInstalledFakeGpsApps()` + +### Examples +- Novo exemplo de app demonstrando detecção de GPS Fake +- Interface completa mostrando resultados detalhados da detecção +- Documentação expandida no README com exemplos de uso + +### Tests +- 100% de cobertura de testes mantida +- Novos testes unitários para o detector de GPS Fake +- Testes de integração para verificar funcionamento completo + ## [1.0.0] - 2024-12-19 ### Added diff --git a/README.md b/README.md index 2c3ddf5..d5450d4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ - 🎯 **Pontuação Pana 100/100** - Qualidade máxima no pub.dev - 🔄 **CI/CD Automatizado** - Pipeline completo com GitHub Actions - 📱 **Android & iOS Exclusivo** - Otimizado para dispositivos móveis -- 🛡️ **4 Detectores Especializados** - Frida, Root/Jailbreak, Emulator, Debugger +- 🛡️ **5 Detectores Especializados** - Frida, Root/Jailbreak, Emulator, Debugger, GPS Fake - ⚡ **Detecção Assíncrona** - Performance otimizada - 🎨 **API Intuitiva** - Fácil integração e uso - 📊 **Sistema de Confiança** - Níveis de confiança calibrados @@ -83,6 +83,7 @@ Future performFullSecurityCheck() async { EngineRootDetector(), EngineEmulatorDetector(), EngineDebuggerDetector(), + EngineGpsFakeDetector(), ]; print('🔍 Executando verificação completa de segurança...\n'); @@ -127,6 +128,12 @@ Future performFullSecurityCheck() async { - **Métodos**: Processos de debug, timing attacks - **Plataformas**: Android, iOS +### 5. 🗺️ GPS Fake Detector (`EngineGpsFakeDetector`) +- **Ameaça**: `SecurityThreatType.gpsFake` +- **Confiança**: 90% +- **Métodos**: Mock location, apps falsos, consistência GPS, análise de localização +- **Plataformas**: Android, iOS + ## 📊 Modelos de Dados ### SecurityCheckModel @@ -166,6 +173,7 @@ enum SecurityThreatType { emulator, // Severidade: 6 rootJailbreak,// Severidade: 8 debugger, // Severidade: 2 + gpsFake, // Severidade: 7 } ``` @@ -195,6 +203,73 @@ flutter run ### Implementação Personalizada ```dart +### Detector de GPS Fake - Exemplo Avançado + +```dart +import 'package:engine_security/engine_security.dart'; + +void main() async { + final gpsDetector = EngineGpsFakeDetector(); + + // Verificação básica + final result = await gpsDetector.performCheck(); + + if (!result.isSecure) { + print('⚠️ GPS Fake detectado!'); + print('📍 Detalhes: ${result.details}'); + print('🔍 Método: ${result.detectionMethod}'); + print('🎯 Confiança: ${(result.confidence * 100).toStringAsFixed(1)}%'); + + // Tomar ações de segurança + await handleGpsFakeDetection(result); + } else { + print('✅ GPS é confiável'); + } + + // Verificações específicas + final mockEnabled = await EngineGpsFakeDetector.checkMockLocationEnabled(); + final fakeApps = await EngineGpsFakeDetector.getInstalledFakeGpsApps(); + + print('📱 Mock Location habilitado: $mockEnabled'); + print('🚫 Apps de GPS Fake encontrados: ${fakeApps.length}'); + + for (final app in fakeApps) { + print(' - $app'); + } +} + +Future handleGpsFakeDetection(SecurityCheckModel result) async { + // Bloquear funcionalidades baseadas em localização + // Registrar tentativa de fraude + // Solicitar verificação adicional do usuário + // Etc. +} +``` + +### Técnicas de Detecção de GPS Fake + +O `EngineGpsFakeDetector` utiliza múltiplas técnicas para detectar manipulação de GPS: + +#### 1. 🔧 Verificação de Mock Location (Android) +- Detecta se as "opções de desenvolvedor" têm mock location habilitado +- Verifica configurações do sistema Android + +#### 2. 📱 Detecção de Apps de GPS Fake +- Verifica instalação de mais de 25 apps conhecidos de GPS fake +- Lista atualizada dos principais apps de spoofing de localização + +#### 3. 📊 Análise de Confiabilidade da Fonte +- Verifica precisão suspeita do GPS (< 100m pode indicar fake) +- Detecta valores impossíveis (altitude e velocidade zero) + +#### 4. 🔄 Verificação de Consistência GPS +- Analisa movimento impossível entre leituras GPS +- Detecta "teletransporte" (distância > 1km em < 10s) + +#### 5. 🔐 Análise de Permissões +- Verifica interferência em permissões de localização +- Detecta desabilitação suspeita de serviços de localização + class MySecurityManager { final List _detectors = [ EngineFridaDetector(), diff --git a/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..09b8616 --- /dev/null +++ b/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,44 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.device_info.DeviceInfoPlusPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin device_info_plus, dev.fluttercommunity.plus.device_info.DeviceInfoPlusPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.baseflow.geolocator.GeolocatorPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin geolocator_android, com.baseflow.geolocator.GeolocatorPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.lyokone.location.LocationPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin location, com.lyokone.location.LocationPlugin", e); + } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.baseflow.permissionhandler.PermissionHandlerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin permission_handler_android, com.baseflow.permissionhandler.PermissionHandlerPlugin", e); + } + } +} diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..99c52db --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,50 @@ +group 'tech.stmr.engine_security' +version '1.0.0' + +buildscript { + ext.kotlin_version = '1.9.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } + + dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + } +} \ No newline at end of file diff --git a/android/local.properties b/android/local.properties new file mode 100644 index 0000000..77493ab --- /dev/null +++ b/android/local.properties @@ -0,0 +1,2 @@ +sdk.dir=/Users/thiagomoreira/Library/Android/sdk +flutter.sdk=/Users/thiagomoreira/development/flutter \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5f55f7c --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/android/src/main/kotlin/com/stmr/engine_security/EngineSecurityPlugin.kt b/android/src/main/kotlin/com/stmr/engine_security/EngineSecurityPlugin.kt new file mode 100644 index 0000000..eae20ea --- /dev/null +++ b/android/src/main/kotlin/com/stmr/engine_security/EngineSecurityPlugin.kt @@ -0,0 +1,63 @@ +package tech.stmr.engine_security + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.provider.Settings +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +class EngineSecurityPlugin: FlutterPlugin, MethodCallHandler { + private lateinit var channel: MethodChannel + private lateinit var context: Context + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "engine_security/gps_fake") + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "checkMockLocationEnabled" -> { + result.success(isMockLocationEnabled()) + } + "getInstalledApps" -> { + result.success(getInstalledAppPackages()) + } + else -> { + result.notImplemented() + } + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + private fun isMockLocationEnabled(): Boolean { + return try { + val mockLocation = Settings.Secure.getString( + context.contentResolver, + Settings.Secure.ALLOW_MOCK_LOCATION + ) + mockLocation == "1" + } catch (e: Exception) { + false + } + } + + private fun getInstalledAppPackages(): List { + return try { + val packageManager = context.packageManager + val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA) + packages.filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 } + .map { it.packageName } + } catch (e: Exception) { + emptyList() + } + } +} \ No newline at end of file diff --git a/example/security_demo/.gitignore b/demo/security_demo/.gitignore similarity index 100% rename from example/security_demo/.gitignore rename to demo/security_demo/.gitignore diff --git a/example/security_demo/.metadata b/demo/security_demo/.metadata similarity index 100% rename from example/security_demo/.metadata rename to demo/security_demo/.metadata diff --git a/example/security_demo/README.md b/demo/security_demo/README.md similarity index 100% rename from example/security_demo/README.md rename to demo/security_demo/README.md diff --git a/example/security_demo/analysis_options.yaml b/demo/security_demo/analysis_options.yaml similarity index 100% rename from example/security_demo/analysis_options.yaml rename to demo/security_demo/analysis_options.yaml diff --git a/example/security_demo/android/.gitignore b/demo/security_demo/android/.gitignore similarity index 100% rename from example/security_demo/android/.gitignore rename to demo/security_demo/android/.gitignore diff --git a/example/security_demo/android/app/build.gradle.kts b/demo/security_demo/android/app/build.gradle.kts similarity index 92% rename from example/security_demo/android/app/build.gradle.kts rename to demo/security_demo/android/app/build.gradle.kts index 0164f9c..1a7ac8c 100644 --- a/example/security_demo/android/app/build.gradle.kts +++ b/demo/security_demo/android/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } android { - namespace = "com.stmr.security.security_demo" + namespace = "tech.stmr.security.security_demo" compileSdk = flutter.compileSdkVersion ndkVersion = "27.0.12077973" @@ -21,7 +21,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.stmr.security.security_demo" + applicationId = "tech.stmr.security.security_demo" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion diff --git a/example/security_demo/android/app/src/debug/AndroidManifest.xml b/demo/security_demo/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/security_demo/android/app/src/debug/AndroidManifest.xml rename to demo/security_demo/android/app/src/debug/AndroidManifest.xml diff --git a/example/security_demo/android/app/src/main/AndroidManifest.xml b/demo/security_demo/android/app/src/main/AndroidManifest.xml similarity index 84% rename from example/security_demo/android/app/src/main/AndroidManifest.xml rename to demo/security_demo/android/app/src/main/AndroidManifest.xml index 4fb2c20..fa546de 100644 --- a/example/security_demo/android/app/src/main/AndroidManifest.xml +++ b/demo/security_demo/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,14 @@ + + + + + + android:icon="@mipmap/ic_launcher" + android:allowBackup="false"> UIApplicationSupportsIndirectInputEvents + + + NSLocationWhenInUseUsageDescription + Este app precisa de acesso à localização para testar o detector de GPS falso e verificar a segurança do dispositivo. + NSLocationAlwaysAndWhenInUseUsageDescription + Este app precisa de acesso à localização para testar o detector de GPS falso e verificar a segurança do dispositivo. + NSLocationAlwaysUsageDescription + Este app precisa de acesso à localização para testar o detector de GPS falso e verificar a segurança do dispositivo. diff --git a/example/security_demo/ios/Runner/Runner-Bridging-Header.h b/demo/security_demo/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from example/security_demo/ios/Runner/Runner-Bridging-Header.h rename to demo/security_demo/ios/Runner/Runner-Bridging-Header.h diff --git a/example/security_demo/ios/RunnerTests/RunnerTests.swift b/demo/security_demo/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from example/security_demo/ios/RunnerTests/RunnerTests.swift rename to demo/security_demo/ios/RunnerTests/RunnerTests.swift diff --git a/example/security_demo/lib/core/theme/app_theme.dart b/demo/security_demo/lib/core/theme/app_theme.dart similarity index 50% rename from example/security_demo/lib/core/theme/app_theme.dart rename to demo/security_demo/lib/core/theme/app_theme.dart index f23ed54..9e55452 100644 --- a/example/security_demo/lib/core/theme/app_theme.dart +++ b/demo/security_demo/lib/core/theme/app_theme.dart @@ -56,11 +56,7 @@ class AppTheme { appBarTheme: const AppBarTheme( backgroundColor: AppColors.surface, elevation: 0, - titleTextStyle: TextStyle( - color: AppColors.onSurface, - fontSize: 20, - fontWeight: FontWeight.w600, - ), + titleTextStyle: TextStyle(color: AppColors.onSurface, fontSize: 20, fontWeight: FontWeight.w600), iconTheme: IconThemeData(color: AppColors.onSurface), ), elevatedButtonTheme: ElevatedButtonThemeData( @@ -68,109 +64,41 @@ class AppTheme { backgroundColor: AppColors.primary, foregroundColor: AppColors.onPrimary, elevation: 4, - shadowColor: AppColors.primary.withOpacity(0.3), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shadowColor: AppColors.primary.withValues(alpha: 0.3), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - textStyle: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: AppColors.primary, side: const BorderSide(color: AppColors.primary, width: 2), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - textStyle: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), ), textTheme: const TextTheme( - displayLarge: TextStyle( - color: AppColors.onBackground, - fontSize: 32, - fontWeight: FontWeight.bold, - ), - displayMedium: TextStyle( - color: AppColors.onBackground, - fontSize: 28, - fontWeight: FontWeight.bold, - ), - displaySmall: TextStyle( - color: AppColors.onBackground, - fontSize: 24, - fontWeight: FontWeight.w600, - ), - headlineLarge: TextStyle( - color: AppColors.onBackground, - fontSize: 22, - fontWeight: FontWeight.w600, - ), - headlineMedium: TextStyle( - color: AppColors.onBackground, - fontSize: 20, - fontWeight: FontWeight.w600, - ), - headlineSmall: TextStyle( - color: AppColors.onBackground, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - titleLarge: TextStyle( - color: AppColors.onBackground, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - titleMedium: TextStyle( - color: AppColors.onBackground, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - titleSmall: TextStyle( - color: AppColors.onSurfaceVariant, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - bodyLarge: TextStyle( - color: AppColors.onBackground, - fontSize: 16, - fontWeight: FontWeight.normal, - ), - bodyMedium: TextStyle( - color: AppColors.onSurfaceVariant, - fontSize: 14, - fontWeight: FontWeight.normal, - ), - bodySmall: TextStyle( - color: AppColors.onSurfaceVariant, - fontSize: 12, - fontWeight: FontWeight.normal, - ), - ), - iconTheme: const IconThemeData( - color: AppColors.onSurface, - size: 24, - ), - dividerTheme: const DividerThemeData( - color: AppColors.surfaceVariant, - thickness: 1, + displayLarge: TextStyle(color: AppColors.onBackground, fontSize: 32, fontWeight: FontWeight.bold), + displayMedium: TextStyle(color: AppColors.onBackground, fontSize: 28, fontWeight: FontWeight.bold), + displaySmall: TextStyle(color: AppColors.onBackground, fontSize: 24, fontWeight: FontWeight.w600), + headlineLarge: TextStyle(color: AppColors.onBackground, fontSize: 22, fontWeight: FontWeight.w600), + headlineMedium: TextStyle(color: AppColors.onBackground, fontSize: 20, fontWeight: FontWeight.w600), + headlineSmall: TextStyle(color: AppColors.onBackground, fontSize: 18, fontWeight: FontWeight.w600), + titleLarge: TextStyle(color: AppColors.onBackground, fontSize: 16, fontWeight: FontWeight.w600), + titleMedium: TextStyle(color: AppColors.onBackground, fontSize: 14, fontWeight: FontWeight.w500), + titleSmall: TextStyle(color: AppColors.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w500), + bodyLarge: TextStyle(color: AppColors.onBackground, fontSize: 16, fontWeight: FontWeight.normal), + bodyMedium: TextStyle(color: AppColors.onSurfaceVariant, fontSize: 14, fontWeight: FontWeight.normal), + bodySmall: TextStyle(color: AppColors.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.normal), ), + iconTheme: const IconThemeData(color: AppColors.onSurface, size: 24), + dividerTheme: const DividerThemeData(color: AppColors.surfaceVariant, thickness: 1), snackBarTheme: SnackBarThemeData( backgroundColor: AppColors.surface, - contentTextStyle: const TextStyle( - color: AppColors.onSurface, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + contentTextStyle: const TextStyle(color: AppColors.onSurface), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), behavior: SnackBarBehavior.floating, ), ); diff --git a/example/security_demo/lib/features/dashboard/dashboard_screen.dart b/demo/security_demo/lib/features/dashboard/dashboard_screen.dart similarity index 79% rename from example/security_demo/lib/features/dashboard/dashboard_screen.dart rename to demo/security_demo/lib/features/dashboard/dashboard_screen.dart index c60e002..c12734f 100644 --- a/example/security_demo/lib/features/dashboard/dashboard_screen.dart +++ b/demo/security_demo/lib/features/dashboard/dashboard_screen.dart @@ -27,40 +27,27 @@ class DashboardScreen extends ConsumerWidget { centerTitle: true, title: const Text( 'Security Demo', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18), ), background: Container( - decoration: const BoxDecoration( - gradient: AppColors.primaryGradient, - ), + decoration: const BoxDecoration(gradient: AppColors.primaryGradient), child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 20), - const Icon( - Icons.security, - size: 48, - color: Colors.white, - ), + const Icon(Icons.security, size: 48, color: Colors.white), const SizedBox(height: 16), Text( 'Engine Security', - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.headlineLarge?.copyWith(color: Colors.white, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( 'Teste todos os detectores', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.white70, - ), + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white70), ), const SizedBox(height: 40), ], @@ -79,20 +66,14 @@ class DashboardScreen extends ConsumerWidget { const SizedBox(height: 24), Row( children: [ - Text( - 'Detectores de Segurança', - style: Theme.of(context).textTheme.headlineSmall, - ), + Text('Detectores de Segurança', style: Theme.of(context).textTheme.headlineSmall), const Spacer(), OutlinedButton.icon( - onPressed: - summary.isRunning ? null : () => ref.read(securityTestProvider.notifier).runAllTests(), + onPressed: summary.isRunning + ? null + : () => ref.read(securityTestProvider.notifier).runAllTests(), icon: summary.isRunning - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ) + ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.play_arrow), label: Text(summary.isRunning ? 'Executando...' : 'Testar Todos'), ), @@ -142,6 +123,15 @@ class DashboardScreen extends ConsumerWidget { isRunning: testResults['Debugger']?.isRunning ?? false, onTest: () => ref.read(securityTestProvider.notifier).runDetectorTest('Debugger'), ), + const SizedBox(height: 16), + SecurityCard( + title: 'Detecção GPS Fake', + icon: Icons.location_off, + description: 'Detecta localização falsa e apps de GPS fake', + testResult: testResults['GPS Fake'], + isRunning: testResults['GPS Fake']?.isRunning ?? false, + onTest: () => ref.read(securityTestProvider.notifier).runDetectorTest('GPS Fake'), + ), const SizedBox(height: 32), ]), ), @@ -185,17 +175,14 @@ class DashboardScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Resumo de Segurança', - style: Theme.of(context).textTheme.titleLarge, - ), + Text('Resumo de Segurança', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 4), Text( summary.overallStatus, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: summary.threatsDetected > 0 ? AppColors.error : AppColors.success, - fontWeight: FontWeight.w500, - ), + color: summary.threatsDetected > 0 ? AppColors.error : AppColors.success, + fontWeight: FontWeight.w500, + ), ), ], ), @@ -252,10 +239,7 @@ class DashboardScreen extends ConsumerWidget { ), ), const SizedBox(height: 8), - Text( - '${summary.completionPercentage.toInt()}% Completo', - style: Theme.of(context).textTheme.bodySmall, - ), + Text('${summary.completionPercentage.toInt()}% Completo', style: Theme.of(context).textTheme.bodySmall), ], ], ], @@ -264,19 +248,13 @@ class DashboardScreen extends ConsumerWidget { ); } - Widget _buildSummaryItem( - BuildContext context, - String label, - String value, - IconData icon, - Color color, - ) { + Widget _buildSummaryItem(BuildContext context, String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), - border: Border.all(color: color.withOpacity(0.2)), + border: Border.all(color: color.withValues(alpha: 0.2)), ), child: Column( children: [ @@ -284,16 +262,9 @@ class DashboardScreen extends ConsumerWidget { const SizedBox(height: 8), Text( value, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: color, - fontWeight: FontWeight.bold, - ), - ), - Text( - label, - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color, fontWeight: FontWeight.bold), ), + Text(label, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center), ], ), ); diff --git a/example/security_demo/lib/main.dart b/demo/security_demo/lib/main.dart similarity index 100% rename from example/security_demo/lib/main.dart rename to demo/security_demo/lib/main.dart diff --git a/example/security_demo/lib/shared/models/detector_test_result.dart b/demo/security_demo/lib/shared/models/detector_test_result.dart similarity index 100% rename from example/security_demo/lib/shared/models/detector_test_result.dart rename to demo/security_demo/lib/shared/models/detector_test_result.dart diff --git a/example/security_demo/lib/shared/providers/security_test_provider.dart b/demo/security_demo/lib/shared/providers/security_test_provider.dart similarity index 66% rename from example/security_demo/lib/shared/providers/security_test_provider.dart rename to demo/security_demo/lib/shared/providers/security_test_provider.dart index 919ffeb..eb54d9e 100644 --- a/example/security_demo/lib/shared/providers/security_test_provider.dart +++ b/demo/security_demo/lib/shared/providers/security_test_provider.dart @@ -1,5 +1,6 @@ import 'package:engine_security/engine_security.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; import '../models/detector_test_result.dart'; @@ -21,6 +22,32 @@ class SecurityTestNotifier extends StateNotifier Apps > Security Demo > Permissões e habilite Localização.', + confidence: 0.0, + ), + timestamp: DateTime.now(), + executionTime: stopwatch.elapsed, + isRunning: false, + ), + }; + return; + } + + // Aguardar um pouco para as permissões serem processadas + await Future.delayed(const Duration(milliseconds: 500)); + } + late ISecurityDetector detector; switch (detectorName) { @@ -36,6 +63,9 @@ class SecurityTestNotifier extends StateNotifier runAllTests() async { - final detectors = ['Frida', 'Root/Jailbreak', 'Emulator', 'Debugger']; + final detectors = ['Frida', 'Root/Jailbreak', 'Emulator', 'Debugger', 'GPS Fake']; for (final detector in detectors) { await runDetectorTest(detector); @@ -92,6 +119,34 @@ class SecurityTestNotifier extends StateNotifier _requestLocationPermissions() async { + try { + // Verificar se as permissões já foram concedidas + final locationStatus = await Permission.location.status; + + if (locationStatus == PermissionStatus.granted) { + return true; + } + + // Solicitar permissão de localização + final result = await Permission.location.request(); + + if (result == PermissionStatus.granted) { + return true; + } + + // Se a permissão foi negada permanentemente, informar o usuário + if (result == PermissionStatus.permanentlyDenied) { + return false; + } + + return false; + } catch (e) { + return false; + } + } } final securityTestProvider = StateNotifierProvider>((ref) { diff --git a/example/security_demo/lib/shared/widgets/security_card.dart b/demo/security_demo/lib/shared/widgets/security_card.dart similarity index 66% rename from example/security_demo/lib/shared/widgets/security_card.dart rename to demo/security_demo/lib/shared/widgets/security_card.dart index 3bb9169..081e721 100644 --- a/example/security_demo/lib/shared/widgets/security_card.dart +++ b/demo/security_demo/lib/shared/widgets/security_card.dart @@ -34,30 +34,17 @@ class SecurityCard extends StatelessWidget { children: [ Container( padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - gradient: _getIconGradient(), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - icon, - color: Colors.white, - size: 24, - ), + decoration: BoxDecoration(gradient: _getIconGradient(), borderRadius: BorderRadius.circular(12)), + child: Icon(icon, color: Colors.white, size: 24), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), + Text(title, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 4), - Text( - description, - style: Theme.of(context).textTheme.bodyMedium, - ), + Text(description, style: Theme.of(context).textTheme.bodyMedium), ], ), ), @@ -65,10 +52,7 @@ class SecurityCard extends StatelessWidget { ], ), const SizedBox(height: 20), - if (testResult != null) ...[ - _buildTestResults(context), - const SizedBox(height: 16), - ], + if (testResult != null) ...[_buildTestResults(context), const SizedBox(height: 16)], SizedBox( width: double.infinity, child: ElevatedButton( @@ -103,19 +87,12 @@ class SecurityCard extends StatelessWidget { return const SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(AppColors.primary), - ), + child: CircularProgressIndicator(strokeWidth: 2, valueColor: AlwaysStoppedAnimation(AppColors.primary)), ); } if (testResult == null) { - return const Icon( - Icons.help_outline, - color: AppColors.onSurfaceVariant, - size: 20, - ); + return const Icon(Icons.help_outline, color: AppColors.onSurfaceVariant, size: 20); } return Icon( @@ -131,10 +108,12 @@ class SecurityCard extends StatelessWidget { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.surfaceVariant.withOpacity(0.3), + color: AppColors.surfaceVariant.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(12), border: Border.all( - color: testResult!.result.isSecure ? AppColors.success.withOpacity(0.3) : AppColors.error.withOpacity(0.3), + color: testResult!.result.isSecure + ? AppColors.success.withValues(alpha: 0.3) + : AppColors.error.withValues(alpha: 0.3), width: 1, ), ), @@ -152,18 +131,14 @@ class SecurityCard extends StatelessWidget { Text( testResult!.statusDescription, style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: testResult!.result.isSecure ? AppColors.success : AppColors.error, - fontWeight: FontWeight.w600, - ), + color: testResult!.result.isSecure ? AppColors.success : AppColors.error, + fontWeight: FontWeight.w600, + ), ), ], ), const SizedBox(height: 12), - _buildResultRow( - context, - 'Detalhes:', - testResult!.result.details ?? 'Nenhum detalhe disponível', - ), + _buildResultRow(context, 'Detalhes:', testResult!.result.details ?? 'Nenhum detalhe disponível'), const SizedBox(height: 8), _buildResultRow( context, @@ -171,24 +146,12 @@ class SecurityCard extends StatelessWidget { '${testResult!.confidencePercentage} (${testResult!.confidenceDescription})', ), const SizedBox(height: 8), - _buildResultRow( - context, - 'Método:', - testResult!.result.detectionMethod ?? 'Não especificado', - ), + _buildResultRow(context, 'Método:', testResult!.result.detectionMethod ?? 'Não especificado'), const SizedBox(height: 8), - _buildResultRow( - context, - 'Tempo:', - '${testResult!.executionTime.inMilliseconds}ms', - ), + _buildResultRow(context, 'Tempo:', '${testResult!.executionTime.inMilliseconds}ms'), if (testResult!.result.threatType != SecurityThreatType.unknown) ...[ const SizedBox(height: 8), - _buildResultRow( - context, - 'Severidade:', - '${testResult!.result.threatType.severityLevel}/10', - ), + _buildResultRow(context, 'Severidade:', '${testResult!.result.threatType.severityLevel}/10'), ], ], ), @@ -201,19 +164,9 @@ class SecurityCard extends StatelessWidget { children: [ SizedBox( width: 80, - child: Text( - label, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - ), - Expanded( - child: Text( - value, - style: Theme.of(context).textTheme.bodySmall, - ), + child: Text(label, style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500)), ), + Expanded(child: Text(value, style: Theme.of(context).textTheme.bodySmall)), ], ); } diff --git a/example/security_demo/pubspec.yaml b/demo/security_demo/pubspec.yaml similarity index 94% rename from example/security_demo/pubspec.yaml rename to demo/security_demo/pubspec.yaml index 72ee9a4..4c65109 100644 --- a/example/security_demo/pubspec.yaml +++ b/demo/security_demo/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: flutter_riverpod: ^2.5.1 go_router: ^14.2.7 + permission_handler: ^11.3.1 dev_dependencies: flutter_test: diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..fe78505 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; +import 'package:engine_security/engine_security.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Engine Security GPS Fake Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(title: 'GPS Fake Detection Demo'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final EngineGpsFakeDetector _gpsDetector = EngineGpsFakeDetector(); + SecurityCheckModel? _lastResult; + bool _isChecking = false; + + Future _checkGpsFake() async { + setState(() { + _isChecking = true; + }); + + try { + final result = await _gpsDetector.performCheck(); + setState(() { + _lastResult = result; + _isChecking = false; + }); + } catch (e) { + setState(() { + _lastResult = SecurityCheckModel.threat( + threatType: SecurityThreatType.gpsFake, + details: 'Erro ao verificar GPS Fake: $e', + detectionMethod: 'Error Handler', + ); + _isChecking = false; + }); + } + } + + Widget _buildResultCard() { + if (_lastResult == null) { + return const Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('Nenhuma verificação realizada ainda'), + ), + ); + } + + final result = _lastResult!; + final isSecure = result.isSecure; + + return Card( + color: isSecure ? Colors.green[50] : Colors.red[50], + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + isSecure ? Icons.security : Icons.warning, + color: isSecure ? Colors.green : Colors.red, + ), + const SizedBox(width: 8), + Text( + isSecure ? 'GPS Seguro' : 'GPS Fake Detectado!', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: isSecure ? Colors.green[700] : Colors.red[700], + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 12), + if (result.details != null) ...[ + Text( + 'Detalhes:', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 4), + Text( + result.details!, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 8), + ], + if (result.detectionMethod != null) ...[ + Text( + 'Método de Detecção:', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 4), + Text( + result.detectionMethod!, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 8), + ], + Row( + children: [ + Text( + 'Confiança: ', + style: Theme.of(context).textTheme.titleSmall, + ), + Text( + '${(result.confidence * 100).toStringAsFixed(1)}%', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + if (result.timestamp != null) ...[ + const SizedBox(height: 4), + Text( + 'Verificado em: ${result.timestamp!.toLocal().toString().split('.')[0]}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Detector de GPS Fake', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Este detector verifica se o usuário está usando aplicativos de GPS falso ou manipulando a localização do dispositivo.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 16), + Row( + children: [ + Icon( + _gpsDetector.isAvailable ? Icons.check_circle : Icons.error, + color: _gpsDetector.isAvailable ? Colors.green : Colors.red, + ), + const SizedBox(width: 8), + Text( + _gpsDetector.isAvailable + ? 'Detector disponível nesta plataforma' + : 'Detector não disponível nesta plataforma', + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _isChecking || !_gpsDetector.isAvailable ? null : _checkGpsFake, + icon: _isChecking + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.gps_fixed), + label: Text(_isChecking ? 'Verificando...' : 'Verificar GPS Fake'), + ), + const SizedBox(height: 24), + Text( + 'Resultado da Verificação', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + _buildResultCard(), + ], + ), + ), + ); + } +} diff --git a/ios/Classes/EngineSecurityPlugin.h b/ios/Classes/EngineSecurityPlugin.h new file mode 100644 index 0000000..6f91698 --- /dev/null +++ b/ios/Classes/EngineSecurityPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface EngineSecurityPlugin : NSObject +@end \ No newline at end of file diff --git a/ios/Classes/EngineSecurityPlugin.m b/ios/Classes/EngineSecurityPlugin.m new file mode 100644 index 0000000..4cd69aa --- /dev/null +++ b/ios/Classes/EngineSecurityPlugin.m @@ -0,0 +1,15 @@ +#import "EngineSecurityPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "engine_security-Swift.h" +#endif + +@implementation EngineSecurityPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [EngineSecurityPlugin registerWithRegistrar:registrar]; +} +@end \ No newline at end of file diff --git a/ios/Classes/EngineSecurityPlugin.swift b/ios/Classes/EngineSecurityPlugin.swift new file mode 100644 index 0000000..d3e69b0 --- /dev/null +++ b/ios/Classes/EngineSecurityPlugin.swift @@ -0,0 +1,172 @@ +import Flutter +import UIKit +import CoreLocation +import Foundation + +public class EngineSecurityPlugin: NSObject, FlutterPlugin { + private let locationManager = CLLocationManager() + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "engine_security/gps_fake", binaryMessenger: registrar.messenger()) + let instance = EngineSecurityPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "checkMockLocationEnabled": + result(checkMockLocationEnabled()) + case "getInstalledApps": + result(getInstalledFakeGpsApps()) + case "checkJailbreakStatus": + result(checkJailbreakStatus()) + case "checkLocationServicesReliability": + checkLocationServicesReliability(result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + private func checkMockLocationEnabled() -> Bool { + // No iOS, verificamos se o device está jailbroken + // Devices jailbroken podem ter location spoofing + return checkJailbreakStatus() + } + + private func getInstalledFakeGpsApps() -> [String] { + let fakeLocationApps = [ + "com.iospirit.gpx", + "com.lexa.fakegps", + "LocationFaker", + "iSpoofer", + "PokeGO++", + "GPS JoyStick", + "Fake GPS Pro", + "Location Spoofer" + ] + + var detectedApps: [String] = [] + + for app in fakeLocationApps { + if canOpenApp(scheme: app) { + detectedApps.append(app) + } + } + + return detectedApps + } + + private func canOpenApp(scheme: String) -> Bool { + guard let url = URL(string: "\(scheme)://") else { return false } + return UIApplication.shared.canOpenURL(url) + } + + private func checkJailbreakStatus() -> Bool { + // Verificar arquivos comuns de jailbreak + let jailbreakPaths = [ + "/Applications/Cydia.app", + "/Library/MobileSubstrate/MobileSubstrate.dylib", + "/bin/bash", + "/usr/sbin/sshd", + "/etc/apt", + "/private/var/lib/apt/", + "/private/var/lib/cydia", + "/private/var/mobile/Library/SBSettings/Themes", + "/System/Library/LaunchDaemons/com.ikey.bbot.plist", + "/private/var/tmp/cydia.log", + "/Applications/Icy.app", + "/Applications/MxTube.app", + "/Applications/RockApp.app", + "/Applications/blackra1n.app", + "/Applications/SBSettings.app", + "/Applications/FakeCarrier.app", + "/Applications/WinterBoard.app", + "/Applications/IntelliScreen.app" + ] + + for path in jailbreakPaths { + if FileManager.default.fileExists(atPath: path) { + return true + } + } + + // Tentar escrever em diretório restrito + do { + let testString = "test" + let testPath = "/private/test_jailbreak" + try testString.write(toFile: testPath, atomically: true, encoding: .utf8) + try FileManager.default.removeItem(atPath: testPath) + return true // Se conseguiu escrever, device está jailbroken + } catch { + // Não conseguiu escrever - comportamento normal + } + + // Verificar se consegue executar comandos do sistema + if system("ls") == 0 { + return true + } + + return false + } + + private func checkLocationServicesReliability(result: @escaping FlutterResult) { + guard CLLocationManager.locationServicesEnabled() else { + result([ + "accuracy": -1, + "isReliable": false, + "reason": "Location services disabled" + ]) + return + } + + locationManager.requestWhenInUseAuthorization() + + // Verificar se o Core Location está funcionando adequadamente + let authStatus = CLLocationManager.authorizationStatus() + + var reliability: [String: Any] = [:] + reliability["authorizationStatus"] = authStatusToString(authStatus) + reliability["locationServicesEnabled"] = CLLocationManager.locationServicesEnabled() + reliability["significantLocationChangeMonitoring"] = CLLocationManager.significantLocationChangeMonitoringAvailable() + reliability["headingAvailable"] = CLLocationManager.headingAvailable() + reliability["regionMonitoring"] = CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) + + // Verificar se há sinais de spoofing baseado nos serviços disponíveis + var suspiciousCount = 0 + + // Em devices jailbroken, alguns serviços podem estar comprometidos + if !CLLocationManager.significantLocationChangeMonitoringAvailable() { + suspiciousCount += 1 + } + + if !CLLocationManager.headingAvailable() && !checkIfIsPad() { + suspiciousCount += 1 + } + + reliability["suspiciousCount"] = suspiciousCount + reliability["isReliable"] = suspiciousCount == 0 && authStatus == .authorizedWhenInUse || authStatus == .authorizedAlways + + result(reliability) + } + + private func authStatusToString(_ status: CLAuthorizationStatus) -> String { + switch status { + case .notDetermined: + return "notDetermined" + case .restricted: + return "restricted" + case .denied: + return "denied" + case .authorizedAlways: + return "authorizedAlways" + case .authorizedWhenInUse: + return "authorizedWhenInUse" + @unknown default: + return "unknown" + } + } + + private func checkIfIsPad() -> Bool { + return UIDevice.current.userInterfaceIdiom == .pad + } +} \ No newline at end of file diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000..a88caf9 --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000..e3ba6fb --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/ios/Runner/GeneratedPluginRegistrant.h b/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000..7a89092 --- /dev/null +++ b/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/ios/Runner/GeneratedPluginRegistrant.m b/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..46ad79d --- /dev/null +++ b/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,49 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import device_info_plus; +#endif + +#if __has_include() +#import +#else +@import geolocator_apple; +#endif + +#if __has_include() +#import +#else +@import location; +#endif + +#if __has_include() +#import +#else +@import package_info_plus; +#endif + +#if __has_include() +#import +#else +@import permission_handler_apple; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { + [FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]]; + [GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]]; + [LocationPlugin registerWithRegistrar:[registry registrarForPlugin:@"LocationPlugin"]]; + [FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]]; + [PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]]; +} + +@end diff --git a/ios/engine_security.podspec b/ios/engine_security.podspec new file mode 100644 index 0000000..8ddbb8c --- /dev/null +++ b/ios/engine_security.podspec @@ -0,0 +1,20 @@ +Pod::Spec.new do |s| + s.name = 'engine_security' + s.version = '1.1.0' + s.summary = 'Advanced security detection system for Flutter applications' + s.description = <<-DESC +Advanced security detection system for Flutter applications focused on Android and iOS. +Detects Frida, Root/Jailbreak, Emulator, Debugger, and GPS Fake threats with high precision. + DESC + s.homepage = 'https://github.com/moreirawebmaster/engine-security' + s.license = { :file => '../LICENSE' } + s.author = { 'moreirawebmaster' => 'moreirawebmaster@gmail.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end \ No newline at end of file diff --git a/lib/src/detectors/detectors.dart b/lib/src/detectors/detectors.dart index 5e4f7db..c1d3a27 100644 --- a/lib/src/detectors/detectors.dart +++ b/lib/src/detectors/detectors.dart @@ -1,5 +1,6 @@ export 'engine_debugger_detector.dart'; export 'engine_emulator_detector.dart'; export 'engine_frida_detector.dart'; +export 'engine_gps_fake_detector.dart'; export 'engine_root_detector.dart'; export 'i_security_detector.dart'; diff --git a/lib/src/detectors/engine_debugger_detector.dart b/lib/src/detectors/engine_debugger_detector.dart index 4db75dc..27a4603 100644 --- a/lib/src/detectors/engine_debugger_detector.dart +++ b/lib/src/detectors/engine_debugger_detector.dart @@ -4,6 +4,21 @@ import 'dart:io'; import 'package:engine_security/src/src.dart'; +/// Detector for debuggers attached to the application process. +/// +/// IMPORTANT: This detector does NOT only detect "USB Debugging enabled". +/// It specifically checks if there is a debugger actively monitoring +/// or instrumenting the application process. +/// +/// Differences: +/// - USB Debugging: Allows connecting device to PC (not a threat) +/// - Attached debugger: Active process controlling/monitoring the app (threat) +/// +/// Checks performed: +/// - TracerPid: If another process is "tracking" this process +/// - Debugger processes: gdb, lldb, strace, ptrace, etc. +/// - Timing attack: Detects instrumentation by execution time +/// - Active ADB connections: Active remote debugging class EngineDebuggerDetector implements ISecurityDetector { EngineDebuggerDetector({this.enabled = true}); @@ -69,6 +84,7 @@ class EngineDebuggerDetector implements ISecurityDetector { Future> _checkAndroidDebugger() async { final indicators = []; + // Verificação principal: processo sendo trackeado final result = await Process.run('cat', [ '/proc/self/status', @@ -91,6 +107,7 @@ class EngineDebuggerDetector implements ISecurityDetector { } } + // Verificação de processos debugger ativos final psResult = await Process.run('ps', [ '-ef', @@ -117,6 +134,34 @@ class EngineDebuggerDetector implements ISecurityDetector { } } + // Verificação adicional: aplicações de debugging conectadas + try { + final debuggableResult = await Process.run('getprop', ['ro.debuggable'], runInShell: true); + final adbResult = await Process.run('getprop', ['service.adb.tcp.port'], runInShell: true); + + if (debuggableResult.exitCode == 0 && adbResult.exitCode == 0) { + final isDebuggable = debuggableResult.stdout.toString().trim() == '1'; + final adbPort = adbResult.stdout.toString().trim(); + + // Se é debuggable E tem porta ADB ativa, pode indicar debugging ativo + if (isDebuggable && adbPort.isNotEmpty && adbPort != '-1') { + // Verificar se há realmente conexão ADB ativa + final netstatResult = await Process.run('netstat', ['-an'], runInShell: true).catchError( + (final _) => ProcessResult(0, 1, '', ''), + ); + + if (netstatResult.exitCode == 0) { + final netOutput = netstatResult.stdout.toString(); + if (netOutput.contains(':$adbPort') || netOutput.contains('5037')) { + indicators.add('Active ADB debugging connection detected'); + } + } + } + } + } catch (e) { + // Falha silenciosa, não é crítico + } + return indicators; } diff --git a/lib/src/detectors/engine_gps_fake_detector.dart b/lib/src/detectors/engine_gps_fake_detector.dart new file mode 100644 index 0000000..7f7572e --- /dev/null +++ b/lib/src/detectors/engine_gps_fake_detector.dart @@ -0,0 +1,285 @@ +// ignore_for_file: empty_catches + +import 'dart:async'; +import 'dart:io'; + +import 'package:engine_security/src/src.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class EngineGpsFakeDetector implements ISecurityDetector { + EngineGpsFakeDetector({this.enabled = true}); + + final bool enabled; + + static const MethodChannel _channel = MethodChannel('engine_security/gps_fake'); + + static final List _fakeLocationApps = [ + 'com.lexa.fakegps', + 'com.incorporateapps.fakegps.fre', + 'com.blogspot.newapphorizons.fakegps', + 'com.evdokimov.fakelocations', + 'com.technoplatform.fake_gps_go', + 'com.fakegps.app', + 'com.theappninjas.gpsjoystick', + 'ru.gavrikov.mocklocations', + 'appinventor.ai_progressdeveloper.FakeGPS', + 'com.gsmarena.mockgps', + 'com.mozzek.location_spoofer', + 'com.ebryx.gps.spoofer', + 'com.paget96.spoofgps', + 'com.droidtrail.mockgps', + 'com.appguru.location_spoofer', + 'com.excellentapps.mocklocation', + 'com.gps.hack.joystick', + 'com.incorporateapps.fakegps', + 'com.theappninjas.gpsemulator', + 'com.hola.fake.location', + 'com.vito.gps', + 'com.xss.gps.location', + 'com.gps.joystick.fake.location', + 'com.gps.hack.app', + 'com.android.fake.location', + ]; + + @override + SecurityThreatType get threatType => SecurityThreatType.gpsFake; + + @override + String get detectorName => 'GpsFakeDetector'; + + @override + bool get isAvailable => enabled; + + @override + DetectorInfoModel get detectorInfo => DetectorInfoModel( + name: detectorName, + threatType: threatType, + enabled: enabled, + platform: Platform.operatingSystem, + ); + + @override + Future performCheck() async { + if (!enabled) { + return SecurityCheckModel.secure( + details: 'GPS fake detector disabled', + detectionMethod: 'disabled_check', + confidence: 1.0, + ); + } + + try { + final detectionMethods = [ + if (Platform.isAndroid) ...await _checkAndroidGpsFake(), + if (Platform.isIOS) ...await _checkIOSGpsFake(), + ]; + + if (detectionMethods.isNotEmpty) { + return SecurityCheckModel.threat( + threatType: SecurityThreatType.gpsFake, + details: 'GPS fake detected: ${detectionMethods.join(', ')}', + detectionMethod: 'platform_specific_checks', + confidence: 0.90, + ); + } + + return SecurityCheckModel.secure( + details: 'No GPS manipulation detected', + detectionMethod: 'platform_checks', + confidence: 0.85, + ); + } catch (e) { + return SecurityCheckModel.secure( + details: 'GPS fake detection failed: $e', + detectionMethod: 'error_handling', + confidence: 0.50, + ); + } + } + + Future> _checkAndroidGpsFake() async { + final indicators = []; + + final futures = >[ + () async { + try { + final mockLocationEnabled = await _channel.invokeMethod('checkMockLocationEnabled'); + if (mockLocationEnabled == true) { + indicators.add('Mock location enabled in developer options'); + } + } catch (e) {} + }(), + + () async { + try { + final installedApps = await _channel.invokeMethod>('getInstalledApps'); + if (installedApps != null) { + final fakeAppsFound = installedApps + .where((final app) => _fakeLocationApps.contains(app.toString())) + .toList(); + if (fakeAppsFound.isNotEmpty) { + indicators.add('Fake GPS apps detected: ${fakeAppsFound.length} apps'); + } + } + } catch (e) {} + }(), + + () async { + try { + final status = await Permission.location.status; + if (status.isDenied || status.isPermanentlyDenied) { + indicators.add('Location permission denied'); + } + } catch (e) {} + }(), + ]; + + await Future.wait(futures); + + if (indicators.isNotEmpty) { + return indicators; + } + + if (await Geolocator.isLocationServiceEnabled()) { + try { + final position = await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.low, + timeLimit: Duration(milliseconds: 800), + ), + ).timeout(const Duration(milliseconds: 1500)); + + if (position.latitude == 0.0 && position.longitude == 0.0) { + indicators.add('GPS returning zero coordinates'); + } + + if (position.accuracy > 1000) { + indicators.add('GPS accuracy extremely poor'); + } + } catch (e) { + if (e.toString().contains('timeout')) { + indicators.add('GPS timeout - possible interference'); + } + } + } + + return indicators; + } + + Future> _checkIOSGpsFake() async { + final indicators = []; + + final futures = >[ + () async { + try { + final isJailbroken = await _channel.invokeMethod('checkJailbreakStatus'); + if (isJailbroken == true) { + indicators.add('Device is jailbroken - GPS spoofing possible'); + } + } catch (e) {} + }(), + + () async { + try { + final fakeApps = await _channel.invokeMethod>('getInstalledApps'); + if (fakeApps != null && fakeApps.isNotEmpty) { + indicators.add('Fake GPS apps detected: ${fakeApps.length} apps'); + } + } catch (e) {} + }(), + + () async { + try { + final reliability = await _channel.invokeMethod>('checkLocationServicesReliability'); + if (reliability != null) { + final isReliable = reliability['isReliable']?.toBool() ?? true; + final suspiciousCount = reliability['suspiciousCount']?.toInt() ?? 0; + + if (!isReliable) { + indicators.add('Location services unreliable'); + } + + if (suspiciousCount > 0) { + indicators.add('Core Location services compromised: $suspiciousCount issues'); + } + + final authStatus = reliability['authorizationStatus']?.toString(); + if (authStatus == 'restricted' || authStatus == 'denied') { + indicators.add('Location authorization issues: $authStatus'); + } + } + } catch (e) {} + }(), + + () async { + try { + final status = await Permission.location.status; + if (status.isDenied || status.isPermanentlyDenied) { + indicators.add('Location permission denied'); + } + } catch (e) {} + }(), + ]; + + await Future.wait(futures); + + if (indicators.isNotEmpty) { + return indicators; + } + + if (await Geolocator.isLocationServiceEnabled()) { + try { + final position = await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.low, + timeLimit: Duration(milliseconds: 800), + ), + ).timeout(const Duration(milliseconds: 1500)); + + if (position.latitude == 0.0 && position.longitude == 0.0) { + indicators.add('GPS returning zero coordinates'); + } + + if (position.accuracy > 1000) { + indicators.add('GPS accuracy extremely poor'); + } + + if (position.accuracy < 0.5 && position.altitude == 0.0) { + indicators.add('GPS data artificially perfect'); + } + } catch (e) { + if (e.toString().contains('timeout')) { + indicators.add('GPS timeout - possible interference'); + } + } + } + + return indicators; + } + + static Future checkMockLocationEnabled() async { + try { + final result = await _channel.invokeMethod('checkMockLocationEnabled'); + return result ?? false; + } catch (e) { + return false; + } + } + + static Future> getInstalledFakeGpsApps() async { + try { + final installedApps = await _channel.invokeMethod>('getInstalledApps'); + if (installedApps != null) { + return installedApps + .where((final app) => _fakeLocationApps.contains(app.toString())) + .map((final app) => app.toString()) + .toList(); + } + } catch (e) { + return []; + } + return []; + } +} diff --git a/lib/src/detectors/engine_root_detector.dart b/lib/src/detectors/engine_root_detector.dart index fdd9f2f..2cdda2a 100644 --- a/lib/src/detectors/engine_root_detector.dart +++ b/lib/src/detectors/engine_root_detector.dart @@ -79,12 +79,6 @@ class EngineRootDetector implements ISecurityDetector { '/system/bin/failsafe/su', '/data/local/su', '/su/bin/su', - '/system/xbin/which', - '/data/local/xbin/which', - '/system/bin/which', - '/system/xbin/busybox', - '/system/bin/busybox', - '/data/local/xbin/busybox', ]; for (final path in rootFiles) { @@ -120,14 +114,32 @@ class EngineRootDetector implements ISecurityDetector { try { final result = await Process.run('su', ['-c', 'id'], runInShell: true).timeout(const Duration(seconds: 1)); if (result.exitCode == 0) { - indicators.add('Su command successful'); + final output = result.stdout.toString(); + if (output.contains('uid=0') || output.contains('root')) { + indicators.add('Su command successful with root privileges'); + } } } catch (e) {} - final result = await Process.run('getprop', ['ro.build.tags'], runInShell: true); - if (result.exitCode == 0 && result.stdout.toString().toLowerCase().contains('test-keys')) { - indicators.add('Test keys build detected'); - } + try { + final result = await Process.run('getprop', ['ro.build.tags'], runInShell: true); + if (result.exitCode == 0) { + final buildTags = result.stdout.toString().toLowerCase().trim(); + if (buildTags.contains('test-keys')) { + final debuggable = await Process.run('getprop', ['ro.debuggable'], runInShell: true); + final secure = await Process.run('getprop', ['ro.secure'], runInShell: true); + + if (debuggable.exitCode == 0 && secure.exitCode == 0) { + final isDebuggable = debuggable.stdout.toString().trim() == '1'; + final isSecure = secure.stdout.toString().trim() == '1'; + + if (!isSecure && !isDebuggable) { + indicators.add('Insecure build with test-keys detected'); + } + } + } + } + } catch (e) {} return indicators; } diff --git a/lib/src/enums/security_threat_type.dart b/lib/src/enums/security_threat_type.dart index 5a152a7..a9bc86f 100644 --- a/lib/src/enums/security_threat_type.dart +++ b/lib/src/enums/security_threat_type.dart @@ -26,6 +26,12 @@ enum SecurityThreatType { displayName: 'Debugger Detection', description: 'Debugger attachment detected', severityLevel: 2, + ), + + gpsFake( + displayName: 'GPS Fake Detection', + description: 'GPS location spoofing or fake GPS app detected', + severityLevel: 7, ); final String displayName; diff --git a/pubspec.yaml b/pubspec.yaml index ff84cc2..b3eb778 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,9 @@ dependencies: sdk: flutter device_info_plus: ^11.5.0 package_info_plus: ^8.3.0 + geolocator: ^13.0.1 + location: ^7.0.0 + permission_handler: ^11.3.1 dev_dependencies: flutter_test: diff --git a/test/engine_gps_fake_detector_test.dart b/test/engine_gps_fake_detector_test.dart new file mode 100644 index 0000000..5576432 --- /dev/null +++ b/test/engine_gps_fake_detector_test.dart @@ -0,0 +1,76 @@ +import 'package:engine_security/engine_security.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('EngineGpsFakeDetector', () { + late EngineGpsFakeDetector detector; + + setUp(() { + detector = EngineGpsFakeDetector(); + }); + + test('should have correct threat type', () { + expect(detector.threatType, SecurityThreatType.gpsFake); + }); + + test('should have correct detector name', () { + expect(detector.detectorName, 'GpsFakeDetector'); + }); + + test('should provide detector info', () { + final info = detector.detectorInfo; + expect(info.name, 'GpsFakeDetector'); + expect(info.threatType, SecurityThreatType.gpsFake); + expect(info.enabled, detector.isAvailable); + }); + + test('should be enabled by default', () { + expect(detector.isAvailable, isTrue); + }); + + test('should respect enabled parameter', () { + final disabledDetector = EngineGpsFakeDetector(enabled: false); + expect(disabledDetector.isAvailable, isFalse); + }); + + test('should return disabled result when disabled', () async { + final disabledDetector = EngineGpsFakeDetector(enabled: false); + final result = await disabledDetector.performCheck(); + + expect(result.isSecure, isTrue); + expect(result.details, contains('disabled')); + expect(result.detectionMethod, equals('disabled_check')); + expect(result.confidence, equals(1.0)); + }); + + test('should perform check without errors', () async { + final result = await detector.performCheck(); + expect(result, isA()); + expect(result.confidence, greaterThanOrEqualTo(0.0)); + expect(result.confidence, lessThanOrEqualTo(1.0)); + + if (result.isSecure) { + expect(result.threatType, SecurityThreatType.unknown); + } else { + expect(result.threatType, SecurityThreatType.gpsFake); + } + }); + + test('should handle static methods', () async { + final mockEnabled = await EngineGpsFakeDetector.checkMockLocationEnabled(); + final fakeApps = await EngineGpsFakeDetector.getInstalledFakeGpsApps(); + + expect(mockEnabled, isA()); + expect(fakeApps, isA>()); + }); + }); + + group('SecurityThreatType.gpsFake', () { + test('should have correct properties', () { + const threatType = SecurityThreatType.gpsFake; + expect(threatType.displayName, 'GPS Fake Detection'); + expect(threatType.description, 'GPS location spoofing or fake GPS app detected'); + expect(threatType.severityLevel, 7); + }); + }); +} diff --git a/test/enums/security_threat_type_test.dart b/test/enums/security_threat_type_test.dart index 84de1d0..8a8cda2 100644 --- a/test/enums/security_threat_type_test.dart +++ b/test/enums/security_threat_type_test.dart @@ -11,6 +11,7 @@ void main() { SecurityThreatType.emulator, SecurityThreatType.rootJailbreak, SecurityThreatType.debugger, + SecurityThreatType.gpsFake, ]; expect(SecurityThreatType.values, containsAll(expectedValues)); @@ -23,6 +24,7 @@ void main() { expect(SecurityThreatType.emulator, isNotNull); expect(SecurityThreatType.rootJailbreak, isNotNull); expect(SecurityThreatType.debugger, isNotNull); + expect(SecurityThreatType.gpsFake, isNotNull); }); }); @@ -33,6 +35,7 @@ void main() { expect(SecurityThreatType.emulator.displayName, equals('Emulator Detection')); expect(SecurityThreatType.rootJailbreak.displayName, equals('Root/Jailbreak Detection')); expect(SecurityThreatType.debugger.displayName, equals('Debugger Detection')); + expect(SecurityThreatType.gpsFake.displayName, equals('GPS Fake Detection')); }); test('should have correct description for all values', () { @@ -44,6 +47,7 @@ void main() { equals('Device has been rooted (Android) or jailbroken (iOS)'), ); expect(SecurityThreatType.debugger.description, equals('Debugger attachment detected')); + expect(SecurityThreatType.gpsFake.description, equals('GPS location spoofing or fake GPS app detected')); }); test('should have correct severityLevel for all values', () { @@ -52,6 +56,7 @@ void main() { expect(SecurityThreatType.emulator.severityLevel, equals(6)); expect(SecurityThreatType.rootJailbreak.severityLevel, equals(8)); expect(SecurityThreatType.debugger.severityLevel, equals(2)); + expect(SecurityThreatType.gpsFake.severityLevel, equals(7)); }); }); @@ -62,6 +67,7 @@ void main() { expect(SecurityThreatType.debugger, equals(SecurityThreatType.debugger)); expect(SecurityThreatType.unknown, equals(SecurityThreatType.unknown)); expect(SecurityThreatType.rootJailbreak, equals(SecurityThreatType.rootJailbreak)); + expect(SecurityThreatType.gpsFake, equals(SecurityThreatType.gpsFake)); }); test('should not be equal to different enum values', () { @@ -84,6 +90,7 @@ void main() { expect(SecurityThreatType.debugger.toString(), contains('SecurityThreatType.debugger')); expect(SecurityThreatType.unknown.toString(), contains('SecurityThreatType.unknown')); expect(SecurityThreatType.rootJailbreak.toString(), contains('SecurityThreatType.rootJailbreak')); + expect(SecurityThreatType.gpsFake.toString(), contains('SecurityThreatType.gpsFake')); }); }); @@ -104,6 +111,7 @@ void main() { expect(list, contains(SecurityThreatType.debugger)); expect(list, contains(SecurityThreatType.unknown)); expect(list, contains(SecurityThreatType.rootJailbreak)); + expect(list, contains(SecurityThreatType.gpsFake)); }); test('should work with set operations', () { @@ -117,6 +125,7 @@ void main() { expect(highSeverityThreats, contains(SecurityThreatType.frida)); expect(highSeverityThreats, contains(SecurityThreatType.rootJailbreak)); + expect(highSeverityThreats, contains(SecurityThreatType.gpsFake)); expect(highSeverityThreats, isNot(contains(SecurityThreatType.debugger))); }); @@ -128,6 +137,7 @@ void main() { expect(displayNames, contains('Debugger Detection')); expect(displayNames, contains('Unknown Threat')); expect(displayNames, contains('Root/Jailbreak Detection')); + expect(displayNames, contains('GPS Fake Detection')); }); }); @@ -145,6 +155,8 @@ void main() { return 'Device is rooted/jailbroken'; case SecurityThreatType.unknown: return 'Unknown threat'; + case SecurityThreatType.gpsFake: + return 'GPS fake detected'; } } @@ -153,6 +165,7 @@ void main() { expect(getTypeDescription(SecurityThreatType.debugger), equals('Debugger attached')); expect(getTypeDescription(SecurityThreatType.rootJailbreak), equals('Device is rooted/jailbroken')); expect(getTypeDescription(SecurityThreatType.unknown), equals('Unknown threat')); + expect(getTypeDescription(SecurityThreatType.gpsFake), equals('GPS fake detected')); }); test('should maintain enum order consistency', () { @@ -162,6 +175,7 @@ void main() { expect(valuesList.indexOf(SecurityThreatType.emulator), equals(2)); expect(valuesList.indexOf(SecurityThreatType.rootJailbreak), equals(3)); expect(valuesList.indexOf(SecurityThreatType.debugger), equals(4)); + expect(valuesList.indexOf(SecurityThreatType.gpsFake), equals(5)); }); test('should work with json serialization scenarios', () { @@ -172,6 +186,7 @@ void main() { expect(enumNames, contains('emulator')); expect(enumNames, contains('rootJailbreak')); expect(enumNames, contains('debugger')); + expect(enumNames, contains('gpsFake')); }); }); @@ -232,7 +247,7 @@ void main() { .reduce((final a, final b) => a + b); final averageSeverity = totalSeverity / SecurityThreatType.values.length; - expect(averageSeverity, equals(6.0)); + expect(averageSeverity, closeTo(6.17, 0.1)); }); }); });