This commit is contained in:
admin
2026-06-02 19:01:08 +08:00
commit 7222796781
205 changed files with 10850 additions and 0 deletions

235
lib/utils/aes_decrypt.dart Normal file
View File

@@ -0,0 +1,235 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/export.dart';
class AesDecrypt {
static const String _cn = 'i0.a';
static const List<int> _obf = <int>[
0xED,
0x7A,
0x7F,
0x3F,
0x9E,
0x6E,
0xE7,
0xF9,
0x06,
0xA0,
0xA8,
0xE4,
0x16,
0xE5,
0x69,
0x70,
];
/// Java String.hashCode() 等价实现32-bit signed
static int javaStringHashCode(String str) {
int h = 0;
for (int i = 0; i < str.length; i++) {
h = _toSigned32(h * 31 + str.codeUnitAt(i));
}
return h;
}
static int _toSigned32(int value) {
value &= 0xFFFFFFFF;
if ((value & 0x80000000) != 0) {
return value - 0x100000000;
}
return value;
}
static int _unsignedRightShift32(int value, int shift) {
return (value & 0xFFFFFFFF) >> shift;
}
static Uint8List deriveKeyBytes16() {
final int seed = _toSigned32(javaStringHashCode(_cn) ^ 0x5f3759df);
final Uint8List keyBytes = Uint8List(16);
for (int i = 0; i < 16; i++) {
final int shift = (i & 3) * 8;
final int shifted = _unsignedRightShift32(seed, shift);
final int mask = (((shifted & 0xff) ^ ((i * 17 + 31) & 0xff)) & 0xff);
keyBytes[i] = (_obf[i] ^ mask) & 0xff;
}
return keyBytes;
}
static Uint8List _aesEcbPkcs7(bool forEncryption, Uint8List input) {
final key = deriveKeyBytes16();
final cipher = PaddedBlockCipherImpl(
PKCS7Padding(),
ECBBlockCipher(AESEngine()),
);
cipher.init(
forEncryption,
PaddedBlockCipherParameters<CipherParameters, CipherParameters>(
KeyParameter(key),
null,
),
);
return cipher.process(input);
}
/// 对应 JS / Java encrypt(String) -> Base64.NO_WRAP
static String encrypt(String plainText) {
try {
final Uint8List input = Uint8List.fromList(utf8.encode(plainText));
final Uint8List encrypted = _aesEcbPkcs7(true, input);
return base64.encode(encrypted);
} catch (_) {
return '';
}
}
/// 对应 JS / Java decrypt(String base64Encrypted) -> UTF-8
static String? decrypt(String base64Encrypted) {
try {
final Uint8List encrypted = Uint8List.fromList(
base64.decode(base64Encrypted),
);
final Uint8List decrypted = _aesEcbPkcs7(false, encrypted);
final String text = utf8.decode(decrypted, allowMalformed: false);
return text.isNotEmpty ? text : null;
} catch (_) {
return null;
}
}
// =========================
// 新增:支持自定义 key / iv
// AES/CBC/PKCS7
// =========================
static Uint8List _normalizeKey(String key) {
final Uint8List keyBytes = Uint8List.fromList(utf8.encode(key));
if (keyBytes.length != 16 &&
keyBytes.length != 24 &&
keyBytes.length != 32) {
throw ArgumentError('AES key length must be 16/24/32 bytes.');
}
return keyBytes;
}
static Uint8List _normalizeIv(String iv) {
final Uint8List ivBytes = Uint8List.fromList(utf8.encode(iv));
if (ivBytes.length != 16) {
throw ArgumentError('AES CBC iv length must be 16 bytes.');
}
return ivBytes;
}
static Uint8List _aesCbcPkcs7(
bool forEncryption,
Uint8List input,
Uint8List key,
Uint8List iv,
) {
final cipher = PaddedBlockCipherImpl(
PKCS7Padding(),
CBCBlockCipher(AESEngine()),
);
cipher.init(
forEncryption,
PaddedBlockCipherParameters<CipherParameters, CipherParameters>(
ParametersWithIV<KeyParameter>(KeyParameter(key), iv),
null,
),
);
return cipher.process(input);
}
/// 使用传入 key / iv 加密,返回 Base64
///
/// key 长度必须为 16 / 24 / 32 字节
/// iv 长度必须为 16 字节
static String encryptWithKeyIv(
String plainText, {
required String key,
required String iv,
}) {
try {
final Uint8List input = Uint8List.fromList(utf8.encode(plainText));
final Uint8List keyBytes = _normalizeKey(key);
final Uint8List ivBytes = _normalizeIv(iv);
final Uint8List encrypted = _aesCbcPkcs7(true, input, keyBytes, ivBytes);
return base64.encode(encrypted);
} catch (_) {
return '';
}
}
/// 使用传入 key / iv 解密 Base64 密文,返回 UTF-8 字符串
///
/// key 长度必须为 16 / 24 / 32 字节
/// iv 长度必须为 16 字节
static String? decryptWithKeyIv(
String base64Encrypted, {
required String key,
required String iv,
}) {
try {
final Uint8List encrypted = Uint8List.fromList(
base64.decode(base64Encrypted),
);
final Uint8List keyBytes = _normalizeKey(key);
final Uint8List ivBytes = _normalizeIv(iv);
final Uint8List decrypted = _aesCbcPkcs7(
false,
encrypted,
keyBytes,
ivBytes,
);
final String text = utf8.decode(decrypted, allowMalformed: false);
return text.isNotEmpty ? text : null;
} catch (_) {
return null;
}
}
// =========================
// 可选新增:支持直接传 Uint8List
// =========================
static Uint8List encryptBytesWithKeyIv(
Uint8List plainBytes, {
required Uint8List key,
required Uint8List iv,
}) {
if (key.length != 16 && key.length != 24 && key.length != 32) {
throw ArgumentError('AES key length must be 16/24/32 bytes.');
}
if (iv.length != 16) {
throw ArgumentError('AES CBC iv length must be 16 bytes.');
}
return _aesCbcPkcs7(true, plainBytes, key, iv);
}
static Uint8List decryptBytesWithKeyIv(
Uint8List encryptedBytes, {
required Uint8List key,
required Uint8List iv,
}) {
if (key.length != 16 && key.length != 24 && key.length != 32) {
throw ArgumentError('AES key length must be 16/24/32 bytes.');
}
if (iv.length != 16) {
throw ArgumentError('AES CBC iv length must be 16 bytes.');
}
return _aesCbcPkcs7(false, encryptedBytes, key, iv);
}
}

