From 4e500ef087e60622665dfdc7d20ad0b4413b716e Mon Sep 17 00:00:00 2001 From: klockky Date: Sat, 16 May 2026 23:36:28 +0300 Subject: [PATCH] fix(security): close critical findings #1-3 from issue #17 - TLS: validate cert chain by default; debug-menu toggle to disable - Logs: redact secrets in sender/dispatcher payloads - Identity: per-install mt_instanceid/deviceId, per-launch clientSessionId --- lib/backend/api.dart | 7 +- lib/core/storage/device_identity.dart | 49 +++++++++++++ lib/core/transport/connection.dart | 18 +++-- lib/core/transport/dispatcher.dart | 9 +-- lib/core/transport/sender.dart | 3 +- lib/core/transport/tls_config.dart | 15 ++++ lib/core/utils/log_redact.dart | 39 +++++++++++ .../screens/profile/debug_menu_screen.dart | 68 +++++++++++++++++++ lib/main.dart | 15 ++++ 9 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 lib/core/storage/device_identity.dart create mode 100644 lib/core/transport/tls_config.dart create mode 100644 lib/core/utils/log_redact.dart diff --git a/lib/backend/api.dart b/lib/backend/api.dart index eba505c..c767808 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -5,6 +5,7 @@ import '../core/config/config.dart'; import '../core/config/countries.dart'; import '../core/protocol/opcode_map.dart'; import '../core/protocol/packet.dart'; +import '../core/storage/device_identity.dart'; import '../core/storage/spoofing_service.dart'; import '../core/transport/connection.dart'; import '../core/transport/dispatcher.dart'; @@ -170,7 +171,7 @@ class Api { String timezone = timeZoneName.identifier; String locale = 'ru'; String deviceLocale = Platform.localeName.substring(0, 2); - String deviceId = 'a1b2c3d4e5f6a7b8'; + String deviceId = await DeviceIdentity.deviceId(); if (Platform.isLinux) { final linuxInfo = await deviceInfo.linuxInfo; @@ -242,8 +243,8 @@ class Api { }; final payload = { - 'mt_instanceid': '550e8400-e29b-41d4-a716-446655440000', - 'clientSessionId': 42, + 'mt_instanceid': await DeviceIdentity.instanceId(), + 'clientSessionId': DeviceIdentity.clientSessionId, 'deviceId': deviceId, 'userAgent': _userAgent, }; diff --git a/lib/core/storage/device_identity.dart b/lib/core/storage/device_identity.dart new file mode 100644 index 0000000..56bcc13 --- /dev/null +++ b/lib/core/storage/device_identity.dart @@ -0,0 +1,49 @@ +import 'dart:math'; + +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class DeviceIdentity { + static const String _instanceIdKey = 'mt_instance_id'; + static const String _deviceIdKey = 'device_id_local'; + + static final Random _rng = Random.secure(); + static int? _clientSessionId; + + static int get clientSessionId => + _clientSessionId ??= _rng.nextInt(0x7FFFFFFF) + 1; + + static Future instanceId() async { + final prefs = await SharedPreferences.getInstance(); + final existing = prefs.getString(_instanceIdKey); + if (existing != null && existing.isNotEmpty) return existing; + final generated = _uuidV4(); + await prefs.setString(_instanceIdKey, generated); + return generated; + } + + static Future deviceId() async { + final prefs = await SharedPreferences.getInstance(); + final existing = prefs.getString(_deviceIdKey); + if (existing != null && existing.isNotEmpty) return existing; + final generated = _hex(8); + await prefs.setString(_deviceIdKey, generated); + return generated; + } + + 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/core/transport/connection.dart b/lib/core/transport/connection.dart index cd0ea40..cb05ae4 100644 --- a/lib/core/transport/connection.dart +++ b/lib/core/transport/connection.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import '../config/proxy_config.dart'; import '../utils/logger.dart'; import 'proxy_connector.dart'; +import 'tls_config.dart'; import 'vpn_bypass.dart'; enum SocketState { disconnected, connecting, connected } @@ -106,11 +107,18 @@ class Connection { ? await RawSocket.connect(host, port) : await RawSocket.connect(host, port, timeout: timeout); } - return RawSecureSocket.secure( - rawSocket, - host: host, - onBadCertificate: (_) => true, - ); + final allowInsecure = await TlsConfig.isInsecureAllowed(); + if (allowInsecure) { + logger.w( + 'TLS: проверка сертификата отключена (дебаг) — соединение уязвимо к MitM', + ); + return RawSecureSocket.secure( + rawSocket, + host: host, + onBadCertificate: (_) => true, + ); + } + return RawSecureSocket.secure(rawSocket, host: host); } void write(Uint8List data) { diff --git a/lib/core/transport/dispatcher.dart b/lib/core/transport/dispatcher.dart index a97b3c8..89c6237 100644 --- a/lib/core/transport/dispatcher.dart +++ b/lib/core/transport/dispatcher.dart @@ -2,6 +2,7 @@ import 'dart:async'; import '../protocol/packet.dart'; import '../protocol/opcode_map.dart'; +import '../utils/log_redact.dart'; import '../utils/logger.dart'; typedef PacketHandler = void Function(Packet packet); @@ -53,12 +54,8 @@ class PacketDispatcher { if (packet.cmd == CmdType.ok || packet.cmd == CmdType.error || packet.cmd == CmdType.notFound) { - final payloadStr = packet.payload.toString(); - final displayPayload = packet.opcode == Opcode.login && payloadStr.length > 50 - ? '${payloadStr.substring(0, 50)}...' - : payloadStr; logger.i( - '<= {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: $displayPayload}', + '<= {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: ${redactForLog(packet.payload)}}', ); final completer = _pendingRequests.remove(packet.seq); @@ -84,7 +81,7 @@ class PacketDispatcher { } } else if (packet.isPush) { logger.i( - '<= push {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: ${packet.payload}}', + '<= push {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: ${redactForLog(packet.payload)}}', ); _pushHandlers[packet.opcode]?.call(packet); _pushController.add(packet); diff --git a/lib/core/transport/sender.dart b/lib/core/transport/sender.dart index aaa3187..0c2451a 100644 --- a/lib/core/transport/sender.dart +++ b/lib/core/transport/sender.dart @@ -1,4 +1,5 @@ import '../protocol/packet.dart'; +import '../utils/log_redact.dart'; import '../utils/logger.dart'; import 'connection.dart'; @@ -17,7 +18,7 @@ class PacketSender { final data = packPacket(opcode, payload, seq: seq); connection.write(data); logger.i( - '=> {ver: 10, cmd: 0, seq: $seq, opcode: $opcode, payload: $payload}', + '=> {ver: 10, cmd: 0, seq: $seq, opcode: $opcode, payload: ${redactForLog(payload)}}', ); return seq; } diff --git a/lib/core/transport/tls_config.dart b/lib/core/transport/tls_config.dart new file mode 100644 index 0000000..50d8639 --- /dev/null +++ b/lib/core/transport/tls_config.dart @@ -0,0 +1,15 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class TlsConfig { + static const String prefKey = 'dev_tls_insecure'; + + static Future isInsecureAllowed() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(prefKey) ?? false; + } + + static Future setInsecureAllowed(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(prefKey, value); + } +} diff --git a/lib/core/utils/log_redact.dart b/lib/core/utils/log_redact.dart new file mode 100644 index 0000000..3ad73e8 --- /dev/null +++ b/lib/core/utils/log_redact.dart @@ -0,0 +1,39 @@ +const _redacted = '***'; + +const _sensitiveSubstrings = ['password', 'token', 'phone', 'secret']; + +const _sensitiveExact = { + 'code', + 'verifycode', + 'smscode', + 'otp', + 'hint', + 'pin', + 'qrlink', + 'text', + 'msisdn', +}; + +bool _isSensitiveKey(Object? key) { + if (key is! String) return false; + final k = key.toLowerCase(); + if (_sensitiveExact.contains(k)) return true; + for (final s in _sensitiveSubstrings) { + if (k.contains(s)) return true; + } + return false; +} + +dynamic redactForLog(dynamic value) { + if (value is Map) { + final out = {}; + value.forEach((k, v) { + out[k] = _isSensitiveKey(k) ? _redacted : redactForLog(v); + }); + return out; + } + if (value is List) { + return value.map(redactForLog).toList(); + } + return value; +} diff --git a/lib/frontend/screens/profile/debug_menu_screen.dart b/lib/frontend/screens/profile/debug_menu_screen.dart index dc7de36..ffac3c3 100644 --- a/lib/frontend/screens/profile/debug_menu_screen.dart +++ b/lib/frontend/screens/profile/debug_menu_screen.dart @@ -226,6 +226,74 @@ class _DebugMenuScreenState extends State { ), ), ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: appState == null + ? const SizedBox.shrink() + : ValueListenableBuilder( + valueListenable: appState.tlsInsecureEnabled, + builder: (context, insecureOn, _) { + return Container( + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, + ), + child: Row( + children: [ + Icon( + Symbols.gpp_bad, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Отключить проверку TLS', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + 'Принимать любой сертификат сервера. ' + 'Только для отладки через MitM-прокси — ' + 'соединение становится уязвимым к ' + 'перехвату трафика', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + ), + ), + ], + ), + ), + Switch( + value: insecureOn, + onChanged: (v) { + appState.setTlsInsecureEnabled(v); + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), diff --git a/lib/main.dart b/lib/main.dart index 37d7504..59eb8ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'backend/modules/contacts.dart'; import 'backend/modules/messages.dart'; import 'core/push/push_service.dart'; import 'core/storage/app_database.dart'; +import 'core/transport/tls_config.dart'; import 'core/transport/vpn_bypass.dart'; import 'core/storage/token_storage.dart'; import 'core/utils/haptics.dart'; @@ -63,6 +64,7 @@ void main() async { final prefs = await SharedPreferences.getInstance(); final initialFpsOverlay = prefs.getBool('dev_fps_overlay') ?? false; final initialVpnBypass = prefs.getBool(VpnBypassService.prefKey) ?? false; + final initialTlsInsecure = prefs.getBool(TlsConfig.prefKey) ?? false; final initialFontId = prefs.getString(AppFonts.prefKey) ?? AppFonts.fallback.id; final initialFontScale = AppFonts.clampScale( @@ -76,6 +78,7 @@ void main() async { initialLocale: initialLocale, initialFpsOverlay: initialFpsOverlay, initialVpnBypass: initialVpnBypass, + initialTlsInsecure: initialTlsInsecure, initialFontId: initialFontId, initialFontScale: initialFontScale, initialAccentSeed: initialAccentSeed, @@ -89,6 +92,7 @@ class KometApp extends StatefulWidget { required this.initialLocale, this.initialFpsOverlay = false, this.initialVpnBypass = false, + this.initialTlsInsecure = false, required this.initialFontId, required this.initialFontScale, this.initialAccentSeed, @@ -97,6 +101,7 @@ class KometApp extends StatefulWidget { final Locale initialLocale; final bool initialFpsOverlay; final bool initialVpnBypass; + final bool initialTlsInsecure; final String initialFontId; final double initialFontScale; final Color? initialAccentSeed; @@ -130,6 +135,9 @@ class KometAppState extends State { late final ValueNotifier vpnBypassEnabled = ValueNotifier( widget.initialVpnBypass, ); + late final ValueNotifier tlsInsecureEnabled = ValueNotifier( + widget.initialTlsInsecure, + ); late final ValueNotifier fontScale = ValueNotifier( widget.initialFontScale, ); @@ -216,6 +224,7 @@ class KometAppState extends State { _profileUpdateController.close(); fpsOverlayEnabled.dispose(); vpnBypassEnabled.dispose(); + tlsInsecureEnabled.dispose(); fontScale.dispose(); accentSeed.dispose(); super.dispose(); @@ -235,6 +244,12 @@ class KometAppState extends State { await prefs.setBool(VpnBypassService.prefKey, value); } + Future setTlsInsecureEnabled(bool value) async { + if (tlsInsecureEnabled.value == value) return; + tlsInsecureEnabled.value = value; + await TlsConfig.setInsecureAllowed(value); + } + Future applyLocale(Locale locale) async { if (!AppLocalizations.supportedLocales.any( (l) => l.languageCode == locale.languageCode,