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
This commit is contained in:
@@ -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<String> 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<String> 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<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)}';
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class TlsConfig {
|
||||
static const String prefKey = 'dev_tls_insecure';
|
||||
|
||||
static Future<bool> isInsecureAllowed() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(prefKey) ?? false;
|
||||
}
|
||||
|
||||
static Future<void> setInsecureAllowed(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(prefKey, value);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user