mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-21 07:26:51 +02:00
perf+security: polling guards, sensitive data redaction, SAF path sanitization
Go backend: - Add sensitive data redaction in log buffer (tokens, keys, passwords) - Validate extension auth URLs (HTTPS only, no private IPs, no embedded creds) - Block embedded credentials in extension HTTP requests - Tighten extension storage file permissions (0644 -> 0600) - Sanitize extension ID in store download path - Summarize auth URLs in logs to prevent token leakage Android (Kotlin): - Add sanitizeRelativeDir to prevent path traversal in SAF operations - Apply sanitizeFilename to all user-provided file names in SAF Flutter: - Add sensitive data redaction in Dart logger (mirrors Go patterns) - Mask device ID in log exports - Add in-flight guard to progress polling (download queue + local library) - Remove redundant _downloadedSpotifyIds Set, use _bySpotifyId map - Remove redundant _isrcSet, use _byIsrc map - Expand DownloadQueueLookup with byItemId and itemIds - Lazy search index building in queue tab - Bound embedded cover cache in queue tab (max 180) - Coalesce embedded cover refresh callbacks via postFrameCallback - Cache album track filtering in downloaded album screen - Cache thumbnail sizes by extension ID in home tab - Simplify recent access aggregation (single-pass) - Remove unused _isTyping state in home tab - Cap pre-warm track batch size to 80 - Skip setShowingRecentAccess if value unchanged - Use downloadQueueLookupProvider for granular queue selectors - Move grouped album filtering before content data computation
This commit is contained in:
+73
-7
@@ -8,6 +8,27 @@ import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
|
||||
const int _maxLogMessageLength = 500;
|
||||
const String _redactedValue = '[REDACTED]';
|
||||
|
||||
final RegExp _authorizationBearerPattern = RegExp(
|
||||
r'\bAuthorization\b\s*[:=]\s*Bearer\s+[A-Za-z0-9._~+/\-]+=*',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
final RegExp _genericSensitiveKeyValuePattern = RegExp(
|
||||
r'\b(access[_\s-]?token|refresh[_\s-]?token|id[_\s-]?token|client[_\s-]?secret|authorization|password|api[_\s-]?key)\b(\s*[:=]\s*)([^\s,;]+)',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
final RegExp _sensitiveQueryPattern = RegExp(
|
||||
r'([?&](?:access_token|refresh_token|id_token|token|client_secret|api_key|apikey|password)=)[^&\s]+',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
final RegExp _bearerTokenPattern = RegExp(
|
||||
r'\bBearer\s+[A-Za-z0-9._~+/\-]+=*',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
String _truncateLogText(String value, {int maxLength = _maxLogMessageLength}) {
|
||||
if (value.length <= maxLength) {
|
||||
@@ -16,6 +37,39 @@ String _truncateLogText(String value, {int maxLength = _maxLogMessageLength}) {
|
||||
return '${value.substring(0, maxLength)}...[truncated]';
|
||||
}
|
||||
|
||||
String _redactSensitiveText(String value) {
|
||||
var redacted = value;
|
||||
|
||||
redacted = redacted.replaceAllMapped(_authorizationBearerPattern, (_) {
|
||||
return 'Authorization: Bearer $_redactedValue';
|
||||
});
|
||||
|
||||
redacted = redacted.replaceAllMapped(_genericSensitiveKeyValuePattern, (
|
||||
match,
|
||||
) {
|
||||
final key = match.group(1) ?? '';
|
||||
final delimiter = match.group(2) ?? '=';
|
||||
return '$key$delimiter$_redactedValue';
|
||||
});
|
||||
|
||||
redacted = redacted.replaceAllMapped(_sensitiveQueryPattern, (match) {
|
||||
final prefix = match.group(1) ?? '';
|
||||
return '$prefix$_redactedValue';
|
||||
});
|
||||
|
||||
redacted = redacted.replaceAllMapped(_bearerTokenPattern, (_) {
|
||||
return 'Bearer $_redactedValue';
|
||||
});
|
||||
|
||||
return redacted;
|
||||
}
|
||||
|
||||
String _maskIdentifier(String value) {
|
||||
if (value.isEmpty) return value;
|
||||
if (value.length <= 4) return '***';
|
||||
return '${value.substring(0, 2)}***${value.substring(value.length - 2)}';
|
||||
}
|
||||
|
||||
class LogEntry {
|
||||
final DateTime timestamp;
|
||||
final String level;
|
||||
@@ -59,6 +113,7 @@ class LogBuffer extends ChangeNotifier {
|
||||
final Queue<LogEntry> _entries = Queue<LogEntry>();
|
||||
Timer? _goLogTimer;
|
||||
int _lastGoLogIndex = 0;
|
||||
bool _isFetchingGoLogs = false;
|
||||
|
||||
static bool _loggingEnabled = false;
|
||||
static bool get loggingEnabled => _loggingEnabled;
|
||||
@@ -79,9 +134,11 @@ class LogBuffer extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
final sanitizedMessage = _truncateLogText(entry.message);
|
||||
final sanitizedMessage = _truncateLogText(
|
||||
_redactSensitiveText(entry.message),
|
||||
);
|
||||
final sanitizedError = entry.error != null
|
||||
? _truncateLogText(entry.error!)
|
||||
? _truncateLogText(_redactSensitiveText(entry.error!))
|
||||
: null;
|
||||
final sanitizedEntry =
|
||||
(sanitizedMessage == entry.message && sanitizedError == entry.error)
|
||||
@@ -105,13 +162,20 @@ class LogBuffer extends ChangeNotifier {
|
||||
void startGoLogPolling() {
|
||||
_goLogTimer?.cancel();
|
||||
_goLogTimer = Timer.periodic(_goLogPollingInterval, (_) async {
|
||||
await _fetchGoLogs();
|
||||
if (_isFetchingGoLogs) return;
|
||||
_isFetchingGoLogs = true;
|
||||
try {
|
||||
await _fetchGoLogs();
|
||||
} finally {
|
||||
_isFetchingGoLogs = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void stopGoLogPolling() {
|
||||
_goLogTimer?.cancel();
|
||||
_goLogTimer = null;
|
||||
_isFetchingGoLogs = false;
|
||||
}
|
||||
|
||||
Future<void> _fetchGoLogs() async {
|
||||
@@ -216,7 +280,7 @@ class LogBuffer extends ChangeNotifier {
|
||||
buffer.writeln(
|
||||
'Android Version: ${android.version.release} (SDK ${android.version.sdkInt})',
|
||||
);
|
||||
buffer.writeln('Device ID: ${android.id}');
|
||||
buffer.writeln('Device ID: ${_maskIdentifier(android.id)}');
|
||||
buffer.writeln('Hardware: ${android.hardware}');
|
||||
buffer.writeln('Product: ${android.product}');
|
||||
buffer.writeln('Supported ABIs: ${android.supportedAbis.join(', ')}');
|
||||
@@ -313,12 +377,14 @@ class BufferedOutput extends LogOutput {
|
||||
void output(OutputEvent event) {
|
||||
if (kDebugMode) {
|
||||
for (final line in event.lines) {
|
||||
debugPrint(_truncateLogText(line));
|
||||
debugPrint(_truncateLogText(_redactSensitiveText(line)));
|
||||
}
|
||||
}
|
||||
|
||||
final level = _levelToString(event.level);
|
||||
final message = _truncateLogText(event.lines.join('\n'));
|
||||
final message = _truncateLogText(
|
||||
_redactSensitiveText(event.lines.join('\n')),
|
||||
);
|
||||
|
||||
LogBuffer().add(
|
||||
LogEntry(
|
||||
@@ -421,7 +487,7 @@ class AppLogger {
|
||||
_addToBuffer('ERROR', message, error: error.toString());
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[$_tag] ERROR: ${_truncateLogText(message)} | ${_truncateLogText(error.toString())}',
|
||||
'[$_tag] ERROR: ${_truncateLogText(_redactSensitiveText(message))} | ${_truncateLogText(_redactSensitiveText(error.toString()))}',
|
||||
);
|
||||
if (stackTrace != null) {
|
||||
debugPrint(stackTrace.toString());
|
||||
|
||||
Reference in New Issue
Block a user