diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 21e9731..473b9f8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,13 +5,15 @@ + + android:label="Melo" + android:name="${applicationName}" + android:icon="@mipmap/ic_launcher" + android:allowBackup="false" + android:requestLegacyExternalStorage="true"> _user; String get token => _token; bool get istAngemeldet => _token.isNotEmpty; @@ -48,12 +53,29 @@ class CloudService { } /// Stellt gespeicherten Token wieder her (Auto-Login nach App-Start). + /// Migriert einmalig alte SharedPreferences-Einträge in SecureStorage. Future restoreLogin() async { final prefs = await SharedPreferences.getInstance(); - final t = prefs.getString('melo_cloud_token') ?? ''; + var t = await _secure.read(key: 'melo_cloud_token') ?? ''; + var u = await _secure.read(key: 'melo_cloud_user') ?? ''; + // Migration alter Versionen (Token lag früher in SharedPreferences) + if (t.isEmpty) { + final altT = prefs.getString('melo_cloud_token') ?? ''; + final altU = prefs.getString('melo_cloud_user') ?? ''; + if (altT.isNotEmpty) { + t = altT; + u = altU; + await _secure.write(key: 'melo_cloud_token', value: t); + if (u.isNotEmpty) { + await _secure.write(key: 'melo_cloud_user', value: u); + } + await prefs.remove('melo_cloud_token'); + await prefs.remove('melo_cloud_user'); + } + } if (t.isEmpty) return false; _token = t; - _user = prefs.getString('melo_cloud_user') ?? ''; + _user = u; MeloLogger.cloudToken = t; return true; } @@ -62,15 +84,13 @@ class CloudService { _token = ''; _user = ''; MeloLogger.cloudToken = null; - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('melo_cloud_token'); - await prefs.remove('melo_cloud_user'); + await _secure.delete(key: 'melo_cloud_token'); + await _secure.delete(key: 'melo_cloud_user'); } Future _speichereToken() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('melo_cloud_token', _token); - await prefs.setString('melo_cloud_user', _user); + await _secure.write(key: 'melo_cloud_token', value: _token); + await _secure.write(key: 'melo_cloud_user', value: _user); } Map get _authHeader => { diff --git a/lib/services/download_service.dart b/lib/services/download_service.dart index 5ea8d92..a3941b3 100644 --- a/lib/services/download_service.dart +++ b/lib/services/download_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import '../models/song.dart'; import '../database/db_helper.dart'; @@ -249,8 +250,8 @@ class DownloadService extends ChangeNotifier { : Directory('${(await getApplicationDocumentsDirectory()).path}/music'); if (!await dir.exists()) await dir.create(recursive: true); - // Sicheren Dateinamen erstellen - final safeName = titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim(); + // Sicheren Dateinamen erstellen (p.basename verhindert Path-Traversal) + final safeName = p.basename(titel).replaceAll(RegExp(r'[^\w\s-]'), '').trim(); final lokalerName = '${safeName.isEmpty ? "song" : safeName}.mp3'; dateiPfad = '${dir.path}/$lokalerName'; diff --git a/lib/services/navidrome_service.dart b/lib/services/navidrome_service.dart index 3c21442..6b9cc0d 100644 --- a/lib/services/navidrome_service.dart +++ b/lib/services/navidrome_service.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../models/song.dart'; @@ -81,7 +83,9 @@ class NavidromeService { _serverUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; _user = user; _password = password; - _salt = DateTime.now().millisecondsSinceEpoch.toString(); + // Kryptographisch sicherer Salt (vorher millisecondsSinceEpoch = vorhersagbar) + final rng = Random.secure(); + _salt = base64Encode(List.generate(16, (_) => rng.nextInt(256))); _token = md5.convert(utf8.encode(_password + _salt)).toString(); } @@ -180,7 +184,7 @@ class NavidromeService { final musikDir = Directory('${dir.path}/music'); if (!await musikDir.exists()) await musikDir.create(recursive: true); - final safeName = s.titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim(); + final safeName = p.basename(s.titel).replaceAll(RegExp(r'[^\w\s-]'), '').trim(); final kurzId = s.id.length > 8 ? s.id.substring(0, 8) : s.id; final dateiName = '${safeName.isEmpty ? "song" : safeName}_$kurzId.mp3'; final dateiPfad = '${musikDir.path}/$dateiName'; diff --git a/lib/viewmodels/melo_home_viewmodel.dart b/lib/viewmodels/melo_home_viewmodel.dart index 88eac81..6129658 100644 --- a/lib/viewmodels/melo_home_viewmodel.dart +++ b/lib/viewmodels/melo_home_viewmodel.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as p; import '../database/db_helper.dart'; import '../services/player_service.dart'; import '../services/musik_scanner.dart'; @@ -309,7 +310,7 @@ class MeloHomeViewModel extends ChangeNotifier { .map((f) => f.path.split('/').last).toSet(); for (final s in serverSongs) { - final title = s['title'].toString(); + final title = p.basename(s['title'].toString()); if (localFiles.contains(title)) continue; // schon lokal await cloud.download(s['id'].toString(), '${dir.path}/$title'); }