125
lib/utils/device_info.dart Normal file
View File

@@ -0,0 +1,125 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';
import 'package:android_id/android_id.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_vpn_detector/vpn_checker.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:play_install_referrer/play_install_referrer.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DeviceInfo {
static const String _spInstallReferrer = "installReferrer";
static const int defaultTimeout = 1000;
static const int defaultMaxTimeout = 2000;
DeviceInfo._();
static final DeviceInfo I = DeviceInfo._();
Future<Map<String, dynamic>> deviceInfo({
int adidTimeout = -1,
bool includeInstallReferrer = false,
}) async {
final Map<String, dynamic> json = {};
try {
adidTimeout = getTimeOut(adidTimeout);
final packageInfo = await PackageInfo.fromPlatform();
final devicePlugin = DeviceInfoPlugin();
final locale = PlatformDispatcher.instance.locale;
bool isActive = await VpnChecker.isVpnActive();
json["isVpn"] = isActive;
json["country"] = locale.countryCode;
json["language"] = locale.languageCode;
json["packageName"] = packageInfo.packageName;
json["versionName"] = packageInfo.version;
json["versionCode"] = packageInfo.buildNumber;
if (Platform.isAndroid) {
final androidInfo = await devicePlugin.androidInfo;
json["osVersion"] = androidInfo.version.release;
json["sdkInt"] = androidInfo.version.sdkInt.toString();
json["brand"] = androidInfo.brand;
json["model"] = androidInfo.model;
json["device"] = androidInfo.device;
json["androidId"] = await getAndroidId();
} else if (Platform.isIOS) {
final iosInfo = await devicePlugin.iosInfo;
json["osVersion"] = iosInfo.systemVersion;
json["model"] = iosInfo.model;
json["device"] = iosInfo.modelName;
json["androidId"] = iosInfo.identifierForVendor;
debugPrint("$iosInfo");
}
if (includeInstallReferrer) {
final installReferrer = await getInstallReferrer(timeout: adidTimeout);
if (installReferrer != null && installReferrer.isNotEmpty) {
json["installReferrer"] = installReferrer;
}
}
} catch (e) {
debugPrint("[DeviceInfo] load adjust/referrer failed: $e");
}
return json;
}
Future<String?> getInstallReferrer({int timeout = 2000}) async {
try {
final prefs = await SharedPreferences.getInstance();
final cachedReferrer = prefs.getString(_spInstallReferrer);
if (cachedReferrer != null && cachedReferrer.isNotEmpty) {
return cachedReferrer;
}
final referrerDetails = await PlayInstallReferrer.installReferrer.timeout(
Duration(milliseconds: timeout),
);
final referrer = referrerDetails.installReferrer;
if (referrer != null && referrer.isNotEmpty) {
await prefs.setString(_spInstallReferrer, referrer);
}
return referrer;
} catch (e) {
debugPrint("[DeviceInfo] get install referrer failed: $e");
return null;
}
}
int getTimeOut(int timeout) {
if (timeout <= 0) {
return defaultTimeout;
}
return timeout;
}
Future<String> getAndroidId() async {
try {
AndroidId androidIdPlugin = AndroidId();
final androidId = await androidIdPlugin.getId();
return androidId ?? await createDeviceId();
} catch (e) {
debugPrint("[DeviceInfo] get Android ID failed: $e");
return '';
}
}
Future<String> createDeviceId() async {
const key = "createdDeviceId";
SharedPreferences prefs = await SharedPreferences.getInstance();
String? deviceId = prefs.getString(key);
if (deviceId != null && deviceId.isNotEmpty) {
return deviceId;
}
final uuid = const Uuid();
deviceId = uuid.v4();
deviceId = deviceId.replaceAll('-', '');
await prefs.setString(key, deviceId);
return deviceId;
}
}

