Datenschutz: Log-Bereinigung vor Server-Upload (Scrubber + Quelle)
Diagnose-Logs werden zentral bereinigt, bevor sie an logs.baka-net.de gehen. Lokaler debugPrint bleibt vollständig. - Freitext-Scrubber (LoggerService.scrubText) für message + stackTrace: maskiert user=/pw=/password=/token=/secret=/url= sowie Datei-Pfade, nackte URLs, Bearer-Token, JWTs und lange Hex-IDs. - stackTrace auf 8 Zeilen gekürzt (capFrames). - deviceModel nur noch grobe Plattform (android/ios) statt Hersteller+Modell; device_info_plus im Logger entfernt. - Quelle entschärft: settings_screen loggte Navidrome-Login mit Klartext user=/url= — jetzt nur neutraler Status inkl. pwLeer-Flag. - Neue Tests: test/logger_service_test.dart (9). 79/79 grün, analyze sauber. Opt-in/Einwilligung bleibt separater Folgeschritt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tr2UwurtxyJUpWpBSeVmBm
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
61e28d128f
commit
d83f8c8c54
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -42,21 +41,61 @@ class LoggerService extends ChangeNotifier {
|
||||
|
||||
Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
final info = await deviceInfo.androidInfo;
|
||||
_deviceModel = '${info.manufacturer} ${info.model}';
|
||||
} else {
|
||||
_deviceModel = 'Unknown Device';
|
||||
}
|
||||
} catch (e) {
|
||||
_deviceModel = 'Unknown Device';
|
||||
}
|
||||
// Datenschutz: nur grobe Plattform senden statt Hersteller+Modell
|
||||
// (das war ein Geräte-Fingerprint). Reicht für Bug-Triage.
|
||||
_deviceModel = Platform.isAndroid
|
||||
? 'android'
|
||||
: (Platform.isIOS ? 'ios' : Platform.operatingSystem);
|
||||
_initialized = true;
|
||||
debugPrint('✅ LoggerService initialized for $_deviceModel');
|
||||
}
|
||||
|
||||
// ─── Datenschutz: Bereinigung vor dem Server-Upload ───
|
||||
|
||||
static const int _maxFrames = 8;
|
||||
|
||||
/// Maskiert sensible Fragmente in Log-Freitext (message/stackTrace), bevor
|
||||
/// er an den Server geht: sensible `key=value`-Fragmente, Datei-Pfade,
|
||||
/// nackte URLs, Bearer-Token, JWTs und lange Hex-IDs.
|
||||
@visibleForTesting
|
||||
static String scrubText(String? roh) {
|
||||
if (roh == null || roh.isEmpty) return roh ?? '';
|
||||
var s = roh.replaceAllMapped(
|
||||
RegExp(r'\b(user|username|benutzer|pw|pass|passwort|password|token|secret|url|serverurl)=([^\s&]+)',
|
||||
caseSensitive: false),
|
||||
(m) => '${m.group(1)}=<redacted>',
|
||||
);
|
||||
s = s
|
||||
.replaceAll(RegExp(r'/(storage|data|home|Users|var)/\S+'), '<path>')
|
||||
.replaceAll(RegExp(r'https?://\S+'), '<url>')
|
||||
.replaceAll(RegExp(r'Bearer\s+[A-Za-z0-9._-]+'), 'Bearer <token>')
|
||||
.replaceAll(RegExp(r'eyJ[A-Za-z0-9._-]{10,}'), '<jwt>')
|
||||
.replaceAll(RegExp(r'\b[A-Fa-f0-9]{16,}\b'), '<hex>');
|
||||
return s;
|
||||
}
|
||||
|
||||
/// Kürzt einen (bereits gescrubbten) Stack-Trace auf wenige Zeilen.
|
||||
@visibleForTesting
|
||||
static String capFrames(String s) {
|
||||
final zeilen = s.split('\n');
|
||||
if (zeilen.length <= _maxFrames) return s;
|
||||
final rest = zeilen.length - _maxFrames;
|
||||
return '${zeilen.take(_maxFrames).join('\n')}\n… ($rest weitere Zeilen entfernt)';
|
||||
}
|
||||
|
||||
/// Bereinigter Upload-Payload eines Eintrags. Der lokale Puffer/debugPrint
|
||||
/// bleibt unberührt — die Maskierung greift nur auf dem Versand-Pfad.
|
||||
@visibleForTesting
|
||||
static Map<String, dynamic> sanitizedJson(LogEntry l) => {
|
||||
'level': l.level,
|
||||
'message': scrubText(l.message),
|
||||
'stackTrace':
|
||||
l.stackTrace == null ? null : capFrames(scrubText(l.stackTrace)),
|
||||
'timestamp': l.timestamp.toIso8601String(),
|
||||
'userAgent': l.userAgent,
|
||||
'deviceModel': l.deviceModel,
|
||||
};
|
||||
|
||||
/// Logge einen Fehler (wird sofort zum Server geschickt)
|
||||
Future<void> error(String message, [Object? error, StackTrace? stackTrace]) async {
|
||||
final entry = LogEntry(
|
||||
@@ -114,7 +153,7 @@ class LoggerService extends ChangeNotifier {
|
||||
Uri.parse(_logServerUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'logs': logsToUpload.map((l) => l.toJson()).toList(),
|
||||
'logs': logsToUpload.map(sanitizedJson).toList(),
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
}),
|
||||
).timeout(const Duration(seconds: 10));
|
||||
|
||||
@@ -263,23 +263,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
: () async {
|
||||
setDialogState(() => verbindet = true);
|
||||
logger.info(
|
||||
'Login-Versuch: url=${urlCtrl.text} user=$selectedUser '
|
||||
'pwLeer=${passCtrl.text.isEmpty}');
|
||||
'Navidrome Login-Versuch (pwLeer=${passCtrl.text.isEmpty})');
|
||||
_navidrome.setCredentials(
|
||||
urlCtrl.text, selectedUser, passCtrl.text);
|
||||
final ok = await _navidrome.ping();
|
||||
setDialogState(() => verbindet = false);
|
||||
if (ok && ctx.mounted) {
|
||||
logger.info(
|
||||
'Login erfolgreich: url=${urlCtrl.text} user=$selectedUser');
|
||||
logger.info('Navidrome Login erfolgreich');
|
||||
await _navidrome.speichereZugangsdaten(
|
||||
urlCtrl.text, selectedUser, passCtrl.text);
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
if (mounted) setState(() {});
|
||||
} else if (ctx.mounted) {
|
||||
await logger.error(
|
||||
'Login fehlgeschlagen: url=${urlCtrl.text} '
|
||||
'user=$selectedUser pwLeer=${passCtrl.text.isEmpty}');
|
||||
'Navidrome Login fehlgeschlagen (pwLeer=${passCtrl.text.isEmpty})');
|
||||
setDialogState(() =>
|
||||
fehler = '❌ Login fehlgeschlagen');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user