@@ -687,6 +687,10 @@ class FolderVaultService extends ChangeNotifier {
687687 final s = _unlocked.remove (folderId);
688688 if (s != null ) {
689689 _wipe (s.folderKek);
690+ // A7 v1.0.4 — cancel le timer auto-lock si plus rien d'unlocked,
691+ // pour éviter qu'un Timer orphelin fire `_autoLockSweep` sur un
692+ // map vide (no-op mais bruit).
693+ if (_unlocked.isEmpty) _autoLockTimer? .cancel ();
690694 notifyListeners ();
691695 }
692696 }
@@ -699,6 +703,8 @@ class FolderVaultService extends ChangeNotifier {
699703 _wipe (s.folderKek);
700704 }
701705 _unlocked.clear ();
706+ // A7 v1.0.4 — cancel le timer auto-lock après wipe global.
707+ _autoLockTimer? .cancel ();
702708 notifyListeners ();
703709 }
704710
@@ -900,7 +906,8 @@ class FolderVaultService extends ChangeNotifier {
900906 return compute <_Argon2Job , Uint8List >(
901907 _argon2WorkerEntry,
902908 _Argon2Job (
903- passphrase: passphrase,
909+ // A8 v1.0.4 — passe une COPIE Uint8List wipable au worker.
910+ passphraseBytes: Uint8List .fromList (utf8Bytes (passphrase)),
904911 salt: salt,
905912 memoryKb: AppConstants .vaultArgon2MemoryKb,
906913 iterations: AppConstants .vaultArgon2Iterations,
@@ -922,7 +929,7 @@ class FolderVaultService extends ChangeNotifier {
922929 return compute <_Argon2Job , Uint8List >(
923930 _argon2WorkerEntry,
924931 _Argon2Job (
925- passphrase : pin,
932+ passphraseBytes : Uint8List . fromList ( utf8Bytes ( pin)) ,
926933 salt: salt,
927934 memoryKb: AppConstants .vaultPinArgon2MemoryKb,
928935 iterations: AppConstants .vaultPinArgon2Iterations,
@@ -969,7 +976,19 @@ class FolderVaultService extends ChangeNotifier {
969976 final secret = SecretKey (key);
970977 final box = SecretBox (cipherText, nonce: iv, mac: Mac (macBytes));
971978 final plain = await algo.decrypt (box, secretKey: secret, aad: aad);
972- return Uint8List .fromList (plain);
979+ // A6 v1.0.4 — wipe le buffer plain retourné par cryptography une fois
980+ // la copie effectuée. Limite la fenêtre où le plaintext folder_kek
981+ // (ou le contenu déchiffré) reste dans le heap du package cryptography
982+ // jusqu'à GC.
983+ final out = Uint8List .fromList (plain);
984+ if (plain is Uint8List ) {
985+ try {
986+ plain.fillRange (0 , plain.length, 0 );
987+ } catch (_) {
988+ // Buffer non-modifiable (vue, FFI) : best-effort.
989+ }
990+ }
991+ return out;
973992 }
974993
975994 Future <Uint8List > _verifierFor (Uint8List folderKek) async {
@@ -1018,16 +1037,22 @@ const _utf8 = Utf8Codec();
10181037
10191038/// Argument transmis à l'isolate. Tous les champs sont passifs et
10201039/// sérialisables (String, Uint8List, int).
1040+ ///
1041+ /// A8 v1.0.4 — `passphraseBytes` est passé en `Uint8List` (au lieu de
1042+ /// `String` ) pour permettre un wipe explicite côté worker isolate à la
1043+ /// fin de la dérivation. Le caller doit fournir `utf8.encode(passphrase)`
1044+ /// converti en `Uint8List` ; la String côté UI reste immutable (limitation
1045+ /// Dart) mais on minimise la fenêtre d'exposition dans l'isolate.
10211046class _Argon2Job {
10221047 const _Argon2Job ({
1023- required this .passphrase ,
1048+ required this .passphraseBytes ,
10241049 required this .salt,
10251050 required this .memoryKb,
10261051 required this .iterations,
10271052 required this .parallelism,
10281053 required this .hashLength,
10291054 });
1030- final String passphrase ;
1055+ final Uint8List passphraseBytes ;
10311056 final Uint8List salt;
10321057 final int memoryKb;
10331058 final int iterations;
@@ -1044,8 +1069,17 @@ Future<Uint8List> _argon2WorkerEntry(_Argon2Job job) async {
10441069 parallelism: job.parallelism,
10451070 hashLength: job.hashLength,
10461071 );
1047- final secret = SecretKey (utf8Bytes (job.passphrase));
1048- final derived = await algo.deriveKey (secretKey: secret, nonce: job.salt);
1049- final bytes = await derived.extractBytes ();
1050- return Uint8List .fromList (bytes);
1072+ try {
1073+ final secret = SecretKey (job.passphraseBytes);
1074+ final derived = await algo.deriveKey (secretKey: secret, nonce: job.salt);
1075+ final bytes = await derived.extractBytes ();
1076+ return Uint8List .fromList (bytes);
1077+ } finally {
1078+ // A8 v1.0.4 — wipe la copie de la passphrase côté worker isolate.
1079+ try {
1080+ job.passphraseBytes.fillRange (0 , job.passphraseBytes.length, 0 );
1081+ } catch (_) {
1082+ /* best-effort */
1083+ }
1084+ }
10511085}
0 commit comments