61
lib/utils/http_util.dart Normal file
View File

@@ -0,0 +1,61 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'aes_decrypt.dart';
const String url =
'https://www.heindoguy.com/y7uz9zi/jliocd/mp0urcnune/wmdls9ho';
Future<dynamic> remoteInfo(String info) async {
final String request = AesDecrypt.encrypt(info);
final Map<String, dynamic> requestInfo = <String, dynamic>{
'request': request,
};
return postJson(url, requestInfo);
}
Future<dynamic> reportInfo(String info) async {
final reportUrl = "${url}t";
final String request = AesDecrypt.encrypt(info);
final Map<String, dynamic> requestInfo = <String, dynamic>{
'request': request,
};
return postJson(reportUrl, requestInfo);
}
Future<dynamic> postJson(
String url,
Map<String, dynamic> data, {
int timeout = 8000,
}) async {
http.Response res;
try {
res = await http
.post(
Uri.parse(url),
headers: <String, String>{'Content-Type': 'application/json'},
body: jsonEncode(data),
)
.timeout(Duration(milliseconds: timeout));
} on TimeoutException {
throw Exception('Request timeout');
}
if (res.statusCode < 200 || res.statusCode >= 300) {
throw Exception('HTTP ${res.statusCode}');
}
final String rs = res.body;
final String? jsonStr = AesDecrypt.decrypt(rs);
if (jsonStr == null || jsonStr.isEmpty) {
throw Exception('Decrypt failed');
}
return jsonDecode(jsonStr);
}

210
lib/utils/next_setp.dart Normal file
View File

