You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit eae86c9
Browse filesBrowse the repository at this point in the historyBrowse files
fix(security): serialise vault writes and make KDF upgrades crash-safe (#54)
KS-2: route every whole-collection read-modify-write in StorageService through
a per-storage-key FIFO mutex, acquiring multiple keys in a fixed order. Two
interleaved mutations (import while a save is in flight, autosave plus manual
save, a save during a rekey) can no longer overwrite each other from the same
in-memory snapshot. A queued mutation rebuilds from the committed cache and a
re-encryption excludes concurrent writes on the collections it rewrites.
KS-1: record the new KDF metadata as a pending recovery record before
re-encrypting the vault and commit the active metadata with a single
SecureStore write. The next master-password login derives both candidates,
inspects which key decrypts the stored collections, and either promotes the
pending metadata or discards the record and keeps the previous password
working. A process kill between the vault write and the metadata commit no
longer leaves ciphertext whose salt was never stored. Biometric unlock defers
to the master password while a recovery record exists, and the login messages
state whether the new or the previous password is required. Crypto parameters
and error checks are unchanged.
Tests: interleaved saves with a delayed AsyncStorage mock; simulated crash
between the vault write and the metadata commit for both the legacy KDF
upgrade and the master-password change, covering the new-password, old-then-new
password and pre-vault-write rollback states. Docs and CHANGELOG updated.
Co-authored-by: Michael Gasperini <michael@mikesoft.it>
Copy file name to clipboardExpand all lines: docs/architecture.md
+10-5Lines changed: 10 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -109,11 +109,16 @@ A top-level error boundary (`src/components/ErrorBoundary.tsx`) wraps the naviga
109
109
3. Biometric login asks SecureStore for the stored biometric vault key with device authentication, verifies it against the same master-key verifier, and then loads it into memory.
110
110
4.`StorageService` initializes the encrypted vault cache with the active key.
111
111
5.`AuthContext` completes the shared post-login pipeline: authentication state, login notification, post-auth migration, notification settings, clipboard timeout, and auto-lock timeout.
112
-
6. PIN setup, PIN rotation, and transparent KDF upgrades treat ciphertext and verifier
113
-
metadata as one authenticated transaction. If rollback also fails, the session and
114
-
in-memory key are cleared instead of continuing against uncertain storage.
115
-
7. Password and note writes are serialized, encrypted with KS1, persisted to AsyncStorage,
116
-
and only then committed to the decrypted in-memory cache.
112
+
6. PIN setup, PIN rotation, and transparent KDF upgrades write a pending recovery record
113
+
before touching the vault, re-encrypt the vault in one batched write, and commit the new
114
+
verifier metadata with a single SecureStore write. A process kill between the vault write
115
+
and the metadata commit is reconciled on the next master-password login; biometric unlock
116
+
waits for that login. If rollback also fails, the session and in-memory key are cleared
117
+
instead of continuing against uncertain storage, and the recovery record is kept so the
118
+
next login can complete the promotion.
119
+
7. Whole-collection writes are queued per storage key, encrypted with KS1, persisted to
120
+
AsyncStorage, and only then committed to the decrypted in-memory cache, so two concurrent
121
+
mutations cannot overwrite each other from the same snapshot.
117
122
8. Logout and auto-lock clear authentication state and the in-memory key.
Copy file name to clipboardExpand all lines: docs/security.md
+14-1Lines changed: 14 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,7 +42,18 @@ Derived keys are normalized to 64-character lowercase hex strings. Invalid deriv
42
42
43
43
### Transparent KDF upgrade on login
44
44
45
-
Vaults created with legacy parameters — either PBKDF2 (`memory = 0`) or the old heavy Argon2id (`64 MiB / t = 3`) — are transparently upgraded to the current Argon2id parameters on the next successful password login, while the vault is decrypted in memory. The upgrade re-derives a new key (new salt and verifier), precomputes both encrypted collections, persists them in one batched storage call, and uses explicit rollback if the following verifier update fails. Recoverable failures restore the previous working key and retry on the next login. If both verifier persistence and rollback fail, Keysoft closes the authenticated session and clears the in-memory key rather than allowing further writes against uncertain storage. Biometric logins do not trigger the upgrade (no password is available); the next password login performs it.
45
+
Vaults created with legacy parameters — either PBKDF2 (`memory = 0`) or the old heavy Argon2id (`64 MiB / t = 3`) — are transparently upgraded to the current Argon2id parameters on the next successful password login, while the vault is decrypted in memory. The same crash-safe sequence is used when the master password changes:
46
+
47
+
1. A pending recovery record containing the new KDF metadata (salt, iterations, memory, verifier) is written to SecureStore while the active metadata still describes the previous key.
48
+
2. Both encrypted collections are re-encrypted and persisted in one batched AsyncStorage write.
49
+
3. The new metadata is committed with a single SecureStore write, which is the atomic switch point.
50
+
4. The recovery record is removed.
51
+
52
+
A process kill between the vault write and the metadata commit would previously leave ciphertext encrypted with a key whose salt was never stored — permanent vault loss. With the recovery record, the next master-password login derives both candidates, inspects which key decrypts the stored collections, and either promotes the pending metadata (vault already re-encrypted) or discards the record and keeps the previous password working (vault not re-encrypted yet). If the password only matches metadata whose key no longer decrypts the vault, login fails with a diagnostic naming the password that is required and keeps the recovery record untouched. A crash after the metadata commit leaves only a stale record, which the next login removes. Biometric unlock cannot derive the pending key: it refuses to run while a record exists and does not disable biometrics, and the following password login refreshes the biometric key best-effort.
53
+
54
+
Recoverable failures during the regular flow restore the previous working key and retry on a later login. If both the metadata commit and the rollback fail, Keysoft closes the authenticated session and clears the in-memory key rather than allowing further writes against uncertain storage; the recovery record is kept so the next password login can complete the promotion instead of losing the vault. The recovery record holds no key material, only the same class of KDF metadata already stored in SecureStore.
55
+
56
+
This safety relies on two platform guarantees: a single SecureStore key write is atomic, and `AsyncStorage.multiSet` persists both collections in one transaction on Android (the supported release platform). The residual risk is the same as any password change: once the vault has been re-encrypted, only the new password can open it, so a user who forgets the new password after a crash cannot fall back to the previous one.
46
57
47
58
When creating or changing the PIN, the verifier metadata and the vault key are produced from the same KDF result. This avoids a duplicate KDF pass for the same new PIN while preserving the configured KDF cost and the 64-character derived-key requirement.
48
59
@@ -71,6 +82,8 @@ Vault data uses the KS1 format:
71
82
72
83
Decryption verifies the MAC before trusting plaintext output.
73
84
85
+
Each collection is stored as one whole encrypted record. Mutations rebuild that record from the decrypted in-memory cache, so writes are queued per storage key: two concurrent mutations cannot both start from the same snapshot and silently drop each other's record, and a re-encryption of the whole vault excludes concurrent saves on the collections it rewrites.
86
+
74
87
### Key separation
75
88
76
89
KS1 derives a single 256-bit key and uses that same value both as the AES-256-CBC
0 commit comments