feat(spoof): пер-аккаунтный спуф с тумблером и прокидыванием в веб-аппы

- спуф теперь свой на каждый аккаунт, вмораживается при входе
- тумблер вкл/выкл в настройках (по умолчанию выключен)
- новый аккаунт добавляется со своим включённым спуфом (другое устройство)
- UA спуфа прокинут в Сферум, Цифровой ID (вебвью + HTTP)
- генерация выбирает Android или iOS; iOS отдаёт deviceType IOS
- экран спуфа показывает текущую личность, а не свежесгенерированную
This commit is contained in:
klockky
2026-06-20 15:15:09 +00:00
parent 62b5d1b9f3
commit d055de268b
14 changed files with 535 additions and 150 deletions
+1
View File
@@ -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) {
+10
View File
@@ -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<dynamic, dynamic>());
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<void> 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<void> removeAccount(int accountId) async {
await AppDatabase.deleteAccount(accountId);
await TokenStorage.deleteAccount(accountId);
await SpoofingService.clearAccountSpoof(accountId);
logger.i('Аккаунт $accountId удалён локально');
}
+30 -5
View File
@@ -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<String> _resolveUserAgent() async {
final spoofed = await SpoofingService.getWebViewUserAgent();
if (spoofed != null && spoofed.isNotEmpty) return spoofed;
return _realUserAgent ??= await _buildRealUserAgent();
}
Future<String> _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<dynamic> _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)));
+226 -17
View File
@@ -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<String> _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<String> activeScope() async {
final id = await TokenStorage.getActiveAccountId();
return id?.toString() ?? pendingScope;
}
static Future<SpoofProfile?> loadProfile(String scope) async {
final prefs = await SharedPreferences.getInstance();
return _read(prefs, scope);
}
static Future<void> saveProfile(String scope, SpoofProfile profile) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_profileKey(scope), jsonEncode(profile.toJson()));
}
static Future<void> clearAccountSpoof(int accountId) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_profileKey('$accountId'));
}
static Future<void> 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<SpoofProfile> prepareNewAccountSpoof(
List<int> existingAccountIds,
) async {
final prefs = await SharedPreferences.getInstance();
final used = <String>{};
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<Map<String, dynamic>?> 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<String?> 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<SpoofProfile?> _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<String, dynamic>);
} catch (_) {}
}
if (scope != pendingScope) {
return _migrateLegacy(prefs, scope);
}
return null;
}
static Future<SpoofProfile?> _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<int>.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)}';
}
}
@@ -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<DigitalIdWebScreen> {
InAppWebViewController? _controller;
WebAppLaunch? _launch;
String? _loadError;
String _userAgent = '';
double _progress = 0;
@override
@@ -189,6 +191,7 @@ class _DigitalIdWebScreenState extends State<DigitalIdWebScreen> {
_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<DigitalIdWebScreen> {
mediaPlaybackRequiresUserGesture: false,
useHybridComposition: true,
useShouldOverrideUrlLoading: true,
userAgent: _userAgent,
),
onWebViewCreated: (controller) {
_controller = controller;
+134 -128
View File
@@ -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<SpoofScreen> {
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<SpoofScreen> {
Future<void> _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<void> _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<SpoofScreen> {
_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<SpoofScreen> {
_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<SpoofScreen> {
Future<void> _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<SpoofScreen> {
_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<SpoofScreen> {
Future<void> _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<SpoofScreen> {
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<SpoofScreen> {
if (confirmed != 'apply') return;
await _saveAllData(prefs);
await _persistProfile();
try {
await api.disconnect();
@@ -378,37 +388,11 @@ class _SpoofScreenState extends State<SpoofScreen> {
}
}
Future<void> _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<void> _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<SpoofScreen> {
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<SpoofScreen> {
);
}
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(
@@ -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<WebAppScreen> {
InAppWebViewController? _controller;
WebAppLaunch? _launch;
String? _loadError;
String _userAgent = '';
double _progress = 0;
@override
@@ -38,6 +40,7 @@ class _WebAppScreenState extends State<WebAppScreen> {
_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<WebAppScreen> {
transparentBackground: true,
mediaPlaybackRequiresUserGesture: false,
useHybridComposition: true,
userAgent: _userAgent,
),
onWebViewCreated: (controller) => _controller = controller,
onPermissionRequest: (controller, request) =>
+3
View File
@@ -58,6 +58,9 @@
"proxyInvalidHostOrPort": "Enter a valid proxy host and port (165535)",
"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",
+18
View File
@@ -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:
+9
View File
@@ -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.';
+10
View File
@@ -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• Длинное нажатие: реальные данные.';
+3
View File
@@ -58,6 +58,9 @@
"proxyInvalidHostOrPort": "Укажите корректный хост и порт прокси (1–65535)",
"spoofScreenTitle": "Подмена данных сессии",
"spoofEnableTitle": "Подмена устройства",
"spoofEnableSubtitleOn": "Включена для этого аккаунта",
"spoofEnableSubtitleOff": "Выключена — используется реальное устройство",
"spoofInfoHint": "Нажмите \"Сгенерировать\":\n• Короткое нажатие: случайный пресет.\n• Длинное нажатие: реальные данные.",
"spoofMethodTitle": "Метод подмены",
"spoofMethodPartial": "Частичный",
+75
View File
@@ -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<String, dynamic> 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<String, dynamic> 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? ?? '',
);
}