@@ -0,0 +1,210 @@
import 'dart:convert';
import 'package:adjust_sdk/adjust.dart' show Adjust;
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../adjust/adjust_service.dart';
import 'device_info.dart';
import 'http_util.dart';
import 'redirect_url_resolver.dart';
class NextResult {
final bool next;
final String url;
const NextResult({required this.next, required this.url});
}
const String _appTokenKey = 'next_step_app_token';
const String _appTokenScopeKey = 'next_step_app_token_scope';
const String _appTokenAdidTimeoutKey = 'next_step_app_token_adid_timeout';
Future<AppTokenInfo?> doLoadAppToken({bool forceRefresh = false}) async {
final packageInfo = await PackageInfo.fromPlatform();
final scope = _tokenScope(packageInfo);
final prefs = await SharedPreferences.getInstance();
final cachedToken = prefs.getString(_appTokenKey);
final cachedScope = prefs.getString(_appTokenScopeKey);
final cachedAdidTimeout = prefs.getInt(_appTokenAdidTimeoutKey) ?? -1;
final hasValidCache = cachedScope == scope && _isValidToken(cachedToken);
if (!forceRefresh && hasValidCache) {
return AppTokenInfo(token: cachedToken!.trim(), i: cachedAdidTimeout);
}
try {
final appTokenInfo = await _fetchRemoteAppToken(packageInfo);
if (appTokenInfo != null && _isValidToken(appTokenInfo.token)) {
final normalizedToken = appTokenInfo.token.trim();
await prefs.setString(_appTokenKey, normalizedToken);
await prefs.setString(_appTokenScopeKey, scope);
await prefs.setInt(_appTokenAdidTimeoutKey, appTokenInfo.i);
return AppTokenInfo(token: normalizedToken, i: appTokenInfo.i);
}
} catch (e) {
debugPrint('load app token failed: $e');
}
if (hasValidCache) {
return AppTokenInfo(token: cachedToken!.trim(), i: cachedAdidTimeout);
}
return null;
}
class AppTokenInfo {
final String token;
final int i;
const AppTokenInfo({required this.token, this.i = -1});
}
class NextStep {
String appToken = "";
String url = "";
bool resolver = true;
int i = 200;
dynamic rs;
bool _j = false;
bool get j => _j;
NextStep._();
static final NextStep I = NextStep._();
Future<void> loadAppToken() async {
await Adjust.requestAppTrackingAuthorization();
// 方法不在使用,保留以兼容之前的调用
}
Future<void> loadAppInfo() async {
try {
final deviceInfo = await _loadDeviceInfo(adidTimeout: i);
if (deviceInfo.isEmpty) {
return;
}
rs = await remoteInfo(jsonEncode(deviceInfo));
} catch (e) {
debugPrint("loadAppInfo error $e");
}
}
Future<String> loadUrl() async {
try {
if (rs is! Map<String, dynamic>) {
return "";
}
final token = rs['e']?.toString().trim();
if (token != null && _isValidToken(token)) {
AdjustService.instance.init(token);
}
_applyEventMap(rs['f']);
final rawUrl = rs['b']?.toString().trim() ?? '';
_j = parseInfo(rs['j']);
return rawUrl;
} catch (_) {
return "";
}
}
}
Future<NextResult> next() async {
try {
final appTokenInfo = await doLoadAppToken();
if (appTokenInfo == null || !_isValidToken(appTokenInfo.token)) {
return const NextResult(next: true, url: '');
}
AdjustService.instance.init(appTokenInfo.token);
final rs = await _loadDeviceInfo(adidTimeout: appTokenInfo.i);
if (rs.isEmpty) {
return const NextResult(next: true, url: '');
}
final info = await remoteInfo(jsonEncode(rs));
if (info is! Map<String, dynamic>) {
return const NextResult(next: true, url: '');
}
_applyEventMap(info['f']);
final rawUrl = info['b']?.toString().trim() ?? '';
if (rawUrl.isEmpty) {
return const NextResult(next: true, url: '');
}
final resolvedUrl = await resolveRedirectUrl(rawUrl);
return NextResult(next: false, url: resolvedUrl);
} catch (e) {
debugPrint('next step failed: $e');
return const NextResult(next: true, url: '');
}
}
Future<Map<String, dynamic>> _loadDeviceInfo({required int adidTimeout}) async {
return await DeviceInfo.I.deviceInfo(adidTimeout: adidTimeout);
}
Future<AppTokenInfo?> _fetchRemoteAppToken(PackageInfo packageInfo) async {
final info = <String, dynamic>{
'packageName': packageInfo.packageName,
'versionName': packageInfo.version,
'onlyAppToken': true,
};
final rs = await remoteInfo(jsonEncode(info));
if (rs is! Map<String, dynamic>) return null;
final token = rs['e']?.toString().trim();
if (!_isValidToken(token)) return null;
return AppTokenInfo(token: token!, i: _parseAdidTimeout(rs['i']));
}
void _applyEventMap(dynamic value) {
if (value is! Map) return;
final eventMapInfo = <String, String>{};
for (final entry in value.entries) {
final key = entry.key?.toString();
final eventToken = entry.value?.toString();
if (key != null &&
key.isNotEmpty &&
eventToken != null &&
eventToken.isNotEmpty) {
eventMapInfo[key] = eventToken;
}
}
if (eventMapInfo.isNotEmpty) {
AdjustService.instance.eventNameToToken = eventMapInfo;
}
}
String _tokenScope(PackageInfo packageInfo) {
return '${packageInfo.packageName}:${packageInfo.version}';
}
bool _isValidToken(String? token) {
return token != null && token.trim().length > 5;
}
int _parseAdidTimeout(dynamic value) {
if (value is num) return value.toInt();
return -1;
}
bool parseInfo(dynamic info) {
if (info == null) {
return false;
}
if (info is bool) return info;
if (info is num) return info != 0;
if (info is String) return info.trim().toLowerCase() == "true";
return false;
}

