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');
}