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
@@ -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) =>