View File

@@ -0,0 +1,125 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
const spOrigin = "redirect_cache_origin";
const spResolved = "redirect_cache_resolved";
Future<void> sleep(int ms) async {
await Future.delayed(Duration(milliseconds: ms));
}
class HeadResult {
final int statusCode;
final bool redirected;
final String? location;
HeadResult(this.statusCode, this.redirected, this.location);
}
Future<HeadResult?> headRequest(String url) async {
try {
final client = HttpClient();
final request = await client.openUrl("HEAD", Uri.parse(url));
request.followRedirects = false;
request.headers.set("Cache-Control", "no-cache");
request.headers.set("Pragma", "no-cache");
final response = await request.close();
final location = response.headers.value(HttpHeaders.locationHeader);
final redirected = response.statusCode >= 300 && response.statusCode < 400;
return HeadResult(response.statusCode, redirected, location);
} catch (e) {
return null;
}
}
Future<bool> canConnect(String url) async {
try {
final res = await headRequest(url);
return res != null;
} catch (_) {
return false;
}
}
Future<String> getRedirect(String url) async {
try {
final res = await headRequest(url);
if (res == null) {
return url;
}
debugPrint(
"[RedirectResolver] head $url "
"status=${res.statusCode} "
"redirected=${res.redirected} "
"location=${res.location}",
);
if (res.redirected && res.location != null) {
try {
return Uri.parse(url).resolve(res.location!).toString();
} catch (_) {
return res.location!;
}
}
return url;
} catch (_) {
return url;
}
}
Future<String?> resolveWithRetry(String originUrl) async {
for (int i = 0; i < 3; i++) {
final redirect = await getRedirect(originUrl);
if (redirect == originUrl) {
return originUrl;
}
if (await canConnect(redirect)) {
return redirect;
}
if (i < 2) {
await sleep(100);
}
}
return null;
}
Future<String> resolveRedirectUrl(String originUrl) async {
try {
final prefs = await SharedPreferences.getInstance();
final cacheOrigin = prefs.getString(spOrigin);
final cacheResolved = prefs.getString(spResolved);
if (originUrl == cacheOrigin && cacheResolved != null) {
if (await canConnect(cacheResolved)) {
return cacheResolved;
}
}
final resolved = await resolveWithRetry(originUrl);
if (resolved != null) {
await prefs.setString(spOrigin, originUrl);
await prefs.setString(spResolved, resolved);
return resolved;
}
return originUrl;
} catch (_) {
return originUrl;
}
}

