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
+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)));