From 4b84ea7dbbafacbdd9fc6bd0e22685779b4a925d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 29 Jul 2026 13:30:41 +0200 Subject: [PATCH 1/4] chore(rust): update vetkeys examples to ic-vetkeys 0.8.1 and @icp-sdk/vetkeys 0.5.0 Backend (ic-vetkeys 0.7.0 -> 0.8.1): - password_manager: replace the hand-written EncryptedMaps pass-through canister with the new `ic_vetkeys::export_encrypted_maps_canister!` macro (~311 -> ~30 lines). The macro owns the endpoint set, so the Candid matches the frontend by construction. Regenerated backend.did (interface unchanged; the newer crate now emits doc comments into the Candid). - basic_ibe, basic_timelock_ibe, basic_bls_signing, password_manager_with_metadata: version bump only (no breaking API changes since 0.7.0). - password_manager_with_metadata deliberately stays hand-written: it keeps a second, backend-authoritative metadata map in sync with the encrypted values, so it must NOT expose the plain insert/remove mutators the macro would generate (they would let a value be written without its metadata row and desync the two stores). See dfinity/vetkeys issue linked in the PR. Frontend (@icp-sdk/vetkeys ^0.5.0-beta.0 -> ^0.5.0): - All 7 Rust frontends bumped to the final 0.5.0 release. - password_manager and password_manager_with_metadata: opt back into IndexedDB persistence (in-memory is the new 0.5.0 default) via IndexedDbDerivedKeyMaterialCache, namespaced per identity, and call EncryptedMaps.clearCache() on logout to drop cached key material. Motoko examples untouched (Rust-only change, per request). Verified: cargo check on all 5 backends; wasm build + candid diff for password_manager; npm build for all 7 frontends. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../basic_bls_signing/backend/Cargo.toml | 2 +- .../basic_bls_signing/frontend/package.json | 2 +- rust/vetkeys/basic_ibe/backend/Cargo.toml | 2 +- rust/vetkeys/basic_ibe/frontend/package.json | 2 +- .../basic_timelock_ibe/backend/Cargo.toml | 2 +- .../basic_timelock_ibe/frontend/package.json | 2 +- .../vetkeys/basic_vetkd/frontend/package.json | 2 +- .../frontend/package.json | 2 +- rust/vetkeys/password_manager/Cargo.toml | 2 +- rust/vetkeys/password_manager/README.md | 2 +- .../password_manager/backend/backend.did | 14 +- .../password_manager/backend/src/lib.rs | 316 +----------------- .../password_manager/frontend/package.json | 2 +- .../frontend/src/lib/encrypted_maps.ts | 22 +- .../frontend/src/store/auth.ts | 5 +- .../password_manager_with_metadata/README.md | 2 +- .../backend/Cargo.toml | 2 +- .../frontend/package.json | 2 +- .../frontend/src/lib/encrypted_maps.ts | 22 +- .../frontend/src/lib/password_manager.ts | 17 +- .../frontend/src/store/auth.ts | 5 +- 21 files changed, 102 insertions(+), 327 deletions(-) diff --git a/rust/vetkeys/basic_bls_signing/backend/Cargo.toml b/rust/vetkeys/basic_bls_signing/backend/Cargo.toml index 35b7410dfa..678d588917 100644 --- a/rust/vetkeys/basic_bls_signing/backend/Cargo.toml +++ b/rust/vetkeys/basic_bls_signing/backend/Cargo.toml @@ -18,7 +18,7 @@ getrandom = { version = "0.2", features = ["custom"] } ic-cdk = "0.20.1" ic-cdk-management-canister = "0.1.1" ic-stable-structures = "0.6.8" -ic-vetkeys = "0.7.0" +ic-vetkeys = "0.8.1" serde = "1.0.217" serde_bytes = "0.11.15" serde_cbor = "0.11.2" diff --git a/rust/vetkeys/basic_bls_signing/frontend/package.json b/rust/vetkeys/basic_bls_signing/frontend/package.json index 0a68d864e4..16390882d0 100644 --- a/rust/vetkeys/basic_bls_signing/frontend/package.json +++ b/rust/vetkeys/basic_bls_signing/frontend/package.json @@ -10,7 +10,7 @@ "dependencies": { "@icp-sdk/auth": "^7.1.0", "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0" + "@icp-sdk/vetkeys": "^0.5.0" }, "devDependencies": { "@icp-sdk/bindgen": "~0.2.2", diff --git a/rust/vetkeys/basic_ibe/backend/Cargo.toml b/rust/vetkeys/basic_ibe/backend/Cargo.toml index 8acc73773e..f5f564b277 100644 --- a/rust/vetkeys/basic_ibe/backend/Cargo.toml +++ b/rust/vetkeys/basic_ibe/backend/Cargo.toml @@ -18,7 +18,7 @@ ic-cdk = "0.20.1" ic-cdk-management-canister = "0.1.1" ic-dummy-getrandom-for-wasm = "0.1.0" ic-stable-structures = "0.6.8" -ic-vetkeys = "0.7.0" +ic-vetkeys = "0.8.1" serde = "1.0.217" serde_bytes = "0.11.15" serde_cbor = "0.11.2" diff --git a/rust/vetkeys/basic_ibe/frontend/package.json b/rust/vetkeys/basic_ibe/frontend/package.json index 0a68d864e4..16390882d0 100644 --- a/rust/vetkeys/basic_ibe/frontend/package.json +++ b/rust/vetkeys/basic_ibe/frontend/package.json @@ -10,7 +10,7 @@ "dependencies": { "@icp-sdk/auth": "^7.1.0", "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0" + "@icp-sdk/vetkeys": "^0.5.0" }, "devDependencies": { "@icp-sdk/bindgen": "~0.2.2", diff --git a/rust/vetkeys/basic_timelock_ibe/backend/Cargo.toml b/rust/vetkeys/basic_timelock_ibe/backend/Cargo.toml index 5874fd7834..f0eca3233c 100644 --- a/rust/vetkeys/basic_timelock_ibe/backend/Cargo.toml +++ b/rust/vetkeys/basic_timelock_ibe/backend/Cargo.toml @@ -20,7 +20,7 @@ ic-cdk-management-canister = "0.1.1" ic-cdk-timers = "1.0.0" ic-dummy-getrandom-for-wasm = "0.1.0" ic-stable-structures = "0.6.8" -ic-vetkeys = "0.7.0" +ic-vetkeys = "0.8.1" serde = "1.0.217" serde_bytes = "0.11.15" serde_cbor = "0.11.2" diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/package.json b/rust/vetkeys/basic_timelock_ibe/frontend/package.json index 399d5a6425..592ff0d587 100644 --- a/rust/vetkeys/basic_timelock_ibe/frontend/package.json +++ b/rust/vetkeys/basic_timelock_ibe/frontend/package.json @@ -26,6 +26,6 @@ "dependencies": { "@icp-sdk/auth": "^7.1.0", "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0" + "@icp-sdk/vetkeys": "^0.5.0" } } diff --git a/rust/vetkeys/basic_vetkd/frontend/package.json b/rust/vetkeys/basic_vetkd/frontend/package.json index 258c8684c1..7936b17ac1 100644 --- a/rust/vetkeys/basic_vetkd/frontend/package.json +++ b/rust/vetkeys/basic_vetkd/frontend/package.json @@ -10,7 +10,7 @@ "dependencies": { "@icp-sdk/auth": "^7.1.0", "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0" + "@icp-sdk/vetkeys": "^0.5.0" }, "devDependencies": { "@icp-sdk/bindgen": "~0.2.2", diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json index a47e8cdc67..eced20f0c7 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json +++ b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json @@ -26,7 +26,7 @@ "dependencies": { "@icp-sdk/auth": "^7.1.0", "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0", + "@icp-sdk/vetkeys": "^0.5.0", "daisyui": "^1.25.4", "idb-keyval": "6.2.1", "isomorphic-dompurify": "^2.25.0", diff --git a/rust/vetkeys/password_manager/Cargo.toml b/rust/vetkeys/password_manager/Cargo.toml index 8450c49e2f..3164c69fb5 100644 --- a/rust/vetkeys/password_manager/Cargo.toml +++ b/rust/vetkeys/password_manager/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" ic-cdk = "0.20.1" ic-cdk-management-canister = "0.1.1" ic-stable-structures = "0.7.0" -ic-vetkeys = "0.7.0" +ic-vetkeys = "0.8.1" [profile.release] lto = true diff --git a/rust/vetkeys/password_manager/README.md b/rust/vetkeys/password_manager/README.md index 966be9e4bf..492b2ad7af 100644 --- a/rust/vetkeys/password_manager/README.md +++ b/rust/vetkeys/password_manager/README.md @@ -58,7 +58,7 @@ icp network stop An **Encrypted Maps**-enabled Rust canister that securely stores passwords. -> **Note.** This backend is hand-written today. An upstream Rust macro that generates an entire Encrypted Maps canister in one line — `ic_vetkeys::export_encrypted_maps_canister!(...)` — is in progress ([dfinity/vetkeys#404](https://github.com/dfinity/vetkeys/pull/404)); it produces the same Candid interface with far less boilerplate. +> **Note.** The entire canister is generated by the `ic_vetkeys::export_encrypted_maps_canister!(...)` macro: the canister owns the `MemoryManager` and passes Encrypted Maps its four `Memory` instances, and the macro emits the `#[init]`/`#[post_upgrade]` plus every `#[query]`/`#[update]` endpoint. Because the macro is the single source of the endpoint set, the exposed Candid matches what the `@icp-sdk/vetkeys` frontend expects by construction. See [`backend/src/lib.rs`](backend/src/lib.rs). When you need to extend the standard endpoints with your own state or invariants, see the [`password_manager_with_metadata`](../password_manager_with_metadata/) example, which layers per-password metadata on hand-written endpoints. ### Frontend (`frontend/`) diff --git a/rust/vetkeys/password_manager/backend/backend.did b/rust/vetkeys/password_manager/backend/backend.did index f30499fd14..0880c066ca 100644 --- a/rust/vetkeys/password_manager/backend/backend.did +++ b/rust/vetkeys/password_manager/backend/backend.did @@ -1,5 +1,17 @@ -type AccessRights = variant { Read; ReadWrite; ReadWriteManage }; +// Access rights of a user to a vetKey in [`crate::key_manager::KeyManager`] and/or an encrypted map in [`crate::encrypted_maps::EncryptedMaps`]. +type AccessRights = variant { + // User can retrieve the vetKey or encrypted map. + Read; + // User can update values in the encrypted map. + ReadWrite; + // User can view/share/revoke access to the vetKey or encrypted map. + ReadWriteManage; +}; +// Efficiently serializable and deserializable byte vector that is `Storable` with `ic_stable_structures`. +// See, e.g., [https://mmapped.blog/posts/01-effective-rust-canisters#serde-bytes](https://mmapped.blog/posts/01-effective-rust-canisters#serde-bytes) for more details regarding why `Vec` does not work out of the box. +// Also, we cannot use `serde_bytes::ByteBuf` directly because it is not `Storable`. type ByteBuf = record { inner : blob }; +// Represents the complete data for an encrypted map, including ownership, contents, and access control. type EncryptedMapData = record { access_control : vec record { principal; AccessRights }; keyvals : vec record { ByteBuf; ByteBuf }; diff --git a/rust/vetkeys/password_manager/backend/src/lib.rs b/rust/vetkeys/password_manager/backend/src/lib.rs index ca58ed57ef..eeff5997a2 100644 --- a/rust/vetkeys/password_manager/backend/src/lib.rs +++ b/rust/vetkeys/password_manager/backend/src/lib.rs @@ -1,311 +1,31 @@ -use std::cell::RefCell; - -use candid::Principal; -use ic_cdk_management_canister::{VetKDCurve, VetKDKeyId}; -use ic_cdk::{init, post_upgrade, query, update}; +// This canister is a thin wrapper around the `ic-vetkeys` EncryptedMaps +// library. The canister owns the single MemoryManager and hands EncryptedMaps +// the four Memory instances it needs; the `#[init]`/`#[post_upgrade]` and every +// `#[query]`/`#[update]` endpoint are generated by the library macro, which +// guarantees the exposed Candid matches what the `@icp-sdk/vetkeys` frontend +// expects. use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; -use ic_stable_structures::storable::Blob; -use ic_stable_structures::{Cell as StableCell, DefaultMemoryImpl}; -use ic_vetkeys::encrypted_maps::{EncryptedMapData, EncryptedMaps, VetKey, VetKeyVerificationKey}; -use ic_vetkeys::types::{AccessRights, ByteBuf, EncryptedMapValue, TransportKey}; +use ic_stable_structures::DefaultMemoryImpl; +use std::cell::RefCell; type Memory = VirtualMemory; -type MapId = (Principal, ByteBuf); thread_local! { static MEMORY_MANAGER: RefCell> = RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); - static ENCRYPTED_MAPS: RefCell>> = - const { RefCell::new(None) }; - static KEY_NAME: RefCell> = - RefCell::new(StableCell::init(id_to_memory(4), String::new())); -} - -#[init] -fn init(key_name: String) { - KEY_NAME.with_borrow_mut(|k| { k.set(key_name.clone()); }); - init_encrypted_maps(key_name); -} - -#[post_upgrade] -fn post_upgrade() { - let key_name = KEY_NAME.with_borrow(|k| k.get().clone()); - init_encrypted_maps(key_name); -} - -fn init_encrypted_maps(key_name: String) { - let key_id = VetKDKeyId { - curve: VetKDCurve::Bls12_381_G2, - name: key_name, - }; - ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { - encrypted_maps.replace(EncryptedMaps::init( - "encrypted_maps_app", - key_id, - id_to_memory(0), - id_to_memory(1), - id_to_memory(2), - id_to_memory(3), - )) - }); -} - -#[query] -fn get_accessible_shared_map_names() -> Vec<(Principal, ByteBuf)> { - ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_accessible_shared_map_names(ic_cdk::api::msg_caller()) - .into_iter() - .map(|map_id| (map_id.0, ByteBuf::from(map_id.1.as_ref().to_vec()))) - .collect() - }) -} - -#[query] -fn get_shared_user_access_for_map( - key_owner: Principal, - key_name: ByteBuf, -) -> Result, String> { - let key_name = bytebuf_to_blob(key_name)?; - let key_id = (key_owner, key_name); - ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_shared_user_access_for_map(ic_cdk::api::msg_caller(), key_id) - }) -} - -#[query] -fn get_encrypted_values_for_map( - map_owner: Principal, - map_name: ByteBuf, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - let result = ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_encrypted_values_for_map(ic_cdk::api::msg_caller(), map_id) - }); - result.map(|map_values| { - map_values - .into_iter() - .map(|(key, value)| (ByteBuf::from(key.as_slice().to_vec()), value)) - .collect() - }) -} - -#[query] -fn get_all_accessible_encrypted_values() -> Vec<(MapId, Vec<(ByteBuf, EncryptedMapValue)>)> { - ENCRYPTED_MAPS - .with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_all_accessible_encrypted_values(ic_cdk::api::msg_caller()) - }) - .into_iter() - .map(|((owner, map_name), encrypted_values)| { - ( - (owner, ByteBuf::from(map_name.as_ref().to_vec())), - encrypted_values - .into_iter() - .map(|(key, value)| (ByteBuf::from(key.as_ref().to_vec()), value)) - .collect(), - ) - }) - .collect() -} - -#[query] -fn get_all_accessible_encrypted_maps() -> Vec> { - ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_all_accessible_encrypted_maps(ic_cdk::api::msg_caller()) - }) -} - -#[query] -fn get_encrypted_value( - map_owner: Principal, - map_name: ByteBuf, - map_key: ByteBuf, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { - encrypted_maps.as_ref().unwrap().get_encrypted_value( - ic_cdk::api::msg_caller(), - map_id, - bytebuf_to_blob(map_key)?, - ) - }) } -#[update] -fn remove_map_values( - map_owner: Principal, - map_name: ByteBuf, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - let result = ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { - encrypted_maps - .as_mut() - .unwrap() - .remove_map_values(ic_cdk::api::msg_caller(), map_id) - }); - result.map(|removed| { - removed - .into_iter() - .map(|key| ByteBuf::from(key.as_ref().to_vec())) - .collect() - }) -} - -#[query] -fn get_owned_non_empty_map_names() -> Vec { - ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_owned_non_empty_map_names(ic_cdk::api::msg_caller()) - .into_iter() - .map(|map_name| ByteBuf::from(map_name.as_slice().to_vec())) - .collect() - }) -} - -#[update] -fn insert_encrypted_value( - map_owner: Principal, - map_name: ByteBuf, - map_key: ByteBuf, - value: EncryptedMapValue, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { - encrypted_maps.as_mut().unwrap().insert_encrypted_value( - ic_cdk::api::msg_caller(), - map_id, - bytebuf_to_blob(map_key)?, - value, - ) - }) -} - -#[update] -fn remove_encrypted_value( - map_owner: Principal, - map_name: ByteBuf, - map_key: ByteBuf, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { - encrypted_maps.as_mut().unwrap().remove_encrypted_value( - ic_cdk::api::msg_caller(), - map_id, - bytebuf_to_blob(map_key)?, - ) - }) -} - -#[update] -async fn get_vetkey_verification_key() -> VetKeyVerificationKey { - ENCRYPTED_MAPS - .with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_vetkey_verification_key() - }) - .await -} - -#[update] -async fn get_encrypted_vetkey( - map_owner: Principal, - map_name: ByteBuf, - transport_key: TransportKey, -) -> Result { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - Ok(ENCRYPTED_MAPS - .with_borrow(|encrypted_maps| { - encrypted_maps.as_ref().unwrap().get_encrypted_vetkey( - ic_cdk::api::msg_caller(), - map_id, - transport_key, - ) - })? - .await) -} - -#[query] -fn get_user_rights( - map_owner: Principal, - map_name: ByteBuf, - user: Principal, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { - encrypted_maps - .as_ref() - .unwrap() - .get_user_rights(ic_cdk::api::msg_caller(), map_id, user) - }) -} - -#[update] -fn set_user_rights( - map_owner: Principal, - map_name: ByteBuf, - user: Principal, - access_rights: AccessRights, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { - encrypted_maps.as_mut().unwrap().set_user_rights( - ic_cdk::api::msg_caller(), - map_id, - user, - access_rights, - ) - }) -} - -#[update] -fn remove_user( - map_owner: Principal, - map_name: ByteBuf, - user: Principal, -) -> Result, String> { - let map_name = bytebuf_to_blob(map_name)?; - let map_id = (map_owner, map_name); - ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { - encrypted_maps - .as_mut() - .unwrap() - .remove_user(ic_cdk::api::msg_caller(), map_id, user) - }) -} - -fn bytebuf_to_blob(buf: ByteBuf) -> Result, String> { - Blob::try_from(buf.as_ref()).map_err(|_| "too large input".to_string()) -} - -fn id_to_memory(id: u8) -> Memory { +fn memory(id: u8) -> Memory { MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(id))) } +// The first argument is the domain separator that isolates this application's +// derived keys; it must stay stable for the life of the canister. The four +// memories are, in order: [domain_separator, access_control, shared_keys, +// encrypted_maps]. +ic_vetkeys::export_encrypted_maps_canister!( + "encrypted_maps_app", + [memory(0), memory(1), memory(2), memory(3)], +); + ic_cdk::export_candid!(); diff --git a/rust/vetkeys/password_manager/frontend/package.json b/rust/vetkeys/password_manager/frontend/package.json index ba0e1f6b23..3d671be29a 100644 --- a/rust/vetkeys/password_manager/frontend/package.json +++ b/rust/vetkeys/password_manager/frontend/package.json @@ -20,7 +20,7 @@ "dependencies": { "@icp-sdk/auth": "^7.1.0", "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0", + "@icp-sdk/vetkeys": "^0.5.0", "@popperjs/core": "^2.11.8", "@sveltejs/vite-plugin-svelte": "^3.0.2", "@tailwindcss/postcss": "^4.0.6", diff --git a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts index 9972d6965c..f929f14a7d 100644 --- a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts +++ b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts @@ -1,8 +1,9 @@ import "./init.ts"; -import { HttpAgent, type HttpAgentOptions } from "@icp-sdk/core/agent"; +import { HttpAgent, type Identity } from "@icp-sdk/core/agent"; import { DefaultEncryptedMapsClient, EncryptedMaps, + IndexedDbDerivedKeyMaterialCache, } from "@icp-sdk/vetkeys/encrypted_maps"; import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; @@ -11,7 +12,7 @@ const canisterEnv = safeGetCanisterEnv<{ }>(); export async function createEncryptedMaps( - agentOptions?: HttpAgentOptions, + identity: Identity, ): Promise { const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; @@ -22,10 +23,23 @@ export async function createEncryptedMaps( } const agent = await HttpAgent.create({ - ...agentOptions, + identity, host: window.location.origin, rootKey: canisterEnv?.IC_ROOT_KEY, }); - return new EncryptedMaps(new DefaultEncryptedMapsClient(agent, canisterId)); + // Since v0.5.0 EncryptedMaps caches derived key material in memory only by + // default, so it is discarded on page reload. We opt back into persisting + // it across reloads with IndexedDbDerivedKeyMaterialCache, namespacing the + // store by the caller's principal so one identity's cached keys are never + // served to another on the same origin. Remember to call clearCache() on + // logout / identity change (see the auth store). + const cache = new IndexedDbDerivedKeyMaterialCache( + `vetkeys-${identity.getPrincipal().toText()}`, + ); + + return new EncryptedMaps( + new DefaultEncryptedMapsClient(agent, canisterId), + { cache }, + ); } diff --git a/rust/vetkeys/password_manager/frontend/src/store/auth.ts b/rust/vetkeys/password_manager/frontend/src/store/auth.ts index 54a75e50ae..3adfca2b11 100644 --- a/rust/vetkeys/password_manager/frontend/src/store/auth.ts +++ b/rust/vetkeys/password_manager/frontend/src/store/auth.ts @@ -79,6 +79,9 @@ export async function logout() { const currentAuth = get(auth); if (currentAuth.state === "initialized") { + // Drop cached derived key material so a persisted (IndexedDB) handle + // can no longer be used to decrypt after logout. + await currentAuth.encryptedMaps.clearCache(); await currentAuth.client.signOut(); auth.update(() => ({ state: "anonymous", @@ -93,7 +96,7 @@ export async function authenticate(client: AuthClient) { try { const identity = await client.getIdentity(); - const encryptedMaps = await createEncryptedMaps({ identity }); + const encryptedMaps = await createEncryptedMaps(identity); auth.update(() => ({ state: "initialized", diff --git a/rust/vetkeys/password_manager_with_metadata/README.md b/rust/vetkeys/password_manager_with_metadata/README.md index cd06c10aa5..1faaed437b 100644 --- a/rust/vetkeys/password_manager_with_metadata/README.md +++ b/rust/vetkeys/password_manager_with_metadata/README.md @@ -61,7 +61,7 @@ icp network stop An **Encrypted Maps**-enabled Rust canister that stores encrypted passwords together with unencrypted metadata (URLs, tags) in atomic update calls. -> **Note.** A plain Encrypted Maps canister can be generated by an upstream Rust macro — `ic_vetkeys::export_encrypted_maps_canister!(...)` — that is in progress ([dfinity/vetkeys#404](https://github.com/dfinity/vetkeys/pull/404)). This example instead hand-writes a custom canister so it can add the metadata-aware methods on top of the Encrypted Maps interface. +> **Note.** A plain Encrypted Maps canister can be generated in one line by the `ic_vetkeys::export_encrypted_maps_canister!(...)` macro — see the [`password_manager`](../password_manager/) example. This example deliberately hand-writes its canister instead: it maintains a custom invariant (every encrypted value has a matching metadata row) and exposes metadata-aware `*_with_metadata` methods. The macro emits a fixed endpoint set — including the plain `insert_encrypted_value`/`remove_encrypted_value` mutators that would bypass and desync the metadata — and offers no way to override or omit them, so it is not a fit here. Reach for the macro when the standard Encrypted Maps interface is enough, and hand-write (as here) when you need to layer your own state on top. ### Frontend (`frontend/`) diff --git a/rust/vetkeys/password_manager_with_metadata/backend/Cargo.toml b/rust/vetkeys/password_manager_with_metadata/backend/Cargo.toml index 3480734795..75aac264ba 100644 --- a/rust/vetkeys/password_manager_with_metadata/backend/Cargo.toml +++ b/rust/vetkeys/password_manager_with_metadata/backend/Cargo.toml @@ -18,6 +18,6 @@ ic-cdk = "0.20.1" ic-cdk-management-canister = "0.1.1" ic-dummy-getrandom-for-wasm = "0.1.0" ic-stable-structures = "0.7.2" -ic-vetkeys = "0.7.0" +ic-vetkeys = "0.8.1" serde = "1.0.217" serde_cbor = "0.11.2" diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/package.json b/rust/vetkeys/password_manager_with_metadata/frontend/package.json index adaf711ac4..d0944300aa 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/package.json +++ b/rust/vetkeys/password_manager_with_metadata/frontend/package.json @@ -23,7 +23,7 @@ "dependencies": { "@icp-sdk/auth": "^7.1.0", "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0", + "@icp-sdk/vetkeys": "^0.5.0", "@sveltejs/vite-plugin-svelte": "^3.0.2", "daisyui": "^4.12.23", "svelte": "^4.2.19", diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts index 6fc0287456..d0256fc9fc 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts @@ -1,7 +1,9 @@ import { HttpAgent } from "@icp-sdk/core/agent"; +import type { Principal } from "@icp-sdk/core/principal"; import { DefaultEncryptedMapsClient, EncryptedMaps, + IndexedDbDerivedKeyMaterialCache, } from "@icp-sdk/vetkeys/encrypted_maps"; import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; @@ -9,7 +11,10 @@ const canisterEnv = safeGetCanisterEnv<{ "PUBLIC_CANISTER_ID:backend": string; }>(); -export function createEncryptedMaps(agent: HttpAgent): EncryptedMaps { +export function createEncryptedMaps( + agent: HttpAgent, + principal: Principal, +): EncryptedMaps { const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; if (!canisterId) { @@ -18,5 +23,18 @@ export function createEncryptedMaps(agent: HttpAgent): EncryptedMaps { ); } - return new EncryptedMaps(new DefaultEncryptedMapsClient(agent, canisterId)); + // Since v0.5.0 EncryptedMaps caches derived key material in memory only by + // default, so it is discarded on page reload. We opt back into persisting + // it across reloads with IndexedDbDerivedKeyMaterialCache, namespacing the + // store by the caller's principal so one identity's cached keys are never + // served to another on the same origin. Remember to call clearCache() on + // logout / identity change (see the auth store). + const cache = new IndexedDbDerivedKeyMaterialCache( + `vetkeys-${principal.toText()}`, + ); + + return new EncryptedMaps( + new DefaultEncryptedMapsClient(agent, canisterId), + { cache }, + ); } diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts index 131eacd51d..99f472a103 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password_manager.ts @@ -1,4 +1,9 @@ -import { Actor, HttpAgent, type ActorSubclass } from "@icp-sdk/core/agent"; +import { + Actor, + HttpAgent, + type ActorSubclass, + type Identity, +} from "@icp-sdk/core/agent"; import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; import type { Principal } from "@icp-sdk/core/principal"; import { EncryptedMaps } from "@icp-sdk/vetkeys/encrypted_maps"; @@ -151,9 +156,9 @@ export class PasswordManager { } } -export async function createPasswordManager(agentOptions?: { - identity?: HttpAgent["config"]["identity"]; -}): Promise { +export async function createPasswordManager( + identity: Identity, +): Promise { const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; if (!canisterId) { @@ -163,12 +168,12 @@ export async function createPasswordManager(agentOptions?: { } const agent = await HttpAgent.create({ - ...agentOptions, + identity, host: window.location.origin, rootKey: canisterEnv?.IC_ROOT_KEY, }); - const encryptedMaps = createEncryptedMaps(agent); + const encryptedMaps = createEncryptedMaps(agent, identity.getPrincipal()); const canisterClient = Actor.createActor<_SERVICE>(idlFactory, { agent, canisterId, diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts index 1f54fb574e..0fc77ca330 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts @@ -80,6 +80,9 @@ export async function logout() { const currentAuth = get(auth); if (currentAuth.state === "initialized") { + // Drop cached derived key material so a persisted (IndexedDB) handle + // can no longer be used to decrypt after logout. + await currentAuth.passwordManager.encryptedMaps.clearCache(); await currentAuth.client.signOut(); auth.update(() => ({ state: "anonymous", @@ -94,7 +97,7 @@ export async function authenticate(client: AuthClient) { try { const identity = await client.getIdentity(); - const passwordManager = await createPasswordManager({ identity }); + const passwordManager = await createPasswordManager(identity); auth.update(() => ({ state: "initialized", From 02c33fa514c8e2e244ac80b0df85d0a1371cfcb0 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 29 Jul 2026 13:51:51 +0200 Subject: [PATCH 2/4] docs(rust): document derived-key caching and how to verify clearCache on logout Explain the IndexedDB opt-in (per-identity namespace) and that clearCache() empties the derived-key-material store rather than deleting the database, so an empty vetkeys- DB remaining is expected. Add the DevTools steps to verify (incl. the Refresh-database gotcha) and clarify the unrelated icp-sdk- subnetNodeKeys DB. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/vetkeys/password_manager/README.md | 12 ++++++++++++ .../vetkeys/password_manager_with_metadata/README.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/rust/vetkeys/password_manager/README.md b/rust/vetkeys/password_manager/README.md index 492b2ad7af..4abff1c3bd 100644 --- a/rust/vetkeys/password_manager/README.md +++ b/rust/vetkeys/password_manager/README.md @@ -64,6 +64,18 @@ An **Encrypted Maps**-enabled Rust canister that securely stores passwords. A **Svelte** application providing a user-friendly interface for managing vaults and passwords. It talks to the backend through the `@icp-sdk/vetkeys` Encrypted Maps client. +#### Derived-key caching + +`EncryptedMaps` derives and caches the vetKey material used to encrypt/decrypt values. As of `@icp-sdk/vetkeys` 0.5.0 this cache is **in memory by default** (discarded on page reload). This example opts into cross-reload persistence with `IndexedDbDerivedKeyMaterialCache`, giving the store a **per-identity namespace** (`vetkeys-`) so one identity's keys are never served to another on the same origin, and it calls `EncryptedMaps.clearCache()` on logout (and on any identity change) to drop the cached material. + +**Verifying `clearCache()` on logout.** `clearCache()` *empties* the cache's object store; it does not delete the database, so an empty `vetkeys-` database remaining in the list is expected (an empty store holds no usable key material). To check it: + +1. Open DevTools → **Application → IndexedDB**. +2. Expand `vetkeys-` → `derived-key-material`. While logged in and after opening a vault it holds one or more entries; after logout it should hold **0**. +3. DevTools caches this view — click **Refresh database** after logging out, otherwise stale entries appear to linger. + +You will also see an unrelated `icp-sdk-` database (object store `subnetNodeKeys`): that is the agent's cache of public subnet node keys, not key material, and is safe to ignore. + ## Updating the Candid interface `backend/backend.did` defines the backend's public interface. If you change the backend's public API, regenerate it: diff --git a/rust/vetkeys/password_manager_with_metadata/README.md b/rust/vetkeys/password_manager_with_metadata/README.md index 1faaed437b..d04479fb70 100644 --- a/rust/vetkeys/password_manager_with_metadata/README.md +++ b/rust/vetkeys/password_manager_with_metadata/README.md @@ -67,6 +67,18 @@ An **Encrypted Maps**-enabled Rust canister that stores encrypted passwords toge A **Svelte** application for managing vaults and passwords. It uses the `@icp-sdk/vetkeys` Encrypted Maps client for the crypto operations and a canister actor (bindings generated from `backend/backend.did` by the `@icp-sdk/bindgen` Vite plugin) for the metadata methods. +#### Derived-key caching + +`EncryptedMaps` derives and caches the vetKey material used to encrypt/decrypt values. As of `@icp-sdk/vetkeys` 0.5.0 this cache is **in memory by default** (discarded on page reload). This example opts into cross-reload persistence with `IndexedDbDerivedKeyMaterialCache`, giving the store a **per-identity namespace** (`vetkeys-`) so one identity's keys are never served to another on the same origin, and it calls `EncryptedMaps.clearCache()` on logout (and on any identity change) to drop the cached material. + +**Verifying `clearCache()` on logout.** `clearCache()` *empties* the cache's object store; it does not delete the database, so an empty `vetkeys-` database remaining in the list is expected (an empty store holds no usable key material). To check it: + +1. Open DevTools → **Application → IndexedDB**. +2. Expand `vetkeys-` → `derived-key-material`. While logged in and after opening a vault it holds one or more entries; after logout it should hold **0**. +3. DevTools caches this view — click **Refresh database** after logging out, otherwise stale entries appear to linger. + +You will also see an unrelated `icp-sdk-` database (object store `subnetNodeKeys`): that is the agent's cache of public subnet node keys, not key material, and is safe to ignore. + ## Updating the Candid interface `backend/backend.did` defines the backend's public interface; the frontend bindings are generated from it during the build. If you change the backend's public API, regenerate it: From e0b4a5dd6b19ad07bf19cdf02b5bddcc92297a36 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 29 Jul 2026 14:54:07 +0200 Subject: [PATCH 3/4] fix(rust): persist metadata across upgrades in password_manager_with_metadata The METADATA StableBTreeMap was initialized with StableBTreeMap::new, which overwrites the backing memory with an empty map every time the thread-local is re-created after an upgrade. Because the read path zips the metadata iterator against the (surviving) encrypted values, an empty metadata map made every password disappear from the UI after an upgrade while the vault still listed as non-empty. Use StableBTreeMap::init so the persisted map is loaded instead. Pre-existing bug, unrelated to the 0.8.1 bump; surfaced during upgrade testing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../password_manager_with_metadata/backend/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust/vetkeys/password_manager_with_metadata/backend/src/lib.rs b/rust/vetkeys/password_manager_with_metadata/backend/src/lib.rs index 334d0509be..dffd736b0c 100644 --- a/rust/vetkeys/password_manager_with_metadata/backend/src/lib.rs +++ b/rust/vetkeys/password_manager_with_metadata/backend/src/lib.rs @@ -76,7 +76,10 @@ thread_local! { RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); static ENCRYPTED_MAPS: RefCell>> = const { RefCell::new(None) }; - static METADATA: RefCell = RefCell::new(StableBTreeMap::new( + // Use `init` (not `new`): after an upgrade the thread-local is re-created, + // and `new` would overwrite the existing map with an empty one, dropping all + // metadata. `init` loads the persisted map when the memory already holds one. + static METADATA: RefCell = RefCell::new(StableBTreeMap::init( MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(4))), )); static KEY_NAME: RefCell> = From d24899604f43e410ed449f592742f2e77c37bea9 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 29 Jul 2026 15:52:16 +0200 Subject: [PATCH 4/4] fix(rust): make logout resilient to cache-clear failures; clarify caching docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review feedback: - password_manager and password_manager_with_metadata: clear the derived-key cache best-effort on logout so sign-out still completes if IndexedDB is unavailable (private mode, quota, blocked storage). - READMEs and code comments: refer to clearCache() as an instance method and drop the "identity change" wording — the apps only clear on logout, and the per-identity cache namespace already isolates identities. - password_manager backend: reword the macro comment so the domain-separator string argument is no longer conflated with the config memory slot. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/vetkeys/password_manager/README.md | 2 +- rust/vetkeys/password_manager/backend/src/lib.rs | 9 +++++---- .../frontend/src/lib/encrypted_maps.ts | 2 +- .../password_manager/frontend/src/store/auth.ts | 12 +++++++++--- .../vetkeys/password_manager_with_metadata/README.md | 2 +- .../frontend/src/lib/encrypted_maps.ts | 2 +- .../frontend/src/store/auth.ts | 12 +++++++++--- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/rust/vetkeys/password_manager/README.md b/rust/vetkeys/password_manager/README.md index 4abff1c3bd..05dc04806a 100644 --- a/rust/vetkeys/password_manager/README.md +++ b/rust/vetkeys/password_manager/README.md @@ -66,7 +66,7 @@ A **Svelte** application providing a user-friendly interface for managing vaults #### Derived-key caching -`EncryptedMaps` derives and caches the vetKey material used to encrypt/decrypt values. As of `@icp-sdk/vetkeys` 0.5.0 this cache is **in memory by default** (discarded on page reload). This example opts into cross-reload persistence with `IndexedDbDerivedKeyMaterialCache`, giving the store a **per-identity namespace** (`vetkeys-`) so one identity's keys are never served to another on the same origin, and it calls `EncryptedMaps.clearCache()` on logout (and on any identity change) to drop the cached material. +`EncryptedMaps` derives and caches the vetKey material used to encrypt/decrypt values. As of `@icp-sdk/vetkeys` 0.5.0 this cache is **in memory by default** (discarded on page reload). This example opts into cross-reload persistence with `IndexedDbDerivedKeyMaterialCache`, giving the store a **per-identity namespace** (`vetkeys-`) so one identity's keys are never served to another on the same origin, and it calls `clearCache()` on the `EncryptedMaps` instance on logout to drop the cached material. **Verifying `clearCache()` on logout.** `clearCache()` *empties* the cache's object store; it does not delete the database, so an empty `vetkeys-` database remaining in the list is expected (an empty store holds no usable key material). To check it: diff --git a/rust/vetkeys/password_manager/backend/src/lib.rs b/rust/vetkeys/password_manager/backend/src/lib.rs index eeff5997a2..4e0c7def57 100644 --- a/rust/vetkeys/password_manager/backend/src/lib.rs +++ b/rust/vetkeys/password_manager/backend/src/lib.rs @@ -19,10 +19,11 @@ fn memory(id: u8) -> Memory { MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(id))) } -// The first argument is the domain separator that isolates this application's -// derived keys; it must stay stable for the life of the canister. The four -// memories are, in order: [domain_separator, access_control, shared_keys, -// encrypted_maps]. +// The first argument is the domain-separator string that isolates this +// application's derived keys; it must stay stable for the life of the canister. +// The four Memory instances back EncryptedMaps' own stable state, in the order +// it expects: config (which persists the domain separator and vetKD key id), +// access control, shared keys, and the encrypted values. ic_vetkeys::export_encrypted_maps_canister!( "encrypted_maps_app", [memory(0), memory(1), memory(2), memory(3)], diff --git a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts index f929f14a7d..20dc6bcfe9 100644 --- a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts +++ b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts @@ -33,7 +33,7 @@ export async function createEncryptedMaps( // it across reloads with IndexedDbDerivedKeyMaterialCache, namespacing the // store by the caller's principal so one identity's cached keys are never // served to another on the same origin. Remember to call clearCache() on - // logout / identity change (see the auth store). + // logout (see the auth store). const cache = new IndexedDbDerivedKeyMaterialCache( `vetkeys-${identity.getPrincipal().toText()}`, ); diff --git a/rust/vetkeys/password_manager/frontend/src/store/auth.ts b/rust/vetkeys/password_manager/frontend/src/store/auth.ts index 3adfca2b11..9634492e55 100644 --- a/rust/vetkeys/password_manager/frontend/src/store/auth.ts +++ b/rust/vetkeys/password_manager/frontend/src/store/auth.ts @@ -79,9 +79,15 @@ export async function logout() { const currentAuth = get(auth); if (currentAuth.state === "initialized") { - // Drop cached derived key material so a persisted (IndexedDB) handle - // can no longer be used to decrypt after logout. - await currentAuth.encryptedMaps.clearCache(); + // Best-effort: drop cached derived key material so a persisted + // (IndexedDB) handle can no longer be used to decrypt after logout. This + // must not block sign-out if IndexedDB is unavailable (private mode, + // quota, blocked storage), so failures are logged and swallowed. + try { + await currentAuth.encryptedMaps.clearCache(); + } catch (e) { + console.error("Failed to clear derived-key cache on logout:", e); + } await currentAuth.client.signOut(); auth.update(() => ({ state: "anonymous", diff --git a/rust/vetkeys/password_manager_with_metadata/README.md b/rust/vetkeys/password_manager_with_metadata/README.md index d04479fb70..c3348538fe 100644 --- a/rust/vetkeys/password_manager_with_metadata/README.md +++ b/rust/vetkeys/password_manager_with_metadata/README.md @@ -69,7 +69,7 @@ A **Svelte** application for managing vaults and passwords. It uses the `@icp-sd #### Derived-key caching -`EncryptedMaps` derives and caches the vetKey material used to encrypt/decrypt values. As of `@icp-sdk/vetkeys` 0.5.0 this cache is **in memory by default** (discarded on page reload). This example opts into cross-reload persistence with `IndexedDbDerivedKeyMaterialCache`, giving the store a **per-identity namespace** (`vetkeys-`) so one identity's keys are never served to another on the same origin, and it calls `EncryptedMaps.clearCache()` on logout (and on any identity change) to drop the cached material. +`EncryptedMaps` derives and caches the vetKey material used to encrypt/decrypt values. As of `@icp-sdk/vetkeys` 0.5.0 this cache is **in memory by default** (discarded on page reload). This example opts into cross-reload persistence with `IndexedDbDerivedKeyMaterialCache`, giving the store a **per-identity namespace** (`vetkeys-`) so one identity's keys are never served to another on the same origin, and it calls `clearCache()` on the `EncryptedMaps` instance on logout to drop the cached material. **Verifying `clearCache()` on logout.** `clearCache()` *empties* the cache's object store; it does not delete the database, so an empty `vetkeys-` database remaining in the list is expected (an empty store holds no usable key material). To check it: diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts index d0256fc9fc..7fe62197eb 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/encrypted_maps.ts @@ -28,7 +28,7 @@ export function createEncryptedMaps( // it across reloads with IndexedDbDerivedKeyMaterialCache, namespacing the // store by the caller's principal so one identity's cached keys are never // served to another on the same origin. Remember to call clearCache() on - // logout / identity change (see the auth store). + // logout (see the auth store). const cache = new IndexedDbDerivedKeyMaterialCache( `vetkeys-${principal.toText()}`, ); diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts index 0fc77ca330..8beea52a6f 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/auth.ts @@ -80,9 +80,15 @@ export async function logout() { const currentAuth = get(auth); if (currentAuth.state === "initialized") { - // Drop cached derived key material so a persisted (IndexedDB) handle - // can no longer be used to decrypt after logout. - await currentAuth.passwordManager.encryptedMaps.clearCache(); + // Best-effort: drop cached derived key material so a persisted + // (IndexedDB) handle can no longer be used to decrypt after logout. This + // must not block sign-out if IndexedDB is unavailable (private mode, + // quota, blocked storage), so failures are logged and swallowed. + try { + await currentAuth.passwordManager.encryptedMaps.clearCache(); + } catch (e) { + console.error("Failed to clear derived-key cache on logout:", e); + } await currentAuth.client.signOut(); auth.update(() => ({ state: "anonymous",