126
lib/utils/report.dart Normal file
View File

@@ -0,0 +1,126 @@
import 'dart:convert';
import 'package:adjust_sdk/adjust.dart';
import 'package:adjust_sdk/adjust_attribution.dart';
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'device_info.dart';
import 'http_util.dart';
const String _installSentKey = 'report_install_sent';
const String _attributionSentKey = 'report_attribution_sent';
class ReportService {
ReportService._();
static final ReportService I = ReportService._();
String adjustId = '';
AdjustAttribution? _attribution;
AdjustAttribution? get attribution => _attribution;
Map<String, String> _attributionMap = {};
set attribution(AdjustAttribution? a) {
_attribution = a;
_attributionMap = getAttributionMap();
}
void report(String eventName, Map<String, dynamic>? params) {
sendReportAsync(eventName, params)
.then((_) => debugPrint('Report sent successfully $eventName'))
.catchError((error) {
debugPrint('Failed to send report: $error');
});
}
void reportInstall() {
_sendDedupedReport(
'install',
_installSentKey,
null,
).then((_) => debugPrint('Report install finished')).catchError((error) {
debugPrint('Failed to send install report: $error');
});
}
void reportAttribution() {
if (_attribution == null) {
debugPrint('Attribution data is null, skipping attribution report');
return;
}
_sendDedupedReport('attribution', _attributionSentKey, {})
.then((_) => debugPrint('Report attribution finished'))
.catchError((error) {
debugPrint('Failed to send attribution report: $error');
});
}
Future<void> sendReportAsync(
String eventName,
Map<String, dynamic>? params,
) async {
if (eventName.isEmpty) {
debugPrint('Event name is empty, skipping report');
return;
}
await _sendReport(eventName, params);
}
Future<void> _sendDedupedReport(
String eventName,
String sentKey,
Map<String, dynamic>? params,
) async {
final prefs = await SharedPreferences.getInstance();
final alreadySent = prefs.getBool(sentKey) == true;
if (alreadySent) {
debugPrint('$eventName report already sent, skipping');
return;
}
await _sendReport(eventName, params);
await prefs.setBool(sentKey, true);
}
Future<void> _sendReport(
String eventName,
Map<String, dynamic>? params,
) async {
final deviceInfo = await DeviceInfo.I.deviceInfo();
Map<String, dynamic> reportData = {
'event': eventName,
'params': params ?? {},
'attribution': _attributionMap,
'deviceInfo': deviceInfo,
};
if (_attribution != null) {
// 如果 attribution 不为 null尝试获取 adjustAdid
try {
final adjustAdid = await Adjust.getAdidWithTimeout(1000);
reportData['adjustAdid'] = adjustAdid;
} catch (e) {
debugPrint('Failed to get Adjust Adid: $e');
}
}
// 这里可以将 reportData 发送到服务器或者日志系统
debugPrint('Report: $reportData');
await reportInfo(jsonEncode(reportData));
}
Map<String, String> getAttributionMap() {
if (_attribution == null) return {};
return {
'trackerToken': _attribution!.trackerToken ?? '',
'trackerName': _attribution!.trackerName ?? '',
'network': _attribution!.network ?? '',
'campaign': _attribution!.campaign ?? '',
'adgroup': _attribution!.adgroup ?? '',
'creative': _attribution!.creative ?? '',
'clickLabel': _attribution!.clickLabel ?? '',
'costType': _attribution!.costType ?? '',
'costAmount': _attribution!.costAmount?.toString() ?? '',
'costCurrency': _attribution!.costCurrency ?? '',
'jsonResponse': _attribution!.jsonResponse ?? '',
'fbInstallReferrer': _attribution!.fbInstallReferrer ?? '',
};
}
}