Skip to content

🐛 fix: drop the vault data key on lock, and let no await undo a lock - #85

Merged
edgarmesquita merged 1 commit into
mainfrom
fix/drop-data-key-on-lock
Sep 29, 2026
Merged

edgarmesquita merged 1 commit into
mainfrom
fix/drop-data-key-on-lock

Conversation

@edgarmesquita

Copy link
Copy Markdown
Contributor

Locking cleared the password-derived key but left the vault's data key in memory until the next unlock replaced it. That's the key that opens the payload and wraps every attachment key — still usable by anything running. Noticed while building #83, whose sign-out does clear it.

What clearing it exposed

Every reader of keysRef, and what it does with a lock behind it:

Reader Before Now
persistLocal · mutate bails on no key same — mutate checks the payload and calls it in one synchronous run, so no edit is lost
readAttachment could decrypt an attachment after a lock throws "O cofre está bloqueado"
sharingContext could wrap the data key for a new recipient after a lock throws
prepareAttachment · importBundle · returnToOwnVault read-and-bail same, now effective
runSync setPayload after the attachment upload and after a merge writes describing the cloud still land; the decrypted vault stays out of memory
pullAfterUnlock setPayload(merged) after the download result dropped whole, ids included
changeMasterPassword wrote the new key to memory and to disk after a lock abandoned whole
rotateDataKey resurrected keys and payload after network writes finishes from the values in hand

The worst one, reproduced before fixing

The password change runs two PBKDF2 derivations — most of a second each — and only then writes: the new key into memory, and into the keystore through syncKeystore. That keystore write also clears the lock's revocation mark from #81. A lock pressed during those seconds was undone, and the next reload opened the vault with no password at all.

I wrote the smoke check first and ran it against main:

FAIL  um bloqueio no meio da troca de senha não é desfeito

— with the full vault, all 14 items, on screen after the reload.

It is not a race: the check clicks Alterar senha and dispatches Ctrl+L in the same synchronous run of script, so the lock always lands while the first derivation is pending.

One rule for all of them

The keys object is the session: a lock drops it, and every unlock mints a new one. stillOpen(keys) — keysRef.current === keys — is asked after an await and before writing secrets. What each flow does with a "no":

  • password change: abandoned whole. Nothing is written before that point, so nothing is half-done, and the old password still opens the vault;
  • sync / pull: writes that describe what is now in the cloud (ids, revision, ciphertext) still land, and only the decrypted vault stays out of memory;
  • key rotation: finished, not abandoned. Once the shares hold the new key, stopping would lock out everyone staying while future writes kept a key the removed person still holds. It seals and pushes from the values in hand without touching a ref.

Tests, and what they do not cover

Two new smoke checks — the lock is not undone, and the old password still opens the vault. Each half of the fix was removed separately, each time with a changed bundle hash to rule out a stale dist/:

lock() keeps the data key          → FAIL  um bloqueio no meio da troca de senha não é desfeito
password change skips the check    → FAIL  um bloqueio no meio da troca de senha não é desfeito

Not covered by any executed test: the guards in runSync, pullAfterUnlock and rotateDataKey. All three need a real cloud account, and the smoke vault is local-only. They rest on reading the code and on the type checker.

Not handled: locking and unlocking again while a rotation is still on the network. The new session would open with the old key and write over the rotation. That takes typing the master password faster than two Drive writes complete, so I left it out rather than add machinery for it.

npm test 404 green · smoke 244 green, up from 242.

Locking cleared the password-derived key but left the vault's data key
in memory until the next unlock replaced it — the key that opens the
payload and every attachment, still usable by anything running.

Clearing it exposed what that had been hiding. Several flows await and
then write session state, and a lock that landed during the await was
quietly undone when they resumed. The worst was the password change:
after two key derivations it wrote the new key into memory and into the
keystore, and the keystore write cleared the lock's revocation mark, so
the next reload opened the vault with no password at all. Reproduced
before fixing: lock mid-change, reload, and the full vault was on
screen.

One rule now covers every case. The keys object is the session: a lock
drops it, and each unlock mints a new one. `stillOpen(keys)` is asked
after an await and before writing secrets. The password change is
abandoned whole. A sync or a pull keeps its writes that describe the
cloud and skips putting the decrypted vault back in memory. A key
rotation interrupted after the shares moved to the new key finishes
from the values in hand, since stopping there would lock out everyone
staying.
@edgarmesquita
edgarmesquita merged commit e6b2eab into main Sep 29, 2026
2 checks passed
@edgarmesquita
edgarmesquita deleted the fix/drop-data-key-on-lock branch September 29, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant