Skip to content

Commit 1ecf661

Browse files
Fenronclaude
andcommitted
feat: App-Lock + Dateinamen-Verschleierung für Bibliotheken (v0.5.2)
Optionaler Passwortschutz pro Bibliothek, einstellbar unter Einstellungen → Bibliotheks-Schutz. App-Lock: - lock.json in .mediashelf/ speichert Salt + AES-256-GCM-Verifier (Passwort wird nie gespeichert, gleiche Crypto-Infrastruktur wie Vault) - Beim Öffnen einer geschützten Bibliothek erscheint ein Passwort-Dialog - Passwort ändern / Schutz deaktivieren jederzeit in den Einstellungen - Funktioniert ideal in Kombination mit VeraCrypt-Containern Dateinamen-Verschleierung (erfordert aktiven App-Lock): - Neue Imports erhalten UUID-basierte Dateinamen auf der Festplatte - Der Bibliotheksordner verrät keine Dateinamen an Außenstehende Vault-Crypto: - generateSalt(), deriveKeyFromSalt(), encryptRaw(), decryptRaw() als public aliases für externe Nutzung durch LibraryLock Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 844c616 commit 1ecf661

7 files changed

Lines changed: 525 additions & 12 deletions

File tree

lib/core/library_lock.dart

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import 'package:path/path.dart' as p;
5+
6+
import 'constants.dart';
7+
import 'vault_crypto.dart';
8+
9+
/// Manages the optional App-Lock for a library.
10+
///
11+
/// When enabled, a `lock.json` file is written to `.mediashelf/`.
12+
/// It contains a salt and an AES-256-GCM verifier blob (same scheme as
13+
/// VaultCrypto) — the password is never stored.
14+
///
15+
/// On disk the file looks like:
16+
/// ```json
17+
/// { "salt": "<base64>", "verifier": "<base64>", "obfuscate_filenames": false }
18+
/// ```
19+
class LibraryLock {
20+
const LibraryLock._();
21+
22+
static String _lockPath(String libraryPath) =>
23+
p.join(libraryPath, kMediashelfDir, 'lock.json');
24+
25+
// ── Query ───────────────────────────────────────────────────────────────────
26+
27+
/// Returns true when the given library has an App-Lock configured.
28+
static bool isLocked(String libraryPath) =>
29+
File(_lockPath(libraryPath)).existsSync();
30+
31+
/// Reads the `obfuscate_filenames` setting from the lock file.
32+
/// Returns false if no lock file exists.
33+
static bool filenamesObfuscated(String libraryPath) {
34+
final f = File(_lockPath(libraryPath));
35+
if (!f.existsSync()) return false;
36+
try {
37+
final data = jsonDecode(f.readAsStringSync()) as Map<String, dynamic>;
38+
return data['obfuscate_filenames'] as bool? ?? false;
39+
} catch (_) {
40+
return false;
41+
}
42+
}
43+
44+
// ── Setup / change password ─────────────────────────────────────────────────
45+
46+
/// Enables the App-Lock for [libraryPath] with [password].
47+
/// Overwrites any existing lock file.
48+
static Future<void> enable(
49+
String libraryPath,
50+
String password, {
51+
bool obfuscateFilenames = false,
52+
}) async {
53+
// Derive key + verifier using the same crypto as VaultCrypto, but with a
54+
// library-scoped prefs key so it doesn't collide with the Vault key.
55+
final salt = VaultCrypto.generateSalt();
56+
final key = await VaultCrypto.deriveKeyFromSalt(password, salt);
57+
final verifier = await VaultCrypto.encryptRaw(
58+
key,
59+
utf8.encode('mediashelf-lock-v1-ok'),
60+
);
61+
62+
final lockFile = File(_lockPath(libraryPath));
63+
await lockFile.writeAsString(
64+
jsonEncode({
65+
'salt': base64.encode(salt),
66+
'verifier': base64.encode(verifier),
67+
'obfuscate_filenames': obfuscateFilenames,
68+
}),
69+
flush: true,
70+
);
71+
}
72+
73+
/// Disables the App-Lock by deleting the lock file.
74+
static Future<void> disable(String libraryPath) async {
75+
final f = File(_lockPath(libraryPath));
76+
if (f.existsSync()) await f.delete();
77+
}
78+
79+
/// Updates only the `obfuscate_filenames` flag without changing the password.
80+
static Future<void> setObfuscateFilenames(
81+
String libraryPath,
82+
bool value,
83+
) async {
84+
final f = File(_lockPath(libraryPath));
85+
if (!f.existsSync()) return;
86+
final data = jsonDecode(f.readAsStringSync()) as Map<String, dynamic>;
87+
data['obfuscate_filenames'] = value;
88+
await f.writeAsString(jsonEncode(data), flush: true);
89+
}
90+
91+
// ── Verification ────────────────────────────────────────────────────────────
92+
93+
/// Returns true if [password] is correct for this library.
94+
static Future<bool> verify(String libraryPath, String password) async {
95+
final f = File(_lockPath(libraryPath));
96+
if (!f.existsSync()) return true; // no lock → always ok
97+
98+
try {
99+
final data = jsonDecode(f.readAsStringSync()) as Map<String, dynamic>;
100+
final salt = base64.decode(data['salt'] as String);
101+
final verifier = base64.decode(data['verifier'] as String);
102+
103+
final key = await VaultCrypto.deriveKeyFromSalt(password, salt);
104+
await VaultCrypto.decryptRaw(key, verifier); // throws if wrong
105+
return true;
106+
} catch (_) {
107+
return false;
108+
}
109+
}
110+
}

lib/core/vault_crypto.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,28 @@ class VaultCrypto {
3838
);
3939
}
4040

41+
/// Public alias for external callers (e.g. LibraryLock).
42+
static Future<SecretKey> deriveKeyFromSalt(
43+
String password, List<int> salt) =>
44+
_deriveKey(password, salt);
45+
4146
/// Generates 16 cryptographically random bytes.
4247
static List<int> _randomSalt() {
4348
final rng = Random.secure();
4449
return List.generate(16, (_) => rng.nextInt(256));
4550
}
4651

52+
/// Public alias for external callers.
53+
static List<int> generateSalt() => _randomSalt();
54+
55+
/// Public alias for [encrypt] — same format, usable without the Vault context.
56+
static Future<Uint8List> encryptRaw(SecretKey key, List<int> plaintext) =>
57+
encrypt(key, plaintext);
58+
59+
/// Public alias for [decrypt].
60+
static Future<Uint8List> decryptRaw(SecretKey key, Uint8List data) =>
61+
decrypt(key, data);
62+
4763
// ── Encryption / Decryption ─────────────────────────────────────────────────
4864

4965
/// Encrypts [plaintext] with [key].
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
3+
import '../core/library_lock.dart';
4+
import 'library_provider.dart';
5+
6+
/// Whether the current library has an App-Lock configured.
7+
final libraryLockedProvider = Provider<bool>((ref) {
8+
final path = ref.watch(libraryPathProvider);
9+
if (path == null) return false;
10+
return LibraryLock.isLocked(path);
11+
});
12+
13+
/// Whether the current library has filename obfuscation enabled.
14+
final filenamesObfuscatedProvider = Provider<bool>((ref) {
15+
final path = ref.watch(libraryPathProvider);
16+
if (path == null) return false;
17+
return LibraryLock.filenamesObfuscated(path);
18+
});

0 commit comments

Comments
 (0)