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
Split out of #59, which closes #57 by binding the vault header to its ciphertext. That fix covers everything that touches the vault with the master key — unlock, add, remove, key-auth connect — but it cannot cover the one path that has no key: metadata rendered while the vault is locked. #59 discloses the gap and leaves it open, because closing it is a frontend change.
keys_meta is read straight off vault.json. After #59 it is authenticated, but authentication is only ever checked by a decrypt, and a locked vault has no key to decrypt with.
The frontend reaches it unconditionally. App.vue:67 calls await keys.load() in onMounted with no vault.unlocked check, and KeyManager.vue then both renders and copies the result while locked:
KeyManager.vue:81-83 — copyPublicKey writes key.public_key_base64 to the clipboard
KeyManager.vue:181 — renders public_key_base64 in the public key viewer
Neither is gated on vault.unlocked.
Why it matters
This is #57's headline scenario minus the unlock. An attacker who can write vault.json — the #12 read + write primitive, a restored backup, a synced roaming profile — substitutes one entry's public_key_base64 and fingerprint. The user opens Clavyn on a cold start, does not unlock, opens Keys, clicks copy on the entry sitting under their own label, and pastes the attacker's key into a server's authorized_keys. No decrypt ever ran, so the tag was never checked, and the user was never asked for a passphrase.
The vault does refuse to open the moment anything needs the key, so the window is exactly "locked, and the user only reads". For a value whose entire purpose is to be copied out of the app and pasted into a server, that is the window that counts.
What to do
Gate the Keys view and the clipboard copy on vault.unlocked:
do not call keys.load() until the vault is unlocked, or clear the store on lock
show an explicit locked / unlock-to-view state in KeyManager.vue rather than an empty list next to a reset button — Bind the vault header to its ciphertext so tampered key metadata cannot pass #59 flags that an empty Keys view on a locked cold start reads as data loss, which is why the metadata was left in the header in the first place
refuse copyPublicKey while locked
Adding an unlocked guard to list_keys would enforce this in the core rather than trusting the caller, and is worth doing alongside; the IPC change belongs with the frontend work rather than with #59.
Out of scope for #59 by design: it does not touch desktop/src/**, and says so.
Split out of #59, which closes #57 by binding the vault header to its ciphertext. That fix covers everything that touches the vault with the master key — unlock, add, remove, key-auth connect — but it cannot cover the one path that has no key: metadata rendered while the vault is locked. #59 discloses the gap and leaves it open, because closing it is a frontend change.
The gap
list_keystakes no unlocked-vault guard:keys_metais read straight offvault.json. After #59 it is authenticated, but authentication is only ever checked by a decrypt, and a locked vault has no key to decrypt with.The frontend reaches it unconditionally.
App.vue:67callsawait keys.load()inonMountedwith novault.unlockedcheck, andKeyManager.vuethen both renders and copies the result while locked:KeyManager.vue:81-83—copyPublicKeywriteskey.public_key_base64to the clipboardKeyManager.vue:181— renderspublic_key_base64in the public key viewerNeither is gated on
vault.unlocked.Why it matters
This is #57's headline scenario minus the unlock. An attacker who can write
vault.json— the #12 read + write primitive, a restored backup, a synced roaming profile — substitutes one entry'spublic_key_base64andfingerprint. The user opens Clavyn on a cold start, does not unlock, opens Keys, clicks copy on the entry sitting under their own label, and pastes the attacker's key into a server'sauthorized_keys. No decrypt ever ran, so the tag was never checked, and the user was never asked for a passphrase.The vault does refuse to open the moment anything needs the key, so the window is exactly "locked, and the user only reads". For a value whose entire purpose is to be copied out of the app and pasted into a server, that is the window that counts.
What to do
Gate the Keys view and the clipboard copy on
vault.unlocked:keys.load()until the vault is unlocked, or clear the store on lockKeyManager.vuerather than an empty list next to a reset button — Bind the vault header to its ciphertext so tampered key metadata cannot pass #59 flags that an empty Keys view on a locked cold start reads as data loss, which is why the metadata was left in the header in the first placecopyPublicKeywhile lockedAdding an unlocked guard to
list_keyswould enforce this in the core rather than trusting the caller, and is worth doing alongside; the IPC change belongs with the frontend work rather than with #59.Out of scope for #59 by design: it does not touch
desktop/src/**, and says so.Refs #57, #59.