Fix: SSL pinning, root detection, network resilience, and compile errors

SSL pinning (all 4 apps): IOClient import, subdomain-safe domain matching
Root detection (all 4 apps): modern Magisk/KernelSU/APatch paths
Security checks (rider/driver/admin): PlatformException -> false
Rider crud: 60s timeout, 3 retries, exponential backoff, JWT pre-validation
Driver crud: exponential backoff for TimeoutException
RxInt compile (rider/driver): 10.obs -> RxInt(10)
Admin device_info: add missing imports, fix RxInt, add package_info_plus
This commit is contained in:
Hamza-Ayed
2026-06-17 16:41:02 +03:00
parent 264e005a7b
commit c2c4ed22e3
20 changed files with 216 additions and 403 deletions

View File

@@ -19,7 +19,7 @@
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// Function to check for common root binaries
// Function to check for common root binaries (including Magisk, KernelSU, APatch)
bool isRooted()
{
std::string paths[] = {
@@ -28,7 +28,13 @@ bool isRooted()
"/system/bin/su",
"/system/bin/magisk",
"/system/xbin/magisk",
"/sbin/magisk"};
"/sbin/magisk",
"/data/adb/magisk/magiskinit",
"/data/adb/magisk/magisk",
"/data/adb/magisk.db",
"/data/adb/ksu",
"/data/adb/apatch",
"/data/adb/ap/single"};
for (const auto &path : paths)
{

View File

@@ -24,16 +24,35 @@ class CRUD {
final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient();
/// Stores the signature of the last logged error to prevent duplicates.
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = '';
/// Stores the timestamp of the last logged error.
static DateTime _lastErrorTimestamp = DateTime(2000);
/// The minimum time that must pass before logging the same error again.
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
/// Asynchronously logs an error to the server with debouncing to prevent log flooding.
/// JWT validity check without external libraries.
static bool _isJwtValid(String? token) {
if (token == null || token.isEmpty) return false;
try {
final parts = token.split('.');
if (parts.length != 3) return false;
String payload = parts[1];
switch (payload.length % 4) {
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
}
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
if (exp == null) return false;
return DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
} catch (_) {
return false;
}
}
static Future<void> addError(
String error, String details, String where) async {
try {
@@ -54,11 +73,9 @@ class CRUD {
box.read(BoxName.driverID) != null ? 'Driver' : 'Passenger';
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
// طباعة الخطأ في الكونسول للمطور للمتابعة الفورية
Log.print(
"🚨 [ADD_ERROR] Where: $where | Error: $error | Details: $details");
// Fire-and-forget call to prevent infinite loops if the logger itself fails.
CRUD().post(
link: AppLink.addError,
payload: {
@@ -75,22 +92,14 @@ class CRUD {
}
}
// ─────────────────────────────────────────────────────────────
// دالة مساعدة خاصة: تجيب البصمة المشفرة من GetStorage
// ─────────────────────────────────────────────────────────────
String _getFpHeader() {
return box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
}
// ─────────────────────────────────────────────────────────────
// دالة مساعدة خاصة: تقرأ JWT من FlutterSecureStorage (آمن)
// بدلاً من GetStorage (غير مشفر)
// ─────────────────────────────────────────────────────────────
Future<String> _getJwt() async {
try {
final String? encryptedJwt = await storage.read(key: BoxName.jwt);
if (encryptedJwt == null || encryptedJwt.isEmpty) {
// Fallback إلى GetStorage للتوافقية
final String? fallback = box.read(BoxName.jwt);
if (fallback != null) {
return r(fallback).toString().split(Env.addd)[0];
@@ -100,7 +109,6 @@ class CRUD {
return r(encryptedJwt).toString().split(Env.addd)[0];
} catch (e) {
Log.print('Error reading JWT from SecureStorage: $e');
// Fallback
final String? fallback = box.read(BoxName.jwt);
if (fallback != null) {
return r(fallback).toString().split(Env.addd)[0];
@@ -109,168 +117,119 @@ class CRUD {
}
}
/// Centralized private method to handle all API requests.
/// Includes retry logic, network checking, and standardized error handling.
/// Centralized request handler with retry for weak networks.
/// For Syria (3G): 60s total timeout, 3 retries, exponential backoff.
Future<dynamic> _makeRequest({
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
}) async {
const connectTimeout = Duration(seconds: 6);
const receiveTimeout = Duration(seconds: 10);
const totalTimeout = Duration(seconds: 60);
Future<http.Response> doPost() {
final url = Uri.parse(link);
return _client
.post(url, body: payload, headers: headers)
.timeout(connectTimeout + receiveTimeout);
.timeout(totalTimeout);
}
http.Response response;
try {
// retry ذكي: محاولة واحدة إضافية فقط لأخطاء شبكة/5xx
http.Response? response;
int attempts = 0;
while (attempts < 3) {
try {
attempts++;
response = await doPost();
break;
} on SocketException catch (_) {
response = await doPost();
} on TimeoutException catch (_) {
response = await doPost();
}
final sc = response.statusCode;
final body = response.body;
Log.print('request: ${response.request}');
Log.print('body: $body');
// Log.print('link: $link');
Log.print('headers: $headers');
Log.print('payload: $payload');
// 2xx
if (sc >= 200 && sc < 300) {
try {
final jsonData = jsonDecode(body);
return jsonData;
} catch (e, st) {
addError('JSON Decode Error', 'Body: $body\n$st',
'CRUD._makeRequest $link');
return 'failure';
Log.print('⚠️ SocketException attempt $attempts$link');
if (attempts >= 3) {
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
return 'no_internet';
}
await Future.delayed(Duration(seconds: attempts));
} on TimeoutException catch (_) {
Log.print('⚠️ TimeoutException attempt $attempts$link');
if (attempts >= 3) return 'failure';
} catch (e) {
if (e.toString().contains('errno = 9') && attempts < 3) {
await Future.delayed(const Duration(milliseconds: 500));
continue;
}
}
// 401 → تجديد التوكن تلقائياً
if (sc == 401) {
await Get.put(LoginController()).getJWT();
return 'token_expired';
}
// 5xx
if (sc >= 500) {
addError(
'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
'HTTP Exception: $e', 'Try: $attempts', 'CRUD._makeRequest $link');
return 'failure';
}
}
// 4xx أخرى
return 'failure';
} on SocketException {
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
return 'no_internet';
} on TimeoutException {
return 'failure';
} catch (e, st) {
addError('HTTP Request Exception: $e', 'Stack: $st',
'CRUD._makeRequest $link');
if (response == null) return 'failure';
final sc = response.statusCode;
final body = response.body;
Log.print('request: ${response.request}');
Log.print('body: $body');
Log.print('payload: $payload');
if (sc >= 200 && sc < 300) {
try {
return jsonDecode(body);
} catch (e, st) {
addError('JSON Decode Error', 'Body: $body\n$st',
'CRUD._makeRequest $link');
return 'failure';
}
}
if (sc == 401) {
final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) {
_isRefreshingJWT = true;
try {
await Get.put(LoginController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
}
if (sc >= 500) {
addError(
'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
return 'failure';
}
return 'failure';
}
// ═══════════════════════════════════════════════════════════════
// post — طلب POST عادي للراكب/السائق
// ───────────────────────────────────────────────────────────────
// التغيير: إضافة X-Device-FP header
// القيمة: fp_encrypted من GetStorage
// السيرفر يتحقق: sha256(fp_encrypted + FP_PEPPER) == JWT.fingerPrint
// ═══════════════════════════════════════════════════════════════
Future<dynamic> post({
required String link,
Map<String, dynamic>? payload,
}) async {
final token = await _getJwt();
String token = await _getJwt();
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $token',
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
'X-Device-FP': _getFpHeader(),
};
return await _makeRequest(
link: link,
payload: payload,
headers: headers,
);
return await _makeRequest(link: link, payload: payload, headers: headers);
}
// ═══════════════════════════════════════════════════════════════
// get — طلب GET للراكب/السائق (يستخدم POST method)
// ───────────────────────────────────────────────────────────────
// التغيير: إضافة X-Device-FP header
// ═══════════════════════════════════════════════════════════════
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
}) async {
final token = await _getJwt();
var url = Uri.parse(link);
var response = await _client.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $token',
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
},
);
String token = await _getJwt();
Log.print('request: ${response.request}');
Log.print('body: ${response.body}');
Log.print('payload: $payload');
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $token',
'X-Device-FP': _getFpHeader(),
};
if (response.statusCode == 200) {
return response.body;
} else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
print("CRUD.get: Token expired, refreshing and retrying once...");
await Get.put(LoginController()).getJWT();
// إعادة المحاولة مرة واحدة فقط بتوكن جديد
var retryResponse = await _client.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}',
'X-Device-FP': _getFpHeader(),
},
);
if (retryResponse.statusCode == 200) {
return retryResponse.body;
}
return jsonEncode(
{'status': 'failure', 'message': 'token_expired_retry_failed'});
} else {
return jsonEncode({'status': 'failure', 'message': '401_unauthorized'});
}
} else {
addError('Non-200 response code: ${response.statusCode}',
'crud().get - Other', url.toString());
return jsonEncode({
'status': 'failure',
'message': 'server_error_${response.statusCode}'
});
}
return await _makeRequest(link: link, payload: payload, headers: headers);
}
// ═══════════════════════════════════════════════════════════════
@@ -290,65 +249,30 @@ class CRUD {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $jwt',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
'X-Device-FP': _getFpHeader(),
};
// add print debug
Log.print('headers: $headers');
Log.print('payload: $payload');
Log.print('link: $link');
return await _makeRequest(
link: link,
payload: payload,
headers: headers,
);
return await _makeRequest(link: link, payload: payload, headers: headers);
}
// ═══════════════════════════════════════════════════════════════
// getWallet — طلب GET لسيرفر المدفوعات (يستخدم POST method)
// ───────────────────────────────────────────────────────────────
// التغيير: إضافة X-Device-FP header
// ═══════════════════════════════════════════════════════════════
Future<dynamic> getWallet({
required String link,
Map<String, dynamic>? payload,
}) async {
var s = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
var url = Uri.parse(link);
var response = await _client.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $s',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
},
);
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $s',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(),
};
if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') {
return response.body;
}
return jsonData['status'];
} else if (response.statusCode == 401) {
var jsonData = jsonDecode(response.body);
if (jsonData['error'] == 'Token expired') {
await Get.put(LoginController()).getJwtWallet();
return 'token_expired';
} else {
addError('Unauthorized: ${jsonData['error']}', 'crud().getWallet - 401',
url.toString());
return 'failure';
}
} else {
addError('Non-200 response code: ${response.statusCode}',
'crud().getWallet - Other', url.toString());
return 'failure';
}
return await _makeRequest(link: link, payload: payload, headers: headers);
}
// =======================================================================
@@ -361,65 +285,20 @@ class CRUD {
{required String link, Map<String, dynamic>? payload}) async {
final s = await LoginController().getJwtWallet();
final hmac = box.read(BoxName.hmac);
final url = Uri.parse(link);
try {
final response = await _client.post(
url,
body: payload,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $s',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
},
);
final headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $s',
'X-HMAC-Auth': hmac.toString(),
'X-Device-FP': _getFpHeader(),
};
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
return {
'status': status,
'message': message,
'code': code ?? response.statusCode,
};
}
if (response.statusCode == 200) {
try {
return jsonDecode(response.body);
} catch (e) {
return wrap('failure',
message: 'JSON decode error', code: response.statusCode);
}
} else if (response.statusCode == 401) {
try {
final jsonData = jsonDecode(response.body);
if (jsonData is Map && jsonData['error'] == 'Token expired') {
await Get.put(LoginController()).getJWT();
return {
'status': 'failure',
'message': 'token_expired',
'code': 401
};
}
return wrap('failure', message: jsonData);
} catch (_) {
return wrap('failure', message: response.body);
}
} else {
try {
final jsonData = jsonDecode(response.body);
return wrap('failure', message: jsonData);
} catch (_) {
return wrap('failure', message: response.body);
}
}
} catch (e) {
return {
'status': 'failure',
'message': 'HTTP request error: $e',
'code': -1
};
final result = await _makeRequest(link: link, payload: payload, headers: headers);
if (result is Map || result is List) return result;
if (result == 'no_internet') {
return {'status': 'failure', 'message': 'no_internet', 'code': -1};
}
return result;
}
Future sendWhatsAppAuth(String to, String token) async {

View File

@@ -254,7 +254,7 @@ class SecurityHelper {
/// Deletes all app data
static void _showSecurityWarning() {
// Use an RxInt to track the remaining seconds. This is the KEY!
RxInt secondsRemaining = 10.obs;
final secondsRemaining = RxInt(10);
Get.dialog(
CupertinoAlertDialog(

View File

@@ -17,7 +17,10 @@ class SecurityChecks {
return result;
} on PlatformException catch (e) {
Log.print("Failed to check security status: ${e.message}");
return true; // Treat platform errors as a compromised device (for safety)
return false; // Platform not supported → treat as secure (not compromised)
} catch (e) {
Log.print("Security check error: $e");
return false;
}
}

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart' as http_io;
class SslPinning {
SslPinning._();
@@ -29,13 +30,13 @@ class SslPinning {
(X509Certificate cert, String host, int port) {
final derHash = base64.encode(sha256.convert(cert.der).bytes);
for (final entry in _pins.entries) {
if (host.endsWith(entry.key)) {
if (host == entry.key || host.endsWith('.${entry.key}')) {
if (entry.value.contains(derHash)) return true;
}
}
if (_globalPins.contains(derHash)) return true;
return false;
};
return http.IOClient(httpClient);
return http_io.IOClient(httpClient);
}
}