diff --git a/.gitignore b/.gitignore index 5509b1a..6377e21 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,11 @@ komet.txt original_app.txt fingerprint.py PCAPdroid_*.txt + +# Локальные рабочие файлы и эксперименты (не для репозитория) +PR_FullStack.md +docs/ +maxmint/ +maxtun/ +turnprobe/ +test/live_server_probe_test.dart diff --git a/lib/backend/api.dart b/lib/backend/api.dart index a0c2d1f..128d656 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -201,6 +201,7 @@ class Api { ); } else if (Platform.isIOS) { final iosInfo = await deviceInfo.iosInfo; + deviceType = 'IOS'; osVersion = iosInfo.systemVersion; deviceName = iosInfo.utsname.machine; } else if (Platform.isAndroid) { diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 3a1a464..4b27c10 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -7,6 +7,7 @@ import '../../core/protocol/chat_cache_fingerprint.dart'; import '../../core/protocol/opcode_map.dart'; import '../../core/protocol/packet.dart'; import '../../core/storage/app_database.dart'; +import '../../core/storage/spoofing_service.dart'; import '../../core/storage/token_storage.dart'; import '../../core/utils/logger.dart'; import 'chats.dart'; @@ -883,6 +884,7 @@ class AccountModule { if (sessionToken != null && accountId != null) { await TokenStorage.saveToken(sessionToken, accountId); await TokenStorage.setActiveAccount(accountId); + await SpoofingService.commitPendingSpoof(accountId); } return result; @@ -938,6 +940,7 @@ class AccountModule { final profile = ProfileData.fromServerMap(contact.cast()); await AppDatabase.saveProfile(profile, isActive: true); await TokenStorage.setActiveAccount(accountId); + await SpoofingService.commitPendingSpoof(accountId); logger.i('Регистрация завершена, accountId=$accountId'); return accountId; @@ -992,6 +995,7 @@ class AccountModule { } await TokenStorage.saveToken(authToken, resolvedAccountId); await TokenStorage.setActiveAccount(resolvedAccountId); + await SpoofingService.commitPendingSpoof(resolvedAccountId); } final result = await _processLoginResponse(dataMap, resolvedAccountId); @@ -1038,6 +1042,11 @@ class AccountModule { } Future beginAddAccount() async { + final existing = await AppDatabase.loadAllProfiles(); + await SpoofingService.prepareNewAccountSpoof( + existing.map((p) => p.id).toList(growable: false), + ); + try { await _api.disconnect(); } catch (_) {} @@ -1088,6 +1097,7 @@ class AccountModule { Future removeAccount(int accountId) async { await AppDatabase.deleteAccount(accountId); await TokenStorage.deleteAccount(accountId); + await SpoofingService.clearAccountSpoof(accountId); logger.i('Аккаунт $accountId удалён локально'); } diff --git a/lib/backend/modules/digital_id.dart b/lib/backend/modules/digital_id.dart index 03a66ec..e3aae4f 100644 --- a/lib/backend/modules/digital_id.dart +++ b/lib/backend/modules/digital_id.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:device_info_plus/device_info_plus.dart'; import '../../core/storage/app_database.dart'; +import '../../core/storage/spoofing_service.dart'; import '../../core/storage/token_storage.dart'; import '../../models/digital_id.dart'; import 'webapp.dart'; @@ -33,6 +34,7 @@ class DigitalIdModule { String? _webAppData; String? _deviceId; + String? _realUserAgent; DigitalIdModule(this._webApp); @@ -94,6 +96,33 @@ class DigitalIdModule { return hex; } + Future _resolveUserAgent() async { + final spoofed = await SpoofingService.getWebViewUserAgent(); + if (spoofed != null && spoofed.isNotEmpty) return spoofed; + return _realUserAgent ??= await _buildRealUserAgent(); + } + + Future _buildRealUserAgent() async { + try { + final info = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final android = await info.androidInfo; + return 'Mozilla/5.0 (Linux; Android ${android.version.release}; ' + '${android.model}) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/124.0.0.0 Mobile Safari/537.36'; + } + if (Platform.isIOS) { + final ios = await info.iosInfo; + final version = ios.systemVersion.replaceAll('.', '_'); + return 'Mozilla/5.0 (iPhone; CPU iPhone OS $version like Mac OS X) ' + 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 ' + 'Mobile/15E148 Safari/604.1'; + } + } catch (_) {} + return 'Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36'; + } + Future _send( String method, String path, { @@ -108,11 +137,7 @@ class DigitalIdModule { request.headers.set('Referer', 'https://digital-id.max.ru/'); request.headers.set('x-requested-with', 'ru.oneme.app'); request.headers.set('Accept', 'application/json'); - request.headers.set( - 'User-Agent', - 'Mozilla/5.0 (Linux; Android 16; Pixel 7 Pro) AppleWebKit/537.36 ' - '(KHTML, like Gecko) Version/4.0 Chrome/148.0.0.0 Mobile Safari/537.36', - ); + request.headers.set('User-Agent', await _resolveUserAgent()); if (body != null) { request.headers.contentType = ContentType.json; request.add(utf8.encode(jsonEncode(body))); diff --git a/lib/core/storage/spoofing_service.dart b/lib/core/storage/spoofing_service.dart index a51890b..f5b3624 100644 --- a/lib/core/storage/spoofing_service.dart +++ b/lib/core/storage/spoofing_service.dart @@ -1,30 +1,239 @@ +import 'dart:convert'; +import 'dart:math'; + import 'package:shared_preferences/shared_preferences.dart'; +import '../config/device_presets.dart'; +import '../../models/spoof_profile.dart'; +import 'token_storage.dart'; + class SpoofingService { static const String hardcodedAppVersion = '26.17.1'; static const int hardcodedBuildNumber = 6712; + static const String pendingScope = 'pending'; + + static const String _legacyEnabledKey = 'spoofing_enabled'; + static const List _legacyKeys = [ + 'spoofing_enabled', + 'spoof_devicename', + 'spoof_osversion', + 'spoof_screen', + 'spoof_timezone', + 'spoof_locale', + 'spoof_devicelocale', + 'spoof_deviceid', + 'spoof_devicetype', + 'spoof_arch', + 'spoof_appversion', + 'spoof_buildnumber', + 'spoof_pushdevicetype', + 'spoof_instanceid', + 'spoof_clientsessionid', + 'spoof_useragent', + ]; + + static final Random _rng = Random.secure(); + + static String _profileKey(String scope) => 'spoof_profile_$scope'; + + static Future activeScope() async { + final id = await TokenStorage.getActiveAccountId(); + return id?.toString() ?? pendingScope; + } + + static Future loadProfile(String scope) async { + final prefs = await SharedPreferences.getInstance(); + return _read(prefs, scope); + } + + static Future saveProfile(String scope, SpoofProfile profile) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_profileKey(scope), jsonEncode(profile.toJson())); + } + + static Future clearAccountSpoof(int accountId) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_profileKey('$accountId')); + } + + static Future commitPendingSpoof(int accountId) async { + final prefs = await SharedPreferences.getInstance(); + final pending = await _read(prefs, pendingScope); + if (pending == null) return; + await prefs.setString( + _profileKey('$accountId'), + jsonEncode(pending.toJson()), + ); + await prefs.remove(_profileKey(pendingScope)); + } + + static Future prepareNewAccountSpoof( + List existingAccountIds, + ) async { + final prefs = await SharedPreferences.getInstance(); + + final used = {}; + for (final id in existingAccountIds) { + final existing = await _read(prefs, '$id'); + if (existing != null && existing.deviceName.isNotEmpty) { + used.add(existing.deviceName); + } + } + + bool isMobile(DevicePreset p) => + p.deviceType == 'ANDROID' || p.deviceType == 'IOS'; + final fresh = devicePresets + .where((p) => isMobile(p) && !used.contains(p.deviceName)) + .toList(); + final pool = fresh.isNotEmpty ? fresh : devicePresets.where(isMobile).toList(); + final preset = pool[_rng.nextInt(pool.length)]; + final shortLocale = preset.locale.split(RegExp(r'[-_]')).first; + + final profile = SpoofProfile( + enabled: true, + deviceName: preset.deviceName, + osVersion: preset.osVersion, + screen: preset.screen, + timezone: preset.timezone, + locale: shortLocale, + deviceLocale: shortLocale, + deviceId: _hex(8), + deviceType: preset.deviceType, + arch: preset.deviceType == 'IOS' ? 'arm64' : 'arm64-v8a', + appVersion: hardcodedAppVersion, + buildNumber: hardcodedBuildNumber, + pushDeviceType: 'GCM', + instanceId: _uuidV4(), + clientSessionId: _rng.nextInt(0x7FFFFFFF) + 1, + userAgent: preset.userAgent, + ); + + await prefs.setString( + _profileKey(pendingScope), + jsonEncode(profile.toJson()), + ); + return profile; + } static Future?> getSpoofedSessionData() async { final prefs = await SharedPreferences.getInstance(); - - final isEnabled = prefs.getBool('spoofing_enabled') ?? false; - if (!isEnabled) return null; + final profile = await _read(prefs, await activeScope()); + if (profile == null || !profile.enabled) return null; return { - 'device_name': prefs.getString('spoof_devicename'), - 'os_version': prefs.getString('spoof_osversion'), - 'screen': prefs.getString('spoof_screen'), - 'timezone': prefs.getString('spoof_timezone'), - 'locale': prefs.getString('spoof_locale'), - 'device_locale': prefs.getString('spoof_devicelocale'), - 'device_id': prefs.getString('spoof_deviceid'), - 'device_type': prefs.getString('spoof_devicetype'), - 'app_version': prefs.getString('spoof_appversion') ?? hardcodedAppVersion, - 'arch': prefs.getString('spoof_arch') ?? 'arm64-v8a', - 'build_number': prefs.getInt('spoof_buildnumber') ?? hardcodedBuildNumber, - 'instance_id': prefs.getString('spoof_instanceid'), - 'client_session_id': prefs.getInt('spoof_clientsessionid'), - 'push_device_type': prefs.getString('spoof_pushdevicetype'), + 'device_name': profile.deviceName, + 'os_version': profile.osVersion, + 'screen': profile.screen, + 'timezone': profile.timezone, + 'locale': profile.locale, + 'device_locale': profile.deviceLocale, + 'device_id': profile.deviceId, + 'device_type': profile.deviceType, + 'app_version': + profile.appVersion.isEmpty ? hardcodedAppVersion : profile.appVersion, + 'arch': profile.arch.isEmpty ? 'arm64-v8a' : profile.arch, + 'build_number': + profile.buildNumber == 0 ? hardcodedBuildNumber : profile.buildNumber, + 'instance_id': profile.instanceId, + 'client_session_id': profile.clientSessionId, + 'push_device_type': profile.pushDeviceType, }; } + + static Future getWebViewUserAgent() async { + final prefs = await SharedPreferences.getInstance(); + final profile = await _read(prefs, await activeScope()); + if (profile == null || !profile.enabled) return null; + + if (profile.userAgent.isNotEmpty) return profile.userAgent; + for (final preset in devicePresets) { + if (preset.deviceName == profile.deviceName) return preset.userAgent; + } + return _deriveUserAgent(profile); + } + + static String _deriveUserAgent(SpoofProfile profile) { + final deviceType = + profile.deviceType.isEmpty ? 'ANDROID' : profile.deviceType; + final osVersion = profile.osVersion; + final model = profile.deviceName.isEmpty ? 'K' : profile.deviceName; + + if (deviceType == 'IOS' || deviceType == 'iOS') { + final version = + osVersion.replaceAll(RegExp(r'[^0-9.]'), '').replaceAll('.', '_'); + return 'Mozilla/5.0 (iPhone; CPU iPhone OS ' + '${version.isEmpty ? '17_0' : version} like Mac OS X) ' + 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 ' + 'Mobile/15E148 Safari/604.1'; + } + + final android = osVersion.isEmpty ? 'Android 14' : osVersion; + return 'Mozilla/5.0 (Linux; $android; $model) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36'; + } + + static Future _read( + SharedPreferences prefs, + String scope, + ) async { + final raw = prefs.getString(_profileKey(scope)); + if (raw != null && raw.isNotEmpty) { + try { + return SpoofProfile.fromJson(jsonDecode(raw) as Map); + } catch (_) {} + } + if (scope != pendingScope) { + return _migrateLegacy(prefs, scope); + } + return null; + } + + static Future _migrateLegacy( + SharedPreferences prefs, + String scope, + ) async { + if (!(prefs.getBool(_legacyEnabledKey) ?? false)) return null; + + final profile = SpoofProfile( + enabled: true, + deviceName: prefs.getString('spoof_devicename') ?? '', + osVersion: prefs.getString('spoof_osversion') ?? '', + screen: prefs.getString('spoof_screen') ?? '', + timezone: prefs.getString('spoof_timezone') ?? '', + locale: prefs.getString('spoof_locale') ?? '', + deviceLocale: prefs.getString('spoof_devicelocale') ?? '', + deviceId: prefs.getString('spoof_deviceid') ?? '', + deviceType: prefs.getString('spoof_devicetype') ?? 'ANDROID', + arch: prefs.getString('spoof_arch') ?? 'arm64-v8a', + appVersion: prefs.getString('spoof_appversion') ?? hardcodedAppVersion, + buildNumber: prefs.getInt('spoof_buildnumber') ?? hardcodedBuildNumber, + pushDeviceType: prefs.getString('spoof_pushdevicetype') ?? 'GCM', + instanceId: prefs.getString('spoof_instanceid') ?? '', + clientSessionId: prefs.getInt('spoof_clientsessionid'), + userAgent: prefs.getString('spoof_useragent') ?? '', + ); + + await prefs.setString(_profileKey(scope), jsonEncode(profile.toJson())); + for (final key in _legacyKeys) { + await prefs.remove(key); + } + return profile; + } + + static String _hex(int bytes) { + final sb = StringBuffer(); + for (var i = 0; i < bytes; i++) { + sb.write(_rng.nextInt(256).toRadixString(16).padLeft(2, '0')); + } + return sb.toString(); + } + + static String _uuidV4() { + final b = List.generate(16, (_) => _rng.nextInt(256)); + b[6] = (b[6] & 0x0f) | 0x40; + b[8] = (b[8] & 0x3f) | 0x80; + String h(int i) => b[i].toRadixString(16).padLeft(2, '0'); + return '${h(0)}${h(1)}${h(2)}${h(3)}-${h(4)}${h(5)}-${h(6)}${h(7)}-' + '${h(8)}${h(9)}-${h(10)}${h(11)}${h(12)}${h(13)}${h(14)}${h(15)}'; + } } diff --git a/lib/frontend/screens/digital_id/digital_id_web_screen.dart b/lib/frontend/screens/digital_id/digital_id_web_screen.dart index fd7e3ac..0e7afea 100644 --- a/lib/frontend/screens/digital_id/digital_id_web_screen.dart +++ b/lib/frontend/screens/digital_id/digital_id_web_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../backend/modules/webapp.dart'; +import '../../../core/storage/spoofing_service.dart'; import '../../../main.dart' show webAppModule, digitalIdModule; import '../../widgets/connection_status.dart'; import '../../widgets/webview_permission_prompt.dart'; @@ -175,6 +176,7 @@ class _DigitalIdWebScreenState extends State { InAppWebViewController? _controller; WebAppLaunch? _launch; String? _loadError; + String _userAgent = ''; double _progress = 0; @override @@ -189,6 +191,7 @@ class _DigitalIdWebScreenState extends State { _launch = null; }); try { + _userAgent = await SpoofingService.getWebViewUserAgent() ?? ''; final launch = await webAppModule.fetchDigitalId(); if (!mounted) return; setState(() => _launch = launch); @@ -280,6 +283,7 @@ class _DigitalIdWebScreenState extends State { mediaPlaybackRequiresUserGesture: false, useHybridComposition: true, useShouldOverrideUrlLoading: true, + userAgent: _userAgent, ), onWebViewCreated: (controller) { _controller = controller; diff --git a/lib/frontend/screens/profile/spoof_screen.dart b/lib/frontend/screens/profile/spoof_screen.dart index 25ddb9b..d2267b0 100644 --- a/lib/frontend/screens/profile/spoof_screen.dart +++ b/lib/frontend/screens/profile/spoof_screen.dart @@ -1,17 +1,18 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/config/device_presets.dart'; import '../../../core/storage/device_identity.dart'; import '../../../core/storage/spoofing_service.dart'; import '../../../core/storage/token_storage.dart'; import '../../../l10n/app_localizations.dart'; +import '../../../models/spoof_profile.dart'; import '../../../main.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/info_action_sheet.dart'; @@ -47,6 +48,9 @@ class _SpoofScreenState extends State { String _selectedDeviceType = 'ANDROID'; String _selectedArch = 'arm64-v8a'; + String _userAgent = ''; + bool _spoofingEnabled = false; + SpoofProfile? _initialProfile; SpoofingMethod _selectedMethod = SpoofingMethod.partial; bool _isLoading = true; @@ -89,49 +93,79 @@ class _SpoofScreenState extends State { Future _loadInitialData() async { setState(() => _isLoading = true); await _loadSessionIdentifiers(); - final prefs = await SharedPreferences.getInstance(); - final isSpoofingEnabled = prefs.getBool('spoofing_enabled') ?? false; - if (isSpoofingEnabled) { - _deviceNameController.text = prefs.getString('spoof_devicename') ?? ''; - _osVersionController.text = prefs.getString('spoof_osversion') ?? ''; - _screenController.text = prefs.getString('spoof_screen') ?? ''; - _timezoneController.text = prefs.getString('spoof_timezone') ?? ''; - _localeController.text = prefs.getString('spoof_locale') ?? ''; - _deviceIdController.text = prefs.getString('spoof_deviceid') ?? ''; - _appVersionController.text = - prefs.getString('spoof_appversion') ?? _hardcodedVersion; - _selectedArch = prefs.getString('spoof_arch') ?? 'arm64-v8a'; - _buildNumberController.text = - prefs.getInt('spoof_buildnumber')?.toString() ?? - '$_hardcodedBuildNumber'; - _pushDeviceTypeController.text = - prefs.getString('spoof_pushdevicetype') ?? 'GCM'; + final scope = await SpoofingService.activeScope(); + final profile = await SpoofingService.loadProfile(scope); + _initialProfile = profile; - final savedDeviceLocale = prefs.getString('spoof_devicelocale'); - if (savedDeviceLocale != null && savedDeviceLocale.isNotEmpty) { - _deviceLocaleController.text = savedDeviceLocale; - } - final savedInstanceId = prefs.getString('spoof_instanceid'); - if (savedInstanceId != null && savedInstanceId.isNotEmpty) { - _instanceIdController.text = savedInstanceId; - } - final savedClientSessionId = prefs.getInt('spoof_clientsessionid'); - if (savedClientSessionId != null) { - _clientSessionIdController.text = '$savedClientSessionId'; - } - - String savedType = prefs.getString('spoof_devicetype') ?? 'ANDROID'; - if (savedType == 'WEB') savedType = 'ANDROID'; - _selectedDeviceType = savedType; + if (profile != null && profile.enabled) { + _spoofingEnabled = true; + _applyProfileToControllers(profile); if (mounted) setState(() => _isLoading = false); } else { + _spoofingEnabled = false; await _loadDeviceData(); } } + void _applyProfileToControllers(SpoofProfile profile) { + _deviceNameController.text = profile.deviceName; + _osVersionController.text = profile.osVersion; + _screenController.text = profile.screen; + _timezoneController.text = profile.timezone; + _localeController.text = profile.locale; + _deviceIdController.text = profile.deviceId; + _appVersionController.text = + profile.appVersion.isEmpty ? _hardcodedVersion : profile.appVersion; + _selectedArch = profile.arch.isEmpty ? 'arm64-v8a' : profile.arch; + _buildNumberController.text = profile.buildNumber == 0 + ? '$_hardcodedBuildNumber' + : '${profile.buildNumber}'; + _pushDeviceTypeController.text = + profile.pushDeviceType.isEmpty ? 'GCM' : profile.pushDeviceType; + _userAgent = profile.userAgent; + + if (profile.deviceLocale.isNotEmpty) { + _deviceLocaleController.text = profile.deviceLocale; + } + if (profile.instanceId.isNotEmpty) { + _instanceIdController.text = profile.instanceId; + } + if (profile.clientSessionId != null) { + _clientSessionIdController.text = '${profile.clientSessionId}'; + } + + var type = profile.deviceType.isEmpty ? 'ANDROID' : profile.deviceType; + if (type == 'WEB') type = 'ANDROID'; + _selectedDeviceType = type; + } + + SpoofProfile _buildProfileFromControllers() { + return SpoofProfile( + enabled: _spoofingEnabled, + deviceName: _deviceNameController.text, + osVersion: _osVersionController.text, + screen: _screenController.text, + timezone: _timezoneController.text, + locale: _localeController.text, + deviceLocale: _deviceLocaleController.text, + deviceId: _deviceIdController.text, + deviceType: _selectedDeviceType, + arch: _selectedArch, + appVersion: _appVersionController.text, + buildNumber: + int.tryParse(_buildNumberController.text) ?? _hardcodedBuildNumber, + pushDeviceType: _pushDeviceTypeController.text, + instanceId: _instanceIdController.text, + clientSessionId: int.tryParse(_clientSessionIdController.text), + userAgent: _userAgent, + ); + } + Future _loadDeviceData() async { setState(() => _isLoading = true); + _userAgent = ''; + _spoofingEnabled = false; final deviceInfo = DeviceInfoPlugin(); final pixelRatio = View.of(context).devicePixelRatio; @@ -158,13 +192,8 @@ class _SpoofScreenState extends State { _screenController.text = '$densityBucket ${dpi}dpi ${size.width.round()}x${size.height.round()}'; - final prefs = await SharedPreferences.getInstance(); - var realDeviceId = prefs.getString('real_device_id'); - if (realDeviceId == null || realDeviceId.isEmpty) { - realDeviceId = _generateDeviceId(); - await prefs.setString('real_device_id', realDeviceId); - } - _deviceIdController.text = realDeviceId; + _deviceIdController.text = await DeviceIdentity.deviceId(); + _buildNumberController.text = '$_hardcodedBuildNumber'; try { final timezoneInfo = await FlutterTimezone.getLocalTimezone(); @@ -181,9 +210,23 @@ class _SpoofScreenState extends State { _selectedArch = androidInfo.supportedAbis.isNotEmpty ? androidInfo.supportedAbis.first : 'arm64-v8a'; - _buildNumberController.text = '$_hardcodedBuildNumber'; - } else { - await _applyGeneratedData(); + } else if (Platform.isIOS) { + final iosInfo = await deviceInfo.iosInfo; + _selectedDeviceType = 'IOS'; + _deviceNameController.text = iosInfo.utsname.machine; + _osVersionController.text = iosInfo.systemVersion; + } else if (Platform.isLinux) { + final linuxInfo = await deviceInfo.linuxInfo; + _deviceNameController.text = linuxInfo.prettyName; + _osVersionController.text = linuxInfo.name; + } else if (Platform.isWindows) { + final windowsInfo = await deviceInfo.windowsInfo; + _deviceNameController.text = windowsInfo.productName; + _osVersionController.text = windowsInfo.productName; + } else if (Platform.isMacOS) { + final macInfo = await deviceInfo.macOsInfo; + _deviceNameController.text = macInfo.model; + _osVersionController.text = 'macOS ${macInfo.osRelease}'; } if (mounted) setState(() => _isLoading = false); @@ -191,9 +234,7 @@ class _SpoofScreenState extends State { Future _applyGeneratedData() async { final filteredPresets = devicePresets - .where( - (p) => p.deviceType != 'WEB' && p.deviceType == _selectedDeviceType, - ) + .where((p) => p.deviceType == 'ANDROID' || p.deviceType == 'IOS') .toList(); if (filteredPresets.isEmpty) return; @@ -209,8 +250,11 @@ class _SpoofScreenState extends State { _screenController.text = preset.screen; _appVersionController.text = _hardcodedVersion; _deviceIdController.text = _generateDeviceId(); + _userAgent = preset.userAgent; + _spoofingEnabled = true; - _selectedArch = 'arm64-v8a'; + _selectedDeviceType = preset.deviceType; + _selectedArch = preset.deviceType == 'IOS' ? 'arm64' : 'arm64-v8a'; _buildNumberController.text = '$_hardcodedBuildNumber'; if (_selectedMethod == SpoofingMethod.full) { @@ -241,53 +285,21 @@ class _SpoofScreenState extends State { Future _saveSpoofingSettings() async { if (!mounted) return; - final prefs = await SharedPreferences.getInstance(); - - final oldValues = { - 'device_name': prefs.getString('spoof_devicename') ?? '', - 'os_version': prefs.getString('spoof_osversion') ?? '', - 'screen': prefs.getString('spoof_screen') ?? '', - 'timezone': prefs.getString('spoof_timezone') ?? '', - 'locale': prefs.getString('spoof_locale') ?? '', - 'device_id': prefs.getString('spoof_deviceid') ?? '', - 'device_type': prefs.getString('spoof_devicetype') ?? 'ANDROID', - 'arch': prefs.getString('spoof_arch') ?? '', - 'device_locale': prefs.getString('spoof_devicelocale') ?? '', - 'app_version': prefs.getString('spoof_appversion') ?? '', - 'build_number': prefs.getInt('spoof_buildnumber')?.toString() ?? '', - 'push_device_type': prefs.getString('spoof_pushdevicetype') ?? '', - 'instance_id': prefs.getString('spoof_instanceid') ?? '', - 'client_session_id': - prefs.getInt('spoof_clientsessionid')?.toString() ?? '', - }; - - final newValues = { - 'device_name': _deviceNameController.text, - 'os_version': _osVersionController.text, - 'screen': _screenController.text, - 'timezone': _timezoneController.text, - 'locale': _localeController.text, - 'device_id': _deviceIdController.text, - 'device_type': _selectedDeviceType, - 'arch': _selectedArch, - 'device_locale': _deviceLocaleController.text, - 'app_version': _appVersionController.text, - 'build_number': _buildNumberController.text, - 'push_device_type': _pushDeviceTypeController.text, - 'instance_id': _instanceIdController.text, - 'client_session_id': _clientSessionIdController.text, - }; - - bool otherDataChanged = false; - for (final key in oldValues.keys) { - if (oldValues[key] != newValues[key]) { - otherDataChanged = true; - break; - } + final newProfile = _buildProfileFromControllers(); + final wasActive = _initialProfile?.enabled ?? false; + final isActive = newProfile.enabled; + final bool identityChanged; + if (!wasActive && !isActive) { + identityChanged = false; + } else if (wasActive != isActive) { + identityChanged = true; + } else { + identityChanged = + jsonEncode(_initialProfile!.toJson()) != jsonEncode(newProfile.toJson()); } - if (!otherDataChanged) { - await _saveAllData(prefs); + if (!identityChanged) { + await _persistProfile(); if (mounted) Navigator.of(context).pop(); return; } @@ -333,14 +345,12 @@ class _SpoofScreenState extends State { if (!mounted || confirmed == null) return; if (confirmed == 'relogin') { - final prefs = await SharedPreferences.getInstance(); - await _saveAllData(prefs); + await _persistProfile(); await api.disconnect(); final accountId = await TokenStorage.getActiveAccountId(); if (accountId != null) { await TokenStorage.deleteToken(accountId); } - await prefs.setBool('spoofing_enabled', true); await api.connect(); if (mounted) { final navState = KometApp.navigatorKey.currentState; @@ -356,7 +366,7 @@ class _SpoofScreenState extends State { if (confirmed != 'apply') return; - await _saveAllData(prefs); + await _persistProfile(); try { await api.disconnect(); @@ -378,37 +388,11 @@ class _SpoofScreenState extends State { } } - Future _saveAllData(SharedPreferences prefs) async { - await prefs.setBool('spoofing_enabled', true); - await prefs.setString('spoof_devicename', _deviceNameController.text); - await prefs.setString('spoof_osversion', _osVersionController.text); - await prefs.setString('spoof_screen', _screenController.text); - await prefs.setString('spoof_timezone', _timezoneController.text); - await prefs.setString('spoof_locale', _localeController.text); - await prefs.setString('spoof_deviceid', _deviceIdController.text); - await prefs.setString('spoof_devicetype', _selectedDeviceType); - await prefs.setString('spoof_arch', _selectedArch); - await prefs.setString('spoof_devicelocale', _deviceLocaleController.text); - await prefs.setString('spoof_appversion', _appVersionController.text); - await prefs.setString( - 'spoof_pushdevicetype', - _pushDeviceTypeController.text, - ); - await prefs.setString('spoof_instanceid', _instanceIdController.text); - - final buildNumber = int.tryParse(_buildNumberController.text); - if (buildNumber != null) { - await prefs.setInt('spoof_buildnumber', buildNumber); - } else { - await prefs.remove('spoof_buildnumber'); - } - - final clientSessionId = int.tryParse(_clientSessionIdController.text); - if (clientSessionId != null) { - await prefs.setInt('spoof_clientsessionid', clientSessionId); - } else { - await prefs.remove('spoof_clientsessionid'); - } + Future _persistProfile() async { + final scope = await SpoofingService.activeScope(); + final profile = _buildProfileFromControllers(); + await SpoofingService.saveProfile(scope, profile); + _initialProfile = profile; } void _generateNewDeviceId() { @@ -450,6 +434,8 @@ class _SpoofScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + _buildEnableCard(), + const SizedBox(height: 16), _buildInfoCard(), const SizedBox(height: 16), _buildSpoofingMethodCard(), @@ -469,6 +455,26 @@ class _SpoofScreenState extends State { ); } + Widget _buildEnableCard() { + final l10n = AppLocalizations.of(context)!; + return Card( + child: SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + title: Text( + l10n.spoofEnableTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + _spoofingEnabled + ? l10n.spoofEnableSubtitleOn + : l10n.spoofEnableSubtitleOff, + ), + value: _spoofingEnabled, + onChanged: (value) => setState(() => _spoofingEnabled = value), + ), + ); + } + Widget _buildInfoCard() { final l10n = AppLocalizations.of(context)!; return Card( diff --git a/lib/frontend/screens/webapp/web_app_screen.dart b/lib/frontend/screens/webapp/web_app_screen.dart index dbb1b9f..f62ee31 100644 --- a/lib/frontend/screens/webapp/web_app_screen.dart +++ b/lib/frontend/screens/webapp/web_app_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../backend/modules/webapp.dart'; +import '../../../core/storage/spoofing_service.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/webview_permission_prompt.dart'; @@ -24,6 +25,7 @@ class _WebAppScreenState extends State { InAppWebViewController? _controller; WebAppLaunch? _launch; String? _loadError; + String _userAgent = ''; double _progress = 0; @override @@ -38,6 +40,7 @@ class _WebAppScreenState extends State { _launch = null; }); try { + _userAgent = await SpoofingService.getWebViewUserAgent() ?? ''; final launch = await widget.loader(); if (!mounted) return; setState(() => _launch = launch); @@ -120,6 +123,7 @@ class _WebAppScreenState extends State { transparentBackground: true, mediaPlaybackRequiresUserGesture: false, useHybridComposition: true, + userAgent: _userAgent, ), onWebViewCreated: (controller) => _controller = controller, onPermissionRequest: (controller, request) => diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 501327c..7157f69 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -58,6 +58,9 @@ "proxyInvalidHostOrPort": "Enter a valid proxy host and port (1–65535)", "spoofScreenTitle": "Session spoofing", + "spoofEnableTitle": "Device spoofing", + "spoofEnableSubtitleOn": "Enabled for this account", + "spoofEnableSubtitleOff": "Disabled — using the real device", "spoofInfoHint": "Tap \"Generate\":\n• Short tap: random preset.\n• Long press: real device data.", "spoofMethodTitle": "Spoofing method", "spoofMethodPartial": "Partial", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9cc2ae6..dac7020 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -392,6 +392,24 @@ abstract class AppLocalizations { /// **'Session spoofing'** String get spoofScreenTitle; + /// No description provided for @spoofEnableTitle. + /// + /// In en, this message translates to: + /// **'Device spoofing'** + String get spoofEnableTitle; + + /// No description provided for @spoofEnableSubtitleOn. + /// + /// In en, this message translates to: + /// **'Enabled for this account'** + String get spoofEnableSubtitleOn; + + /// No description provided for @spoofEnableSubtitleOff. + /// + /// In en, this message translates to: + /// **'Disabled — using the real device'** + String get spoofEnableSubtitleOff; + /// No description provided for @spoofInfoHint. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4cd5ef1..9c6dd7e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -161,6 +161,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get spoofScreenTitle => 'Session spoofing'; + @override + String get spoofEnableTitle => 'Device spoofing'; + + @override + String get spoofEnableSubtitleOn => 'Enabled for this account'; + + @override + String get spoofEnableSubtitleOff => 'Disabled — using the real device'; + @override String get spoofInfoHint => 'Tap \"Generate\":\n• Short tap: random preset.\n• Long press: real device data.'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index d9f7b1a..a5c4f38 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -163,6 +163,16 @@ class AppLocalizationsRu extends AppLocalizations { @override String get spoofScreenTitle => 'Подмена данных сессии'; + @override + String get spoofEnableTitle => 'Подмена устройства'; + + @override + String get spoofEnableSubtitleOn => 'Включена для этого аккаунта'; + + @override + String get spoofEnableSubtitleOff => + 'Выключена — используется реальное устройство'; + @override String get spoofInfoHint => 'Нажмите \"Сгенерировать\":\n• Короткое нажатие: случайный пресет.\n• Длинное нажатие: реальные данные.'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 7cca1be..5b373f6 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -58,6 +58,9 @@ "proxyInvalidHostOrPort": "Укажите корректный хост и порт прокси (1–65535)", "spoofScreenTitle": "Подмена данных сессии", + "spoofEnableTitle": "Подмена устройства", + "spoofEnableSubtitleOn": "Включена для этого аккаунта", + "spoofEnableSubtitleOff": "Выключена — используется реальное устройство", "spoofInfoHint": "Нажмите \"Сгенерировать\":\n• Короткое нажатие: случайный пресет.\n• Длинное нажатие: реальные данные.", "spoofMethodTitle": "Метод подмены", "spoofMethodPartial": "Частичный", diff --git a/lib/models/spoof_profile.dart b/lib/models/spoof_profile.dart new file mode 100644 index 0000000..215f253 --- /dev/null +++ b/lib/models/spoof_profile.dart @@ -0,0 +1,75 @@ +class SpoofProfile { + final bool enabled; + final String deviceName; + final String osVersion; + final String screen; + final String timezone; + final String locale; + final String deviceLocale; + final String deviceId; + final String deviceType; + final String arch; + final String appVersion; + final int buildNumber; + final String pushDeviceType; + final String instanceId; + final int? clientSessionId; + final String userAgent; + + const SpoofProfile({ + required this.enabled, + this.deviceName = '', + this.osVersion = '', + this.screen = '', + this.timezone = '', + this.locale = '', + this.deviceLocale = '', + this.deviceId = '', + this.deviceType = 'ANDROID', + this.arch = 'arm64-v8a', + this.appVersion = '', + this.buildNumber = 0, + this.pushDeviceType = 'GCM', + this.instanceId = '', + this.clientSessionId, + this.userAgent = '', + }); + + Map toJson() => { + 'enabled': enabled, + 'device_name': deviceName, + 'os_version': osVersion, + 'screen': screen, + 'timezone': timezone, + 'locale': locale, + 'device_locale': deviceLocale, + 'device_id': deviceId, + 'device_type': deviceType, + 'arch': arch, + 'app_version': appVersion, + 'build_number': buildNumber, + 'push_device_type': pushDeviceType, + 'instance_id': instanceId, + 'client_session_id': clientSessionId, + 'user_agent': userAgent, + }; + + factory SpoofProfile.fromJson(Map json) => SpoofProfile( + enabled: json['enabled'] as bool? ?? false, + deviceName: json['device_name'] as String? ?? '', + osVersion: json['os_version'] as String? ?? '', + screen: json['screen'] as String? ?? '', + timezone: json['timezone'] as String? ?? '', + locale: json['locale'] as String? ?? '', + deviceLocale: json['device_locale'] as String? ?? '', + deviceId: json['device_id'] as String? ?? '', + deviceType: json['device_type'] as String? ?? 'ANDROID', + arch: json['arch'] as String? ?? 'arm64-v8a', + appVersion: json['app_version'] as String? ?? '', + buildNumber: (json['build_number'] as num?)?.toInt() ?? 0, + pushDeviceType: json['push_device_type'] as String? ?? 'GCM', + instanceId: json['instance_id'] as String? ?? '', + clientSessionId: (json['client_session_id'] as num?)?.toInt(), + userAgent: json['user_agent'] as String? ?? '', + ); +}